diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b7ea7d5 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +extern crate embed_resource; + +fn main() { + embed_resource::compile("soundboard.rc", embed_resource::NONE); +} diff --git a/soundboard.rc b/soundboard.rc new file mode 100644 index 0000000..5c701b4 --- /dev/null +++ b/soundboard.rc @@ -0,0 +1 @@ +icon-red ICON "icons/icon-red.ico" \ No newline at end of file diff --git a/src/bin/ui_test.rs b/src/bin/ui_test.rs new file mode 100644 index 0000000..dafb1e7 --- /dev/null +++ b/src/bin/ui_test.rs @@ -0,0 +1,116 @@ +use axum::{ + routing::{self, get}, + Router, +}; +use eframe::egui; +use serde::{Deserialize, Serialize}; +use std::{ + net::SocketAddr, + sync::{Arc, Mutex}, + thread, +}; +use tokio::sync::mpsc; + +#[derive(Debug, Clone)] +struct AppState { + settings: Arc>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ServerSettings { + port: String, // Change to String for text editing +} + +async fn home() -> &'static str { + "Hello, World!" +} + +#[tokio::main] +async fn main() { + let (tx, rx) = mpsc::channel(32); + let settings = Arc::new(Mutex::new(ServerSettings { + port: "3311".to_string(), + })); + let app_state = AppState { + settings: settings.clone(), + }; + + // Run the server in a separate thread + let server_thread = thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + run_server(app_state, rx).await; + }); + }); + + // Run the UI on the main thread + run_ui(settings, tx); + + // Wait for the server thread to finish + server_thread.join().unwrap(); +} + +async fn run_server(app_state: AppState, mut rx: mpsc::Receiver) { + loop { + let addr = { + let settings = app_state.settings.lock().unwrap(); + let port: u16 = settings.port.parse().unwrap_or(3311); + SocketAddr::from(([0, 0, 0, 0], port)) + }; + + let app = Router::new() + .route("/", get(home)) + .with_state(app_state.clone()); + + println!("listening on {}", addr); + + let server = axum::Server::bind(&addr).serve(app.into_make_service()); + + tokio::select! { + _ = server => {}, + Some(new_settings) = rx.recv() => { + println!("Received new settings: {:?}", new_settings); + // Here you can handle updates to server settings as needed + } + } + } +} + +fn run_ui(settings: Arc>, tx: mpsc::Sender) { + let app = NativeApp { settings, tx }; + let native_options = eframe::NativeOptions::default(); + eframe::run_native( + "Custom window frame", // unused title + native_options, + Box::new(|_cc| Box::new(app)), + ) + .unwrap(); +} + +struct NativeApp { + settings: Arc>, + tx: mpsc::Sender, +} + +impl eframe::App for NativeApp { + fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + let mut settings = self.settings.lock().unwrap(); + + ui.heading("Server Settings"); + + ui.horizontal(|ui| { + ui.label("Port:"); + if ui.text_edit_singleline(&mut settings.port).changed() { + eprintln!("Editing!"); + } + }); + + if ui.button("Apply").clicked() { + if self.tx.try_send(settings.clone()).is_err() { + eprintln!("Failed to send settings update"); + } + } + }); + } +} diff --git a/src/bin/ui_test_safe_restart.rs b/src/bin/ui_test_safe_restart.rs new file mode 100644 index 0000000..61021f8 --- /dev/null +++ b/src/bin/ui_test_safe_restart.rs @@ -0,0 +1,187 @@ +use axum::{ + routing::{self, get}, + Router, +}; +use eframe::egui; +use serde::{Deserialize, Serialize}; +use std::{ + net::SocketAddr, + sync::{Arc, Mutex}, + thread, +}; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; + +#[derive(Debug, Clone)] +struct AppState { + settings: Arc>, + should_run: Arc>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ServerSettings { + port: String, // Change to String for text editing +} + +async fn home() -> &'static str { + "Hello, World!" +} + +#[tokio::main] +async fn main() { + let (tx, rx) = mpsc::channel(32); + let settings = Arc::new(Mutex::new(ServerSettings { + port: "3311".to_string(), + })); + let should_run = Arc::new(Mutex::new(true)); + let app_state = AppState { + settings: settings.clone(), + should_run: should_run.clone(), + }; + + // Run the server in a separate thread + let server_thread = thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + run_server(app_state, rx).await; + }); + }); + + // Run the UI on the main thread + run_ui(settings, should_run, tx); + + // Wait for the server thread to finish + server_thread.join().unwrap(); +} + +async fn run_server(app_state: AppState, mut rx: mpsc::Receiver) { + let mut server_handle = start_server(&app_state); + + loop { + tokio::select! { + _ = &mut server_handle => { + // Server has stopped + break; + }, + Some(message) = rx.recv() => { + match message { + ServerMessage::UpdateSettings(new_settings) => { + println!("Received new settings: {:?}", new_settings); + let mut settings_guard = app_state.settings.lock().unwrap(); + *settings_guard = new_settings; + + // Restart the server with new settings + println!("Aborting current server..."); + server_handle.abort(); // Cancel the previous server task + println!("Starting new server..."); + drop(settings_guard); // Ensure the lock is released before starting the new server + server_handle = start_server(&app_state); + println!("Server started."); + }, + ServerMessage::Interact => { + // Example interaction: print current settings + let settings_guard = app_state.settings.lock().unwrap(); + println!("Current settings: {:?}", *settings_guard); + }, + // Handle other message types here + } + } + } + } +} + +fn start_server(app_state: &AppState) -> JoinHandle<()> { + let addr = { + let settings = app_state.settings.lock().unwrap(); + let port: u16 = settings.port.parse().unwrap_or(3311); + SocketAddr::from(([0, 0, 0, 0], port)) + }; + + let server_app_state = app_state.clone(); + + println!("listening on {}", addr); + + tokio::spawn(async move { + let app = Router::new() + .route("/", get(home)) + .with_state(server_app_state.clone()); + let server = axum::Server::bind(&addr).serve(app.into_make_service()); + + // Run the server and monitor the should_run flag + tokio::select! { + _ = server => {}, + _ = monitor_shutdown(server_app_state.should_run.clone()) => { + println!("Server shutdown signal received"); + } + } + }) +} + +async fn monitor_shutdown(should_run: Arc>) { + while *should_run.lock().unwrap() { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } +} + +fn run_ui( + settings: Arc>, + should_run: Arc>, + tx: mpsc::Sender, +) { + let app = NativeApp { + settings, + should_run, + tx, + }; + let native_options = eframe::NativeOptions::default(); + eframe::run_native( + "Test Safe Restart", + native_options, + Box::new(|_cc| Box::new(app)), + ) + .expect("Eframe Error"); +} + +struct NativeApp { + settings: Arc>, + should_run: Arc>, + tx: mpsc::Sender, +} + +impl eframe::App for NativeApp { + fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + let mut settings = self.settings.lock().unwrap(); + + ui.heading("Server Settings"); + + ui.horizontal(|ui| { + ui.label("Port:"); + if ui.text_edit_singleline(&mut settings.port).changed() {} + }); + + if ui.button("Apply").clicked() { + if self + .tx + .try_send(ServerMessage::UpdateSettings(settings.clone())) + .is_err() + { + eprintln!("Failed to send settings update"); + } + } + + if ui.button("Interact").clicked() { + if self.tx.try_send(ServerMessage::Interact).is_err() { + eprintln!("Failed to send interaction message"); + } + } + }); + } +} + +#[derive(Debug, Clone)] +enum ServerMessage { + UpdateSettings(ServerSettings), + Interact, + // Add other message types here +} diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..07f3bb6 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,62 @@ +use crate::{ + soundclips, + state::{AppState, TemplateEngine}, + vbplay, +}; +use axum::extract::State; +use axum_template::{Key, RenderHtml}; + +use log::info; +use serde::Serialize; + +#[derive(Serialize)] +pub struct TemplateContext { + clips: Vec, +} + +pub async fn home( + engine: TemplateEngine, + state: State, +) -> 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) +} + +pub async fn play_handler( + axum::extract::Path(hash): axum::extract::Path, + state: State, +) -> 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() +} + +pub async fn stop_handler(state: State) -> String { + let player = state.player.clone(); + player + .lock() + .unwrap() + .listener + .send(vbplay::Command::Stop) + .unwrap(); + + "".to_owned() +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..fa4cc25 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,111 @@ +use crate::handlers; +use crate::state::AppState; +use axum::{routing, Router}; +use serde::{Deserialize, Serialize}; +use std::{ + net::SocketAddr, + sync::{Arc, Mutex}, +}; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerSettings { + pub port: String, +} + +#[derive(Debug, Clone)] +pub enum ServerMessage { + UpdateSettings(ServerSettings), + Interact, + Shutdown, + // Add other message types here +} + +pub async fn run_server(app_state: AppState, mut rx: mpsc::Receiver) { + let mut server_handle = start_server(&app_state); + + loop { + tokio::select! { + _ = &mut server_handle => { + // Server has stopped + eprintln!("Server stopped."); + break; + }, + Some(message) = rx.recv() => { + match message { + // This is the message type that actually restarts the server. + ServerMessage::UpdateSettings(new_settings) => { + println!("Received new settings: {:?}", new_settings); + let mut settings_guard = app_state.settings.lock().unwrap(); + *settings_guard = new_settings; + + // Restart the server with new settings + println!("Aborting current server..."); + server_handle.abort(); // Cancel the previous server task + println!("Starting new server..."); + drop(settings_guard); // Ensure the lock is released before starting the new server + server_handle = start_server(&app_state); + println!("Server started."); + }, + ServerMessage::Shutdown => { + // Handle server shutdown + println!("Shutting down server..."); + let mut should_run_guard = app_state.should_run.lock().unwrap(); + *should_run_guard = false; + server_handle.abort(); + println!("Server shutdown."); + break; + }, + // 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); + }, + } + } + } + } +} + +fn start_server(app_state: &AppState) -> JoinHandle<()> { + // Compute the address before entering the async block + let addr = { + let settings = app_state.settings.lock().unwrap(); + let port: u16 = settings.port.parse().unwrap_or(3311); + SocketAddr::from(([0, 0, 0, 0], port)) + }; + + let server_app_state = app_state.clone(); + + println!("listening on {}", addr); + + tokio::spawn(async move { + let app = Router::new() + .route("/", routing::get(handlers::home)) + .route("/play/:hash", routing::get(handlers::play_handler)) + .route("/stop", routing::get(handlers::stop_handler)) + .with_state(server_app_state.clone()); + + let server = axum::Server::bind(&addr).serve(app.into_make_service()); + + // Run the server and monitor the should_run flag + tokio::select! { + _ = server => { + eprintln!("Server..."); + }, + _ = monitor_shutdown(server_app_state.should_run.clone()) => { + println!("Server shutdown signal received"); + } + } + }) +} + +async fn monitor_shutdown(should_run: Arc>) { + eprintln!("Monitoring shutdown..."); + while *should_run.lock().unwrap() { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + eprintln!("Shutdown completed."); +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..ffda055 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,130 @@ +use eframe::egui; +use raw_window_handle::{HasWindowHandle, RawWindowHandle}; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; +use tray_item::TrayItem; + +use crate::server::{ServerMessage, ServerSettings}; + +pub fn run_ui( + settings: Arc>, + should_run: Arc>, + tx: mpsc::Sender, +) { + // Create an atomic bool to track window visibility + static VISIBLE: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| Mutex::new(true)); + + let native_options = eframe::NativeOptions::default(); + eframe::run_native( + "App", + native_options, + Box::new(|cc| { + // Set up the tray icon event handler + let window_handle = cc.window_handle().unwrap(); + let window_handle = window_handle.as_raw(); + 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, + }; + + tray.add_label("Server Control").unwrap(); + + tray.add_menu_item("Show/Hide", { + move || { + let mut visible_lock = VISIBLE.lock().unwrap(); + let window_handle = HWND(handle.hwnd.into()); + + if *visible_lock { + unsafe { + _ = ShowWindow(window_handle, SW_HIDE); + } + *visible_lock = false; + } else { + unsafe { + _ = ShowWindow(window_handle, SW_RESTORE); + } + *visible_lock = true; + } + } + }) + .unwrap(); + } else { + println!("Unsupported platform for tray icon."); + } + + tray.add_menu_item("Quit", move || { + std::process::exit(0); + }) + .unwrap(); + + let app = NativeApp { + settings, + should_run, + tx, + tray, + }; + + Box::new(app) + }), + ) + .expect("Error running UI."); +} + +struct NativeApp { + settings: Arc>, + #[allow(dead_code)] + should_run: Arc>, + tx: mpsc::Sender, + #[allow(dead_code)] + tray: TrayItem, +} + +impl eframe::App for NativeApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + let mut settings = self.settings.lock().unwrap(); + + ui.heading("Server Settings"); + + ui.horizontal(|ui| { + ui.label("Port:"); + if ui.text_edit_singleline(&mut settings.port).changed() {} + }); + + if ui.button("Apply").clicked() { + if self + .tx + .try_send(ServerMessage::UpdateSettings(settings.clone())) + .is_err() + { + eprintln!("Failed to send settings update"); + } + } + + if ui.button("Interact").clicked() { + if self.tx.try_send(ServerMessage::Interact).is_err() { + eprintln!("Failed to send interaction message"); + } + } + + if ui.button("Shutdown Server").clicked() { + if self.tx.try_send(ServerMessage::Shutdown).is_err() { + eprintln!("Failed to send shutdown message"); + } + } + }); + } + + 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"); + } + } +}