further implementations

This commit is contained in:
Gabor Körber 2024-06-07 17:26:13 +02:00
parent 61241ae56a
commit 25e7680cee
8 changed files with 4262 additions and 210 deletions

4213
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -22,3 +22,22 @@ serde = { version = "1.0.171", features = ["derive"] }
tokio = { version = "1.29.1", features = ["full"] } tokio = { version = "1.29.1", features = ["full"] }
xxhash-rust = { version = "0.8.6", features = ["xxh3", "const_xxh3"] } xxhash-rust = { version = "0.8.6", features = ["xxh3", "const_xxh3"] }
strinto = { path = "./strinto" } strinto = { path = "./strinto" }
eframe = "0.27.2"
tray-item = "0.10"
winit = "0.25"
once_cell = "1.19.0"
tray-icon = "0.12"
raw-window-handle = "0.6.2"
[build-dependencies]
embed-resource = "2.3"
[dependencies.windows]
version = "0.56.0"
features = [
"Data_Xml_Dom",
"Win32_Foundation",
"Win32_Security",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
]

View File

@ -2,7 +2,7 @@ use axum::{
routing::{self, get}, routing::{self, get},
Router, Router,
}; };
use eframe::{egui, epi}; use eframe::egui;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
net::SocketAddr, net::SocketAddr,
@ -56,7 +56,7 @@ async fn main() {
// Set up the system tray icon // Set up the system tray icon
let (tray_tx, tray_rx) = std::sync::mpsc::channel(); let (tray_tx, tray_rx) = std::sync::mpsc::channel();
let mut tray = TrayItem::new("App Name", "icon-red").unwrap(); let mut tray = TrayItem::new("App Name", tray_item::IconSource::Resource("icon-red")).unwrap();
tray.add_label("Server Control").unwrap(); tray.add_label("Server Control").unwrap();
tray.add_menu_item("Show/Hide", { tray.add_menu_item("Show/Hide", {
let tray_tx = tray_tx.clone(); let tray_tx = tray_tx.clone();
@ -179,7 +179,7 @@ fn run_ui(
ui_visible, ui_visible,
}; };
let native_options = eframe::NativeOptions::default(); let native_options = eframe::NativeOptions::default();
eframe::run_native(Box::new(app), native_options); eframe::run_native("Yo", native_options, Box::new(|_cc| Box::new(app)));
} }
struct NativeApp { struct NativeApp {
@ -189,8 +189,8 @@ struct NativeApp {
ui_visible: Arc<AtomicBool>, ui_visible: Arc<AtomicBool>,
} }
impl epi::App for NativeApp { impl eframe::App for NativeApp {
fn update(&mut self, ctx: &egui::CtxRef, frame: &eframe::epi::Frame) { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// Check visibility state and hide if needed // Check visibility state and hide if needed
if !self.ui_visible.load(Ordering::SeqCst) { if !self.ui_visible.load(Ordering::SeqCst) {
eprintln!("frame switch!"); eprintln!("frame switch!");
@ -231,10 +231,6 @@ impl epi::App for NativeApp {
}); });
} }
} }
fn name(&self) -> &str {
"NativeApp"
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -1,8 +1,6 @@
use axum::{ use axum::{routing::get, Router};
routing::{self, get},
Router,
};
use eframe::egui; use eframe::egui;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
net::SocketAddr, net::SocketAddr,
@ -11,9 +9,7 @@ use std::{
}; };
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tray_icon::{TrayIconBuilder, TrayIconEvent}; use tray_item::TrayItem;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_HIDE, SW_SHOWDEFAULT};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct AppState { struct AppState {
@ -23,7 +19,7 @@ struct AppState {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct ServerSettings { struct ServerSettings {
port: String, // Change to String for text editing port: String,
} }
async fn home() -> &'static str { async fn home() -> &'static str {
@ -64,10 +60,12 @@ async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>)
tokio::select! { tokio::select! {
_ = &mut server_handle => { _ = &mut server_handle => {
// Server has stopped // Server has stopped
eprintln!("Server stopped.");
break; break;
}, },
Some(message) = rx.recv() => { Some(message) = rx.recv() => {
match message { match message {
// This is the message type that actually restarts the server.
ServerMessage::UpdateSettings(new_settings) => { ServerMessage::UpdateSettings(new_settings) => {
println!("Received new settings: {:?}", new_settings); println!("Received new settings: {:?}", new_settings);
let mut settings_guard = app_state.settings.lock().unwrap(); let mut settings_guard = app_state.settings.lock().unwrap();
@ -81,11 +79,6 @@ async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>)
server_handle = start_server(&app_state); server_handle = start_server(&app_state);
println!("Server started."); println!("Server started.");
}, },
ServerMessage::Interact => {
// Example interaction: print current settings
let settings_guard = app_state.settings.lock().unwrap();
println!("Current settings: {:?}", *settings_guard);
},
ServerMessage::Shutdown => { ServerMessage::Shutdown => {
// Handle server shutdown // Handle server shutdown
println!("Shutting down server..."); println!("Shutting down server...");
@ -96,6 +89,11 @@ async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>)
break; break;
}, },
// Handle other message types here // Handle other message types here
ServerMessage::Interact => {
// Example interaction: print current settings
let settings_guard = app_state.settings.lock().unwrap();
println!("Current settings: {:?}", *settings_guard);
},
} }
} }
} }
@ -145,56 +143,79 @@ fn run_ui(
static VISIBLE: once_cell::sync::Lazy<Mutex<bool>> = static VISIBLE: once_cell::sync::Lazy<Mutex<bool>> =
once_cell::sync::Lazy::new(|| Mutex::new(true)); once_cell::sync::Lazy::new(|| Mutex::new(true));
let native_options = eframe::NativeOptions::default();
eframe::run_native( eframe::run_native(
"App", "App",
eframe::NativeOptions::default(), native_options,
Box::new(|cc| { Box::new(|cc| {
// Set up the tray icon event handler // Set up the tray icon event handler
let window_handle = cc let window_handle = cc.window_handle().unwrap();
if let raw_window_handle::RawWindowHandle(handle) = window_handle { let window_handle = window_handle.as_raw();
let context = cc.egui_ctx.clone(); let mut tray =
TrayItem::new("App Name", tray_item::IconSource::Resource("icon-red")).unwrap();
if let RawWindowHandle::Win32(handle) = window_handle {
// Windows Only.
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::{
ShowWindow,
SW_HIDE,
SW_RESTORE, // SW_SHOWDEFAULT, SW_SHOWNORMAL,
};
TrayIconEvent::set_event_handler(Some(move |event: TrayIconEvent| { tray.add_label("Server Control").unwrap();
if event.click_type != tray_icon::ClickType::Double {
return;
}
let mut visible = VISIBLE.lock().unwrap(); tray.add_menu_item("Show/Hide", {
let window_handle = HWND(handle.hwnd as isize); move || {
let mut visible_lock = VISIBLE.lock().unwrap();
let window_handle = HWND(handle.hwnd.into());
if *visible { if *visible_lock {
unsafe { unsafe {
ShowWindow(window_handle, SW_HIDE); _ = ShowWindow(window_handle, SW_HIDE);
}
*visible_lock = false;
} else {
unsafe {
_ = ShowWindow(window_handle, SW_RESTORE);
}
*visible_lock = true;
} }
*visible = false;
} else {
unsafe {
ShowWindow(window_handle, SW_SHOWDEFAULT);
}
*visible = true;
} }
})); })
.unwrap();
} else { } else {
panic!("Unsupported platform"); println!("Unsupported platform for tray icon.");
} }
Box::new(NativeApp { tray.add_menu_item("Quit", move || {
settings: settings.clone(), std::process::exit(0);
should_run: should_run.clone(),
tx: tx.clone(),
}) })
.unwrap();
let app = NativeApp {
settings,
should_run,
tx,
tray,
};
Box::new(app)
}), }),
); )
.expect("Error running UI.");
} }
struct NativeApp { struct NativeApp {
settings: Arc<Mutex<ServerSettings>>, settings: Arc<Mutex<ServerSettings>>,
#[allow(dead_code)]
should_run: Arc<Mutex<bool>>, should_run: Arc<Mutex<bool>>,
tx: mpsc::Sender<ServerMessage>, tx: mpsc::Sender<ServerMessage>,
#[allow(dead_code)]
tray: TrayItem,
} }
impl eframe::App for NativeApp { impl eframe::App for NativeApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
let mut settings = self.settings.lock().unwrap(); let mut settings = self.settings.lock().unwrap();
@ -228,6 +249,12 @@ impl eframe::App for NativeApp {
} }
}); });
} }
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
if self.tx.try_send(ServerMessage::Shutdown).is_err() {
eprintln!("Failed to send shutdown message");
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -1,3 +1,5 @@
pub mod handlers;
pub mod server;
pub mod soundclips; pub mod soundclips;
pub mod state; pub mod state;
pub mod vbplay; pub mod vbplay;

View File

@ -1,20 +1,27 @@
use axum::extract::State; use axum::extract::State;
use axum::{routing, Router}; use axum::{routing, routing::get, Router};
use axum_template::{engine::Engine, Key, RenderHtml}; use axum_template::{engine::Engine, Key, RenderHtml};
use minijinja::{path_loader, Environment}; use minijinja::{path_loader, Environment};
use state::{AppState, TemplateEngine}; use state::{AppState, TemplateEngine};
use std::net::SocketAddr;
use log::info; use log::info;
use serde::Serialize; use serde::{Deserialize, Serialize};
use std::env; use std::env;
use std::sync::{Arc, Mutex}; use std::{
net::SocketAddr,
sync::{Arc, Mutex},
thread,
};
use tokio::sync::mpsc;
use dotenvy::dotenv; use dotenvy::dotenv;
use vbplay::{example_handler, AudioThread, DeviceSelection, PlaybackAction, SoundDecoder}; use vbplay::{example_handler, AudioThread, DeviceSelection, PlaybackAction, SoundDecoder};
mod handlers;
mod server;
mod soundclips; mod soundclips;
mod state; mod state;
mod ui;
mod vbplay; mod vbplay;
#[tokio::main] #[tokio::main]
@ -36,78 +43,36 @@ async fn main() {
audio.on_playback(event_handler); audio.on_playback(event_handler);
let audio = Arc::new(Mutex::new(audio)); let audio = Arc::new(Mutex::new(audio));
let server_settings = Arc::new(Mutex::new(server::ServerSettings {
port: "3000".to_string(),
}));
let should_run = Arc::new(Mutex::new(true));
let app_state = AppState { let app_state = AppState {
engine: template_engine, engine: template_engine,
clips: soundclips::scan_directory_for_clips(&folder, &["mp3", "ogg", "wav", "flac"]) clips: soundclips::scan_directory_for_clips(&folder, &["mp3", "ogg", "wav", "flac"])
.expect("No Soundclips found."), .expect("No Soundclips found."),
player: audio, player: audio.clone(),
playback: None, playback: None,
settings: server_settings.clone(),
should_run: should_run.clone(),
}; };
// Set the address to run our application on let (ui_to_server_tx, ui_to_server_rx) = mpsc::channel(32);
let addr = SocketAddr::from(([0, 0, 0, 0], 3311));
// Build our application with a route // Run the server in a separate thread
let app = Router::new() let server_thread = thread::spawn(move || {
.route("/", routing::get(home)) let rt = tokio::runtime::Runtime::new().unwrap();
.route("/play/:hash", routing::get(play_handler)) rt.block_on(async {
.route("/stop", routing::get(stop_handler)) server::run_server(app_state, ui_to_server_rx).await;
.with_state(app_state); });
});
println!("listening on {}", addr); // Run the UI on the main thread
ui::run_ui(server_settings, should_run, ui_to_server_tx);
// Run it with hyper // Wait for the server thread to finish
axum::Server::bind(&addr) server_thread.join().unwrap();
.serve(app.into_make_service()) audio.lock().unwrap().exit();
.await eprintln!("Reached end of program.");
.unwrap();
}
#[derive(Serialize)]
struct TemplateContext {
clips: Vec<soundclips::SoundClip>,
}
async fn home(engine: TemplateEngine, state: State<AppState>) -> impl axum::response::IntoResponse {
let clips = state.0.clone().clips; // this is not ideal.
let context = TemplateContext { clips: clips };
RenderHtml(Key("soundboard.jinja".to_owned()), engine, context)
}
async fn play_handler(
axum::extract::Path(hash): axum::extract::Path<String>,
state: State<AppState>,
) -> String {
if let Some(clip) = state
.clips
.clone()
.into_iter()
.find(|s| hash == s.hash().to_string())
{
let player = state.player.clone();
let full_file_name = &clip.full_file_name();
info!("Playing {}", full_file_name);
player
.lock()
.unwrap()
.listener
.send(vbplay::Command::PlayWhilePressing(
full_file_name.into(),
"".to_owned(), // placeholder.
))
.unwrap();
}
"".to_owned()
}
async fn stop_handler(state: State<AppState>) -> String {
let player = state.player.clone();
player
.lock()
.unwrap()
.listener
.send(vbplay::Command::Stop)
.unwrap();
"".to_owned()
} }

