mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-04-28 14:31:23 +00:00
split main.rs into main.rs and app.rs
This commit is contained in:
+457
@@ -0,0 +1,457 @@
|
|||||||
|
use eframe::{CreationContext, Frame};
|
||||||
|
use egui::{
|
||||||
|
Button, CentralPanel, ComboBox, Context, Label, ScrollArea, Separator, Slider, TextEdit, Ui,
|
||||||
|
Vec2,
|
||||||
|
};
|
||||||
|
use egui_material_icons::icons;
|
||||||
|
use metadata::media_file::MediaFileMetadata;
|
||||||
|
use rfd::FileDialog;
|
||||||
|
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
mod pw;
|
||||||
|
|
||||||
|
enum PlayerState {
|
||||||
|
PLAYING,
|
||||||
|
PAUSED,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlayerState {
|
||||||
|
fn default() -> Self {
|
||||||
|
PlayerState::PAUSED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
player_position: f32,
|
||||||
|
prev_player_position: f32,
|
||||||
|
max_player_position: f32,
|
||||||
|
volume: f32,
|
||||||
|
|
||||||
|
player_state: PlayerState,
|
||||||
|
|
||||||
|
directories: Vec<PathBuf>,
|
||||||
|
directory_to_delete: Option<usize>,
|
||||||
|
current_directory: Option<usize>,
|
||||||
|
|
||||||
|
selected_input_device: String,
|
||||||
|
available_input_devices: Vec<pw::InputDevice>,
|
||||||
|
|
||||||
|
current_file: PathBuf,
|
||||||
|
|
||||||
|
search_query: String,
|
||||||
|
|
||||||
|
_audio_stream: OutputStream,
|
||||||
|
_audio_stream_handle: OutputStreamHandle,
|
||||||
|
audio_sink: Sink,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(_cc: &CreationContext<'_>) -> Self {
|
||||||
|
let saved_dirs_path = dirs::config_dir().unwrap().join("pwsp").join("saved_dirs");
|
||||||
|
let saved_dirs_content = fs::read_to_string(&saved_dirs_path).unwrap_or_default();
|
||||||
|
let directories: Vec<_> = saved_dirs_content.lines().map(PathBuf::from).collect();
|
||||||
|
let current_directory = match directories.is_empty() {
|
||||||
|
false => Some(0),
|
||||||
|
true => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let saved_mic_path = dirs::config_dir().unwrap().join("pwsp").join("saved_mic");
|
||||||
|
let selected_input_device = fs::read_to_string(&saved_mic_path).unwrap_or_default();
|
||||||
|
|
||||||
|
let saved_volume_path = dirs::config_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join("pwsp")
|
||||||
|
.join("saved_volume");
|
||||||
|
let saved_volume = fs::read_to_string(&saved_volume_path).unwrap_or_default();
|
||||||
|
let volume = match saved_volume.is_empty() {
|
||||||
|
true => 1.0,
|
||||||
|
false => saved_volume.parse().unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_audio_stream, _audio_stream_handle) = OutputStream::try_default().unwrap();
|
||||||
|
let audio_sink = Sink::try_new(&_audio_stream_handle).unwrap();
|
||||||
|
audio_sink.pause();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
player_position: 0.0,
|
||||||
|
prev_player_position: 0.0,
|
||||||
|
max_player_position: 1.0,
|
||||||
|
volume,
|
||||||
|
|
||||||
|
player_state: PlayerState::PAUSED,
|
||||||
|
|
||||||
|
directories,
|
||||||
|
directory_to_delete: None,
|
||||||
|
current_directory,
|
||||||
|
|
||||||
|
selected_input_device,
|
||||||
|
available_input_devices: Vec::new(),
|
||||||
|
|
||||||
|
current_file: PathBuf::new(),
|
||||||
|
|
||||||
|
search_query: String::new(),
|
||||||
|
|
||||||
|
_audio_stream,
|
||||||
|
_audio_stream_handle,
|
||||||
|
audio_sink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upd(&mut self, ui: &mut Ui, ctx: &Context, _frame: &mut Frame) {
|
||||||
|
self.render_ui(ui);
|
||||||
|
|
||||||
|
self.available_input_devices = pw::get_input_devices().unwrap();
|
||||||
|
|
||||||
|
let saved_mic_path = dirs::config_dir().unwrap().join("pwsp").join("saved_mic");
|
||||||
|
let saved_mic_content = fs::read_to_string(&saved_mic_path).unwrap_or_default();
|
||||||
|
if self.selected_input_device != saved_mic_content {
|
||||||
|
fs::write(saved_mic_path, self.selected_input_device.clone()).ok();
|
||||||
|
}
|
||||||
|
let saved_volume_path = dirs::config_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join("pwsp")
|
||||||
|
.join("saved_volume");
|
||||||
|
fs::write(saved_volume_path, self.volume.to_string()).ok();
|
||||||
|
|
||||||
|
if self.audio_sink.len() == 0 && !self.audio_sink.is_paused() {
|
||||||
|
self.audio_sink.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.player_state = match self.audio_sink.is_paused() {
|
||||||
|
true => PlayerState::PAUSED,
|
||||||
|
false => PlayerState::PLAYING,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.player_position != self.prev_player_position {
|
||||||
|
let mut target_pos = self.player_position - 0.5;
|
||||||
|
target_pos = target_pos.clamp(0f32, self.max_player_position);
|
||||||
|
|
||||||
|
let target_pos_dur = core::time::Duration::from_secs_f32(target_pos);
|
||||||
|
self.audio_sink.try_seek(target_pos_dur).unwrap();
|
||||||
|
self.prev_player_position = self.player_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let PlayerState::PLAYING = self.player_state {
|
||||||
|
ctx.request_repaint();
|
||||||
|
self.audio_sink.set_volume(self.volume);
|
||||||
|
|
||||||
|
self.player_position = self.audio_sink.get_pos().as_secs_f32();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prev_player_position = self.player_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_ui(&mut self, ui: &mut Ui) {
|
||||||
|
self.render_player(ui);
|
||||||
|
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
self.render_content(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_player(&mut self, ui: &mut Ui) {
|
||||||
|
let file_title_label = Label::new(
|
||||||
|
self.current_file
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or(""),
|
||||||
|
);
|
||||||
|
|
||||||
|
let play_button_icon = match self.player_state {
|
||||||
|
PlayerState::PLAYING => icons::ICON_PAUSE,
|
||||||
|
PlayerState::PAUSED => icons::ICON_PLAY_ARROW,
|
||||||
|
};
|
||||||
|
let play_button = Button::new(play_button_icon).corner_radius(15.0);
|
||||||
|
|
||||||
|
let position_minutes = (self.player_position / 60.0) as i32;
|
||||||
|
let position_seconds = (self.player_position - (position_minutes as f32) * 60.0) as i32;
|
||||||
|
let position_minutes_str = match position_minutes.to_string().chars().count() {
|
||||||
|
1 => "0".to_string() + &position_minutes.to_string(),
|
||||||
|
2 => position_minutes.to_string(),
|
||||||
|
_ => "00".to_string(),
|
||||||
|
};
|
||||||
|
let position_seconds_str = match position_seconds.to_string().chars().count() {
|
||||||
|
1 => "0".to_string() + &position_seconds.to_string(),
|
||||||
|
2 => position_seconds.to_string(),
|
||||||
|
_ => "00".to_string(),
|
||||||
|
};
|
||||||
|
let player_position_label =
|
||||||
|
Label::new(format!("{}:{}", position_minutes_str, position_seconds_str));
|
||||||
|
|
||||||
|
let player_position_slider =
|
||||||
|
Slider::new(&mut self.player_position, 0.0..=self.max_player_position)
|
||||||
|
.show_value(false);
|
||||||
|
|
||||||
|
let volume_slider = Slider::new(&mut self.volume, 0.0..=1.0).show_value(false);
|
||||||
|
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
ui.horizontal_top(|ui| {
|
||||||
|
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
|
||||||
|
if !self.current_file.display().to_string().is_empty() && play_button_response.clicked()
|
||||||
|
{
|
||||||
|
match self.player_state {
|
||||||
|
PlayerState::PLAYING => {
|
||||||
|
self.audio_sink.pause();
|
||||||
|
}
|
||||||
|
PlayerState::PAUSED => {
|
||||||
|
self.audio_sink.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.spacing_mut().slider_width = ui.available_width() - 150.0;
|
||||||
|
ui.add_sized([ui.available_width() - 150.0, 15.0], file_title_label);
|
||||||
|
ui.add_sized([ui.available_width() - 150.0, 15.0], player_position_slider);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_sized([30.0, 30.0], player_position_label);
|
||||||
|
ui.add_sized([15.0, 30.0], volume_slider);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_content(&mut self, ui: &mut Ui) {
|
||||||
|
let dirs_area_size = Vec2::new(120.0, ui.available_height());
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
self.render_directories_list(ui, dirs_area_size);
|
||||||
|
self.render_mic_selection(ui);
|
||||||
|
});
|
||||||
|
ui.allocate_ui(dirs_area_size, |ui| {
|
||||||
|
if !self.directories.is_empty() {
|
||||||
|
ui.add(Separator::default().vertical());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let music_area_size = Vec2::new(ui.available_width(), ui.available_height());
|
||||||
|
self.render_music_list(ui, music_area_size);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.handle_directory_deletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_directories_list(&mut self, ui: &mut Ui, scroll_area_size: Vec2) {
|
||||||
|
let add_dir_button = Button::new(icons::ICON_ADD).frame(false);
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
let add_dir_button_response = ui.add_sized([20.0, 20.0], add_dir_button);
|
||||||
|
if add_dir_button_response.clicked() {
|
||||||
|
self.handle_directory_adding();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
ui.allocate_ui(scroll_area_size, |ui| {
|
||||||
|
ScrollArea::vertical().id_salt(0).show(ui, |ui| {
|
||||||
|
for (index, dir) in self.directories.iter().enumerate() {
|
||||||
|
let dir_name = dir
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("Invalid Directory");
|
||||||
|
let dir_button = Button::new(dir_name).frame(false);
|
||||||
|
|
||||||
|
let dir_delete_button = Button::new(icons::ICON_DELETE).frame(false);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let dir_button_response = ui.add(dir_button);
|
||||||
|
if dir_button_response.clicked() {
|
||||||
|
self.current_directory = Some(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
let directory_delete_button_response = ui.add(dir_delete_button);
|
||||||
|
if directory_delete_button_response.clicked() {
|
||||||
|
self.directory_to_delete = Some(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_directory_adding(&mut self) {
|
||||||
|
if let Some(dirs) = FileDialog::pick_folders(Default::default()) {
|
||||||
|
let saved_dirs_path = dirs::config_dir().unwrap().join("pwsp").join("saved_dirs");
|
||||||
|
if let Ok(mut file) = fs::OpenOptions::new().append(true).open(saved_dirs_path) {
|
||||||
|
for path in dirs {
|
||||||
|
if !self.directories.contains(&path) {
|
||||||
|
self.directories.push(path.clone());
|
||||||
|
writeln!(file, "{}", path.display()).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_directory_deletion(&mut self) {
|
||||||
|
if let Some(index) = self.directory_to_delete {
|
||||||
|
if let Some(current_index) = self.current_directory {
|
||||||
|
if current_index > index {
|
||||||
|
self.current_directory = Some(current_index - 1);
|
||||||
|
} else if current_index == index {
|
||||||
|
self.current_directory = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.directories.remove(index);
|
||||||
|
self.directory_to_delete = None;
|
||||||
|
|
||||||
|
let saved_dirs_path = dirs::config_dir().unwrap().join("pwsp").join("saved_dirs");
|
||||||
|
let content = self
|
||||||
|
.directories
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
fs::write(saved_dirs_path, content).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_music_list(&mut self, ui: &mut Ui, scroll_area_size: Vec2) {
|
||||||
|
if self.current_directory.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_path = self
|
||||||
|
.directories
|
||||||
|
.get(self.current_directory.unwrap())
|
||||||
|
.unwrap();
|
||||||
|
if !fs::exists(current_path).ok().unwrap_or(false) {
|
||||||
|
self.directory_to_delete = self.current_directory;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = fs::read_dir(current_path).ok().unwrap();
|
||||||
|
let mut music_files = Vec::new();
|
||||||
|
let music_extensions = ["mp3", "wav", "ogg", "flac", "mp4", "aac"].to_vec();
|
||||||
|
for file in files {
|
||||||
|
let path = file.unwrap().path();
|
||||||
|
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(extension) = path.extension().and_then(|n| n.to_str()) {
|
||||||
|
if music_extensions.contains(&extension.to_lowercase().as_str()) {
|
||||||
|
if path
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
.to_lowercase()
|
||||||
|
.contains(self.search_query.as_str())
|
||||||
|
{
|
||||||
|
music_files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
let search_entry = TextEdit::singleline(&mut self.search_query);
|
||||||
|
ui.add_sized([ui.available_width(), 20.0], search_entry);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.allocate_ui(scroll_area_size, |ui| {
|
||||||
|
ScrollArea::vertical().id_salt(1).show(ui, |ui| {
|
||||||
|
for file in music_files.iter() {
|
||||||
|
let file_name = file.file_name().and_then(|n| n.to_str()).unwrap();
|
||||||
|
let file_button = Button::new(file_name).frame(false);
|
||||||
|
let file_button_response = ui.add(file_button);
|
||||||
|
if file_button_response.clicked() {
|
||||||
|
self.current_file = file.to_path_buf();
|
||||||
|
self.play_current_file();
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_mic_selection(&mut self, ui: &mut Ui) {
|
||||||
|
ComboBox::from_label("Choose MIC")
|
||||||
|
.selected_text(format!("{:?}", self.selected_input_device))
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for device in self.available_input_devices.iter() {
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.selected_input_device,
|
||||||
|
device.audio_device.name.clone(),
|
||||||
|
device.audio_device.nick.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_current_file(&mut self) {
|
||||||
|
if self.current_file.to_str().unwrap().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.current_file.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.selected_input_device.is_empty() {
|
||||||
|
self.link_devices();
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = fs::File::open(self.current_file.display().to_string()).unwrap();
|
||||||
|
let source = Decoder::new(file).unwrap();
|
||||||
|
|
||||||
|
self.audio_sink.stop();
|
||||||
|
self.audio_sink.play();
|
||||||
|
self.audio_sink.append(source);
|
||||||
|
|
||||||
|
let metadata = MediaFileMetadata::new(&self.current_file.as_path()).unwrap();
|
||||||
|
self.max_player_position = metadata._duration.unwrap() as f32;
|
||||||
|
self.player_position = 0.0;
|
||||||
|
self.prev_player_position = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_devices(&self) {
|
||||||
|
let output_devices = pw::get_output_devices().unwrap();
|
||||||
|
let mut pwsp_output: Option<&pw::OutputDevice> = None;
|
||||||
|
|
||||||
|
for device in output_devices.iter() {
|
||||||
|
if device.audio_device.name == "alsa_playback.pwsp" {
|
||||||
|
pwsp_output = Some(device);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pwsp_output.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_devices = pw::get_input_devices().unwrap();
|
||||||
|
let mut mic: Option<&pw::InputDevice> = None;
|
||||||
|
|
||||||
|
for device in input_devices.iter() {
|
||||||
|
if device.audio_device.name == self.selected_input_device {
|
||||||
|
mic = Some(device);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mic.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pwsp_output.unwrap().unlink(mic.unwrap());
|
||||||
|
pwsp_output.unwrap().link(mic.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for App {
|
||||||
|
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
|
||||||
|
CentralPanel::default().show(ctx, |ui| self.upd(ui, ctx, frame));
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-31
@@ -1,39 +1,37 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
|
|
||||||
pub struct AudioDevice {
|
pub struct AudioDevice {
|
||||||
pub nick: String,
|
pub nick: String,
|
||||||
pub name: String
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct InputDevice {
|
pub struct InputDevice {
|
||||||
pub audio_device: AudioDevice,
|
pub audio_device: AudioDevice,
|
||||||
pub input_fl: String,
|
pub input_fl: String,
|
||||||
pub input_fr: String
|
pub input_fr: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct OutputDevice {
|
pub struct OutputDevice {
|
||||||
pub audio_device: AudioDevice,
|
pub audio_device: AudioDevice,
|
||||||
pub output_fl: String,
|
pub output_fl: String,
|
||||||
pub output_fr: String
|
pub output_fr: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl OutputDevice {
|
impl OutputDevice {
|
||||||
pub fn link(&self, input_device: &InputDevice) {
|
pub fn link(&self, input_device: &InputDevice) {
|
||||||
Command::new("pw-link")
|
Command::new("pw-link")
|
||||||
.arg(&self.output_fl)
|
.arg(&self.output_fl)
|
||||||
.arg(&input_device.input_fl)
|
.arg(&input_device.input_fl)
|
||||||
.status().ok();
|
.status()
|
||||||
|
.ok();
|
||||||
|
|
||||||
Command::new("pw-link")
|
Command::new("pw-link")
|
||||||
.arg(&self.output_fr)
|
.arg(&self.output_fr)
|
||||||
.arg(&input_device.input_fr)
|
.arg(&input_device.input_fr)
|
||||||
.status().ok();
|
.status()
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unlink(&self, input_device: &InputDevice) {
|
pub fn unlink(&self, input_device: &InputDevice) {
|
||||||
@@ -41,17 +39,18 @@ impl OutputDevice {
|
|||||||
.arg("--disconnect")
|
.arg("--disconnect")
|
||||||
.arg(&self.output_fl)
|
.arg(&self.output_fl)
|
||||||
.arg(&input_device.input_fl)
|
.arg(&input_device.input_fl)
|
||||||
.status().ok();
|
.status()
|
||||||
|
.ok();
|
||||||
|
|
||||||
Command::new("pw-link")
|
Command::new("pw-link")
|
||||||
.arg("--disconnect")
|
.arg("--disconnect")
|
||||||
.arg(&self.output_fr)
|
.arg(&self.output_fr)
|
||||||
.arg(&input_device.input_fr)
|
.arg(&input_device.input_fr)
|
||||||
.status().ok();
|
.status()
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn get_pw_entries() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
fn get_pw_entries() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
||||||
let output = Command::new("pw-cli")
|
let output = Command::new("pw-cli")
|
||||||
.args(&["ls", "Node"])
|
.args(&["ls", "Node"])
|
||||||
@@ -67,7 +66,7 @@ fn get_pw_entries() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
|||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
|
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if line.starts_with("id ") {
|
if line.starts_with("id ") {
|
||||||
@@ -80,12 +79,18 @@ fn get_pw_entries() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
|||||||
|
|
||||||
let key = match parts.next() {
|
let key = match parts.next() {
|
||||||
Some(k) => k.trim().to_string(),
|
Some(k) => k.trim().to_string(),
|
||||||
None => continue
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let value = match parts.next() {
|
let value = match parts.next() {
|
||||||
Some(k) => k.trim().strip_prefix("\"").unwrap().strip_suffix("\"").unwrap().to_string(),
|
Some(k) => k
|
||||||
None => continue
|
.trim()
|
||||||
|
.strip_prefix("\"")
|
||||||
|
.unwrap()
|
||||||
|
.strip_suffix("\"")
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
current_entry.insert(key, value);
|
current_entry.insert(key, value);
|
||||||
@@ -106,21 +111,24 @@ pub fn get_input_devices() -> Result<Vec<InputDevice>, Box<dyn Error>> {
|
|||||||
|
|
||||||
for entry in entries.iter() {
|
for entry in entries.iter() {
|
||||||
let media_class = entry.get("media.class").map(String::as_str).unwrap_or("");
|
let media_class = entry.get("media.class").map(String::as_str).unwrap_or("");
|
||||||
let nick = entry.get("node.nick").map(String::as_str)
|
let nick = entry.get("node.nick").map(String::as_str).unwrap_or(
|
||||||
.unwrap_or(entry.get("node.description").map(String::as_str)
|
entry
|
||||||
.unwrap_or(entry.get("node.name").map(String::as_str).unwrap_or("")));
|
.get("node.description")
|
||||||
|
.map(String::as_str)
|
||||||
|
.unwrap_or(entry.get("node.name").map(String::as_str).unwrap_or("")),
|
||||||
|
);
|
||||||
let name = entry.get("node.name").map(String::as_str).unwrap_or("");
|
let name = entry.get("node.name").map(String::as_str).unwrap_or("");
|
||||||
|
|
||||||
if media_class.is_empty() {
|
if media_class.is_empty() {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !media_class.starts_with(&"Audio/Source") {
|
if !media_class.starts_with(&"Audio/Source") {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if nick.is_empty() || name.is_empty() {
|
if nick.is_empty() || name.is_empty() {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let audio_device = AudioDevice {
|
let audio_device = AudioDevice {
|
||||||
@@ -131,7 +139,7 @@ pub fn get_input_devices() -> Result<Vec<InputDevice>, Box<dyn Error>> {
|
|||||||
let device = InputDevice {
|
let device = InputDevice {
|
||||||
audio_device,
|
audio_device,
|
||||||
input_fl: String::new(),
|
input_fl: String::new(),
|
||||||
input_fr: String::new()
|
input_fr: String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
input_devices.push(device);
|
input_devices.push(device);
|
||||||
@@ -171,21 +179,24 @@ pub fn get_output_devices() -> Result<Vec<OutputDevice>, Box<dyn Error>> {
|
|||||||
|
|
||||||
for entry in entries.iter() {
|
for entry in entries.iter() {
|
||||||
let media_class = entry.get("media.class").map(String::as_str).unwrap_or("");
|
let media_class = entry.get("media.class").map(String::as_str).unwrap_or("");
|
||||||
let nick = entry.get("node.nick").map(String::as_str)
|
let nick = entry.get("node.nick").map(String::as_str).unwrap_or(
|
||||||
.unwrap_or(entry.get("node.description").map(String::as_str)
|
entry
|
||||||
.unwrap_or(entry.get("node.name").map(String::as_str).unwrap_or("")));
|
.get("node.description")
|
||||||
|
.map(String::as_str)
|
||||||
|
.unwrap_or(entry.get("node.name").map(String::as_str).unwrap_or("")),
|
||||||
|
);
|
||||||
let name = entry.get("node.name").map(String::as_str).unwrap_or("");
|
let name = entry.get("node.name").map(String::as_str).unwrap_or("");
|
||||||
|
|
||||||
if media_class.is_empty() {
|
if media_class.is_empty() {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !media_class.starts_with(&"Stream/Output/Audio") {
|
if !media_class.starts_with(&"Stream/Output/Audio") {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if nick.is_empty() || name.is_empty() {
|
if nick.is_empty() || name.is_empty() {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let audio_device = AudioDevice {
|
let audio_device = AudioDevice {
|
||||||
@@ -196,7 +207,7 @@ pub fn get_output_devices() -> Result<Vec<OutputDevice>, Box<dyn Error>> {
|
|||||||
let device = OutputDevice {
|
let device = OutputDevice {
|
||||||
audio_device,
|
audio_device,
|
||||||
output_fl: String::new(),
|
output_fl: String::new(),
|
||||||
output_fr: String::new()
|
output_fr: String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
output_devices.push(device);
|
output_devices.push(device);
|
||||||
+5
-457
@@ -1,460 +1,8 @@
|
|||||||
use eframe::{CreationContext, Frame, NativeOptions};
|
use eframe::NativeOptions;
|
||||||
use egui::{
|
use egui::Vec2;
|
||||||
Button, CentralPanel, ComboBox, Context, Label, ScrollArea, Separator, Slider, TextEdit, Ui,
|
use std::fs;
|
||||||
Vec2,
|
|
||||||
};
|
|
||||||
use egui_material_icons::icons;
|
|
||||||
use metadata::media_file::MediaFileMetadata;
|
|
||||||
use rfd::FileDialog;
|
|
||||||
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::{fs, path::PathBuf};
|
|
||||||
|
|
||||||
mod pw;
|
mod app;
|
||||||
|
|
||||||
enum PlayerState {
|
|
||||||
PLAYING,
|
|
||||||
PAUSED,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PlayerState {
|
|
||||||
fn default() -> Self {
|
|
||||||
PlayerState::PAUSED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct App {
|
|
||||||
player_position: f32,
|
|
||||||
prev_player_position: f32,
|
|
||||||
max_player_position: f32,
|
|
||||||
volume: f32,
|
|
||||||
|
|
||||||
player_state: PlayerState,
|
|
||||||
|
|
||||||
directories: Vec<PathBuf>,
|
|
||||||
directory_to_delete: Option<usize>,
|
|
||||||
current_directory: Option<usize>,
|
|
||||||
|
|
||||||
selected_input_device: String,
|
|
||||||
available_input_devices: Vec<pw::InputDevice>,
|
|
||||||
|
|
||||||
current_file: PathBuf,
|
|
||||||
|
|
||||||
search_query: String,
|
|
||||||
|
|
||||||
_audio_stream: OutputStream,
|
|
||||||
_audio_stream_handle: OutputStreamHandle,
|
|
||||||
audio_sink: Sink,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
pub fn new(_cc: &CreationContext<'_>) -> Self {
|
|
||||||
let saved_dirs_path = dirs::config_dir().unwrap().join("pwsp").join("saved_dirs");
|
|
||||||
let saved_dirs_content = fs::read_to_string(&saved_dirs_path).unwrap_or_default();
|
|
||||||
let directories: Vec<_> = saved_dirs_content.lines().map(PathBuf::from).collect();
|
|
||||||
let current_directory = match directories.is_empty() {
|
|
||||||
false => Some(0),
|
|
||||||
true => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let saved_mic_path = dirs::config_dir().unwrap().join("pwsp").join("saved_mic");
|
|
||||||
let selected_input_device = fs::read_to_string(&saved_mic_path).unwrap_or_default();
|
|
||||||
|
|
||||||
let saved_volume_path = dirs::config_dir()
|
|
||||||
.unwrap()
|
|
||||||
.join("pwsp")
|
|
||||||
.join("saved_volume");
|
|
||||||
let saved_volume = fs::read_to_string(&saved_volume_path).unwrap_or_default();
|
|
||||||
let volume = match saved_volume.is_empty() {
|
|
||||||
true => 1.0,
|
|
||||||
false => saved_volume.parse().unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (_audio_stream, _audio_stream_handle) = OutputStream::try_default().unwrap();
|
|
||||||
let audio_sink = Sink::try_new(&_audio_stream_handle).unwrap();
|
|
||||||
audio_sink.pause();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
player_position: 0.0,
|
|
||||||
prev_player_position: 0.0,
|
|
||||||
max_player_position: 1.0,
|
|
||||||
volume,
|
|
||||||
|
|
||||||
player_state: PlayerState::PAUSED,
|
|
||||||
|
|
||||||
directories,
|
|
||||||
directory_to_delete: None,
|
|
||||||
current_directory,
|
|
||||||
|
|
||||||
selected_input_device,
|
|
||||||
available_input_devices: Vec::new(),
|
|
||||||
|
|
||||||
current_file: PathBuf::new(),
|
|
||||||
|
|
||||||
search_query: String::new(),
|
|
||||||
|
|
||||||
_audio_stream,
|
|
||||||
_audio_stream_handle,
|
|
||||||
audio_sink,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upd(&mut self, ui: &mut Ui, ctx: &Context, _frame: &mut Frame) {
|
|
||||||
self.render_ui(ui);
|
|
||||||
|
|
||||||
self.available_input_devices = pw::get_input_devices().unwrap();
|
|
||||||
|
|
||||||
let saved_mic_path = dirs::config_dir().unwrap().join("pwsp").join("saved_mic");
|
|
||||||
let saved_mic_content = fs::read_to_string(&saved_mic_path).unwrap_or_default();
|
|
||||||
if self.selected_input_device != saved_mic_content {
|
|
||||||
fs::write(saved_mic_path, self.selected_input_device.clone()).ok();
|
|
||||||
}
|
|
||||||
let saved_volume_path = dirs::config_dir()
|
|
||||||
.unwrap()
|
|
||||||
.join("pwsp")
|
|
||||||
.join("saved_volume");
|
|
||||||
fs::write(saved_volume_path, self.volume.to_string()).ok();
|
|
||||||
|
|
||||||
if self.audio_sink.len() == 0 && !self.audio_sink.is_paused() {
|
|
||||||
self.audio_sink.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.player_state = match self.audio_sink.is_paused() {
|
|
||||||
true => PlayerState::PAUSED,
|
|
||||||
false => PlayerState::PLAYING,
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.player_position != self.prev_player_position {
|
|
||||||
let mut target_pos = self.player_position - 0.5;
|
|
||||||
target_pos = target_pos.clamp(0f32, self.max_player_position);
|
|
||||||
|
|
||||||
let target_pos_dur = core::time::Duration::from_secs_f32(target_pos);
|
|
||||||
self.audio_sink.try_seek(target_pos_dur).unwrap();
|
|
||||||
self.prev_player_position = self.player_position;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let PlayerState::PLAYING = self.player_state {
|
|
||||||
ctx.request_repaint();
|
|
||||||
self.audio_sink.set_volume(self.volume);
|
|
||||||
|
|
||||||
self.player_position = self.audio_sink.get_pos().as_secs_f32();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.prev_player_position = self.player_position;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_ui(&mut self, ui: &mut Ui) {
|
|
||||||
self.render_player(ui);
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
self.render_content(ui);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_player(&mut self, ui: &mut Ui) {
|
|
||||||
let file_title_label = Label::new(
|
|
||||||
self.current_file
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or(""),
|
|
||||||
);
|
|
||||||
|
|
||||||
let play_button_icon = match self.player_state {
|
|
||||||
PlayerState::PLAYING => icons::ICON_PAUSE,
|
|
||||||
PlayerState::PAUSED => icons::ICON_PLAY_ARROW,
|
|
||||||
};
|
|
||||||
let play_button = Button::new(play_button_icon).corner_radius(15.0);
|
|
||||||
|
|
||||||
let position_minutes = (self.player_position / 60.0) as i32;
|
|
||||||
let position_seconds = (self.player_position - (position_minutes as f32) * 60.0) as i32;
|
|
||||||
let position_minutes_str = match position_minutes.to_string().chars().count() {
|
|
||||||
1 => "0".to_string() + &position_minutes.to_string(),
|
|
||||||
2 => position_minutes.to_string(),
|
|
||||||
_ => "00".to_string(),
|
|
||||||
};
|
|
||||||
let position_seconds_str = match position_seconds.to_string().chars().count() {
|
|
||||||
1 => "0".to_string() + &position_seconds.to_string(),
|
|
||||||
2 => position_seconds.to_string(),
|
|
||||||
_ => "00".to_string(),
|
|
||||||
};
|
|
||||||
let player_position_label =
|
|
||||||
Label::new(format!("{}:{}", position_minutes_str, position_seconds_str));
|
|
||||||
|
|
||||||
let player_position_slider =
|
|
||||||
Slider::new(&mut self.player_position, 0.0..=self.max_player_position)
|
|
||||||
.show_value(false);
|
|
||||||
|
|
||||||
let volume_slider = Slider::new(&mut self.volume, 0.0..=1.0).show_value(false);
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
ui.horizontal_top(|ui| {
|
|
||||||
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
|
|
||||||
if !self.current_file.display().to_string().is_empty() && play_button_response.clicked()
|
|
||||||
{
|
|
||||||
match self.player_state {
|
|
||||||
PlayerState::PLAYING => {
|
|
||||||
self.audio_sink.pause();
|
|
||||||
}
|
|
||||||
PlayerState::PAUSED => {
|
|
||||||
self.audio_sink.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.spacing_mut().slider_width = ui.available_width() - 150.0;
|
|
||||||
ui.add_sized([ui.available_width() - 150.0, 15.0], file_title_label);
|
|
||||||
ui.add_sized([ui.available_width() - 150.0, 15.0], player_position_slider);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_sized([30.0, 30.0], player_position_label);
|
|
||||||
ui.add_sized([15.0, 30.0], volume_slider);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_content(&mut self, ui: &mut Ui) {
|
|
||||||
let dirs_area_size = Vec2::new(120.0, ui.available_height());
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
self.render_directories_list(ui, dirs_area_size);
|
|
||||||
self.render_mic_selection(ui);
|
|
||||||
});
|
|
||||||
ui.allocate_ui(dirs_area_size, |ui| {
|
|
||||||
if !self.directories.is_empty() {
|
|
||||||
ui.add(Separator::default().vertical());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let music_area_size = Vec2::new(ui.available_width(), ui.available_height());
|
|
||||||
self.render_music_list(ui, music_area_size);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.handle_directory_deletion();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_directories_list(&mut self, ui: &mut Ui, scroll_area_size: Vec2) {
|
|
||||||
let add_dir_button = Button::new(icons::ICON_ADD).frame(false);
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
let add_dir_button_response = ui.add_sized([20.0, 20.0], add_dir_button);
|
|
||||||
if add_dir_button_response.clicked() {
|
|
||||||
self.handle_directory_adding();
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
ui.allocate_ui(scroll_area_size, |ui| {
|
|
||||||
ScrollArea::vertical().id_salt(0).show(ui, |ui| {
|
|
||||||
for (index, dir) in self.directories.iter().enumerate() {
|
|
||||||
let dir_name = dir
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("Invalid Directory");
|
|
||||||
let dir_button = Button::new(dir_name).frame(false);
|
|
||||||
|
|
||||||
let dir_delete_button = Button::new(icons::ICON_DELETE).frame(false);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let dir_button_response = ui.add(dir_button);
|
|
||||||
if dir_button_response.clicked() {
|
|
||||||
self.current_directory = Some(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
let directory_delete_button_response = ui.add(dir_delete_button);
|
|
||||||
if directory_delete_button_response.clicked() {
|
|
||||||
self.directory_to_delete = Some(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_directory_adding(&mut self) {
|
|
||||||
if let Some(dirs) = FileDialog::pick_folders(Default::default()) {
|
|
||||||
let saved_dirs_path = dirs::config_dir().unwrap().join("pwsp").join("saved_dirs");
|
|
||||||
if let Ok(mut file) = fs::OpenOptions::new().append(true).open(saved_dirs_path) {
|
|
||||||
for path in dirs {
|
|
||||||
if !self.directories.contains(&path) {
|
|
||||||
self.directories.push(path.clone());
|
|
||||||
writeln!(file, "{}", path.display()).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_directory_deletion(&mut self) {
|
|
||||||
if let Some(index) = self.directory_to_delete {
|
|
||||||
if let Some(current_index) = self.current_directory {
|
|
||||||
if current_index > index {
|
|
||||||
self.current_directory = Some(current_index - 1);
|
|
||||||
} else if current_index == index {
|
|
||||||
self.current_directory = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.directories.remove(index);
|
|
||||||
self.directory_to_delete = None;
|
|
||||||
|
|
||||||
let saved_dirs_path = dirs::config_dir().unwrap().join("pwsp").join("saved_dirs");
|
|
||||||
let content = self
|
|
||||||
.directories
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.display().to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
fs::write(saved_dirs_path, content).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_music_list(&mut self, ui: &mut Ui, scroll_area_size: Vec2) {
|
|
||||||
if self.current_directory.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_path = self
|
|
||||||
.directories
|
|
||||||
.get(self.current_directory.unwrap())
|
|
||||||
.unwrap();
|
|
||||||
if !fs::exists(current_path).ok().unwrap_or(false) {
|
|
||||||
self.directory_to_delete = self.current_directory;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let files = fs::read_dir(current_path).ok().unwrap();
|
|
||||||
let mut music_files = Vec::new();
|
|
||||||
let music_extensions = ["mp3", "wav", "ogg", "flac", "mp4", "aac"].to_vec();
|
|
||||||
for file in files {
|
|
||||||
let path = file.unwrap().path();
|
|
||||||
|
|
||||||
if !path.is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(extension) = path.extension().and_then(|n| n.to_str()) {
|
|
||||||
if music_extensions.contains(&extension.to_lowercase().as_str()) {
|
|
||||||
if path
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string()
|
|
||||||
.to_lowercase()
|
|
||||||
.contains(self.search_query.as_str())
|
|
||||||
{
|
|
||||||
music_files.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
let search_entry = TextEdit::singleline(&mut self.search_query);
|
|
||||||
ui.add_sized([ui.available_width(), 20.0], search_entry);
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.allocate_ui(scroll_area_size, |ui| {
|
|
||||||
ScrollArea::vertical().id_salt(1).show(ui, |ui| {
|
|
||||||
for file in music_files.iter() {
|
|
||||||
let file_name = file.file_name().and_then(|n| n.to_str()).unwrap();
|
|
||||||
let file_button = Button::new(file_name).frame(false);
|
|
||||||
let file_button_response = ui.add(file_button);
|
|
||||||
if file_button_response.clicked() {
|
|
||||||
self.current_file = file.to_path_buf();
|
|
||||||
self.play_current_file();
|
|
||||||
}
|
|
||||||
ui.separator();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_mic_selection(&mut self, ui: &mut Ui) {
|
|
||||||
ComboBox::from_label("Choose MIC")
|
|
||||||
.selected_text(format!("{:?}", self.selected_input_device))
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
for device in self.available_input_devices.iter() {
|
|
||||||
ui.selectable_value(
|
|
||||||
&mut self.selected_input_device,
|
|
||||||
device.audio_device.name.clone(),
|
|
||||||
device.audio_device.nick.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play_current_file(&mut self) {
|
|
||||||
if self.current_file.to_str().unwrap().is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.current_file.exists() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.selected_input_device.is_empty() {
|
|
||||||
self.link_devices();
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = fs::File::open(self.current_file.display().to_string()).unwrap();
|
|
||||||
let source = Decoder::new(file).unwrap();
|
|
||||||
|
|
||||||
self.audio_sink.stop();
|
|
||||||
self.audio_sink.play();
|
|
||||||
self.audio_sink.append(source);
|
|
||||||
|
|
||||||
let metadata = MediaFileMetadata::new(&self.current_file.as_path()).unwrap();
|
|
||||||
self.max_player_position = metadata._duration.unwrap() as f32;
|
|
||||||
self.player_position = 0.0;
|
|
||||||
self.prev_player_position = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn link_devices(&self) {
|
|
||||||
let output_devices = pw::get_output_devices().unwrap();
|
|
||||||
let mut pwsp_output: Option<&pw::OutputDevice> = None;
|
|
||||||
|
|
||||||
for device in output_devices.iter() {
|
|
||||||
if device.audio_device.name == "alsa_playback.pwsp" {
|
|
||||||
pwsp_output = Some(device);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pwsp_output.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let input_devices = pw::get_input_devices().unwrap();
|
|
||||||
let mut mic: Option<&pw::InputDevice> = None;
|
|
||||||
|
|
||||||
for device in input_devices.iter() {
|
|
||||||
if device.audio_device.name == self.selected_input_device {
|
|
||||||
mic = Some(device);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mic.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pwsp_output.unwrap().unlink(mic.unwrap());
|
|
||||||
pwsp_output.unwrap().link(mic.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for App {
|
|
||||||
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
|
|
||||||
CentralPanel::default().show(ctx, |ui| self.upd(ui, ctx, frame));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), eframe::Error> {
|
fn main() -> Result<(), eframe::Error> {
|
||||||
let config_dir_path = dirs::config_dir().unwrap().join("pwsp");
|
let config_dir_path = dirs::config_dir().unwrap().join("pwsp");
|
||||||
@@ -491,7 +39,7 @@ fn main() -> Result<(), eframe::Error> {
|
|||||||
options,
|
options,
|
||||||
Box::new(|cc| {
|
Box::new(|cc| {
|
||||||
egui_material_icons::initialize(&cc.egui_ctx);
|
egui_material_icons::initialize(&cc.egui_ctx);
|
||||||
Ok(Box::new(App::new(cc)))
|
Ok(Box::new(app::App::new(cc)))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user