View File

@ -1,5 +1,5 @@
use crate::soundclips::SoundClip;
use crate::vbplay::AudioHandler; use crate::vbplay::AudioHandler;
use crate::{server::ServerSettings, soundclips::SoundClip};
use axum::extract::FromRef; use axum::extract::FromRef;
use axum_template::engine::Engine; use axum_template::engine::Engine;
use minijinja::Environment; use minijinja::Environment;
@ -13,6 +13,8 @@ pub struct AppState {
pub clips: Vec<SoundClip>, pub clips: Vec<SoundClip>,
pub player: Arc<Mutex<AudioHandler>>, pub player: Arc<Mutex<AudioHandler>>,
pub playback: Option<Arc<Mutex<AudioHandler>>>, pub playback: Option<Arc<Mutex<AudioHandler>>>,
pub settings: Arc<Mutex<ServerSettings>>,
pub should_run: Arc<Mutex<bool>>,
} }
impl FromRef<AppState> for TemplateEngine { impl FromRef<AppState> for TemplateEngine {

View File

@ -22,6 +22,7 @@ pub enum SoundDecoder {
} }
pub enum Command { pub enum Command {
Exit,
Play(String), Play(String),
PlayWhilePressing(String, String), PlayWhilePressing(String, String),
Stop, Stop,
@ -117,6 +118,13 @@ impl AudioHandler {
pub fn on_playback(&mut self, event: Box<dyn PlaybackAction + Send>) { pub fn on_playback(&mut self, event: Box<dyn PlaybackAction + Send>) {
self.events.lock().unwrap().actions.push(event); self.events.lock().unwrap().actions.push(event);
} }
pub fn exit(&mut self) {
self.events.lock().unwrap().actions.clear();
self.listener
.send(Command::Exit)
.expect("Error sending Exit Command.");
}
} }
pub mod example_handler { pub mod example_handler {
@ -172,6 +180,10 @@ pub fn audio_thread(
for command in rx { for command in rx {
match command { match command {
Command::Exit => {
eprintln!("Exiting Soundloop.");
break;
}
Command::Play(file_name) => { Command::Play(file_name) => {
if let Ok(sink) = sink_mutex.lock() { if let Ok(sink) = sink_mutex.lock() {
play_file(&sink, file_name, &select_decoder); play_file(&sink, file_name, &select_decoder);