UI for Server base code #1
4213
Cargo.lock
generated
4213
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
@ -22,3 +22,22 @@ serde = { version = "1.0.171", features = ["derive"] }
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
xxhash-rust = { version = "0.8.6", features = ["xxh3", "const_xxh3"] }
|
||||
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",
|
||||
]
|
||||
|
@ -2,7 +2,7 @@ use axum::{
|
||||
routing::{self, get},
|
||||
Router,
|
||||
};
|
||||
use eframe::{egui, epi};
|
||||
use eframe::egui;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
@ -56,7 +56,7 @@ async fn main() {
|
||||
|
||||
// Set up the system tray icon
|
||||
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_menu_item("Show/Hide", {
|
||||
let tray_tx = tray_tx.clone();
|
||||
@ -179,7 +179,7 @@ fn run_ui(
|
||||
ui_visible,
|
||||
};
|
||||
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 {
|
||||
@ -189,8 +189,8 @@ struct NativeApp {
|
||||
ui_visible: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl epi::App for NativeApp {
|
||||
fn update(&mut self, ctx: &egui::CtxRef, frame: &eframe::epi::Frame) {
|
||||
impl eframe::App for NativeApp {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
// Check visibility state and hide if needed
|
||||
if !self.ui_visible.load(Ordering::SeqCst) {
|
||||
eprintln!("frame switch!");
|
||||
@ -231,10 +231,6 @@ impl epi::App for NativeApp {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"NativeApp"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -1,8 +1,6 @@
|
||||
use axum::{
|
||||
routing::{self, get},
|
||||
Router,
|
||||
};
|
||||
use axum::{routing::get, Router};
|
||||
use eframe::egui;
|
||||
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
@ -11,9 +9,7 @@ use std::{
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tray_icon::{TrayIconBuilder, TrayIconEvent};
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_HIDE, SW_SHOWDEFAULT};
|
||||
use tray_item::TrayItem;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AppState {
|
||||
@ -23,7 +19,7 @@ struct AppState {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ServerSettings {
|
||||
port: String, // Change to String for text editing
|
||||
port: String,
|
||||
}
|
||||
|
||||
async fn home() -> &'static str {
|
||||
@ -64,10 +60,12 @@ async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>)
|
||||
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();
|
||||
@ -81,11 +79,6 @@ async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>)
|
||||
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);
|
||||
},
|
||||
ServerMessage::Shutdown => {
|
||||
// Handle server shutdown
|
||||
println!("Shutting down server...");
|
||||
@ -96,6 +89,11 @@ async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>)
|
||||
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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,56 +143,79 @@ fn run_ui(
|
||||
static VISIBLE: once_cell::sync::Lazy<Mutex<bool>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(true));
|
||||
|
||||
let native_options = eframe::NativeOptions::default();
|
||||
eframe::run_native(
|
||||
"App",
|
||||
eframe::NativeOptions::default(),
|
||||
native_options,
|
||||
Box::new(|cc| {
|
||||
// Set up the tray icon event handler
|
||||
let window_handle = cc
|
||||
if let raw_window_handle::RawWindowHandle(handle) = window_handle {
|
||||
let context = cc.egui_ctx.clone();
|
||||
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,
|
||||
};
|
||||
|
||||
TrayIconEvent::set_event_handler(Some(move |event: TrayIconEvent| {
|
||||
if event.click_type != tray_icon::ClickType::Double {
|
||||
return;
|
||||
}
|
||||
tray.add_label("Server Control").unwrap();
|
||||
|
||||
let mut visible = VISIBLE.lock().unwrap();
|
||||
let window_handle = HWND(handle.hwnd as isize);
|
||||
tray.add_menu_item("Show/Hide", {
|
||||
move || {
|
||||
let mut visible_lock = VISIBLE.lock().unwrap();
|
||||
let window_handle = HWND(handle.hwnd.into());
|
||||
|
||||
if *visible {
|
||||
if *visible_lock {
|
||||
unsafe {
|
||||
ShowWindow(window_handle, SW_HIDE);
|
||||
_ = ShowWindow(window_handle, SW_HIDE);
|
||||
}
|
||||
*visible = false;
|
||||
*visible_lock = false;
|
||||
} else {
|
||||
unsafe {
|
||||
ShowWindow(window_handle, SW_SHOWDEFAULT);
|
||||
_ = ShowWindow(window_handle, SW_RESTORE);
|
||||
}
|
||||
*visible = true;
|
||||
*visible_lock = true;
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
panic!("Unsupported platform");
|
||||
}
|
||||
|
||||
Box::new(NativeApp {
|
||||
settings: settings.clone(),
|
||||
should_run: should_run.clone(),
|
||||
tx: tx.clone(),
|
||||
})
|
||||
.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<Mutex<ServerSettings>>,
|
||||
#[allow(dead_code)]
|
||||
should_run: Arc<Mutex<bool>>,
|
||||
tx: mpsc::Sender<ServerMessage>,
|
||||
#[allow(dead_code)]
|
||||
tray: TrayItem,
|
||||
}
|
||||
|
||||
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| {
|
||||
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)]
|
||||
|
@ -1,3 +1,5 @@
|
||||
pub mod handlers;
|
||||
pub mod server;
|
||||
pub mod soundclips;
|
||||
pub mod state;
|
||||
pub mod vbplay;
|
||||
|
101
src/main.rs
101
src/main.rs
@ -1,20 +1,27 @@
|
||||
use axum::extract::State;
|
||||
use axum::{routing, Router};
|
||||
use axum::{routing, routing::get, Router};
|
||||
use axum_template::{engine::Engine, Key, RenderHtml};
|
||||
use minijinja::{path_loader, Environment};
|
||||
use state::{AppState, TemplateEngine};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use log::info;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use dotenvy::dotenv;
|
||||
use vbplay::{example_handler, AudioThread, DeviceSelection, PlaybackAction, SoundDecoder};
|
||||
|
||||
mod handlers;
|
||||
mod server;
|
||||
mod soundclips;
|
||||
mod state;
|
||||
mod ui;
|
||||
mod vbplay;
|
||||
|
||||
#[tokio::main]
|
||||
@ -36,78 +43,36 @@ async fn main() {
|
||||
audio.on_playback(event_handler);
|
||||
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 {
|
||||
engine: template_engine,
|
||||
clips: soundclips::scan_directory_for_clips(&folder, &["mp3", "ogg", "wav", "flac"])
|
||||
.expect("No Soundclips found."),
|
||||
player: audio,
|
||||
player: audio.clone(),
|
||||
playback: None,
|
||||
settings: server_settings.clone(),
|
||||
should_run: should_run.clone(),
|
||||
};
|
||||
|
||||
// Set the address to run our application on
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 3311));
|
||||
let (ui_to_server_tx, ui_to_server_rx) = mpsc::channel(32);
|
||||
|
||||
// Build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/", routing::get(home))
|
||||
.route("/play/:hash", routing::get(play_handler))
|
||||
.route("/stop", routing::get(stop_handler))
|
||||
.with_state(app_state);
|
||||
// Run the server in a separate thread
|
||||
let server_thread = thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
server::run_server(app_state, ui_to_server_rx).await;
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.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()
|
||||
// Wait for the server thread to finish
|
||||
server_thread.join().unwrap();
|
||||
audio.lock().unwrap().exit();
|
||||
eprintln!("Reached end of program.");
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::soundclips::SoundClip;
|
||||
use crate::vbplay::AudioHandler;
|
||||
use crate::{server::ServerSettings, soundclips::SoundClip};
|
||||
use axum::extract::FromRef;
|
||||
use axum_template::engine::Engine;
|
||||
use minijinja::Environment;
|
||||
@ -13,6 +13,8 @@ pub struct AppState {
|
||||
pub clips: Vec<SoundClip>,
|
||||
pub player: 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 {
|
||||
|
@ -22,6 +22,7 @@ pub enum SoundDecoder {
|
||||
}
|
||||
|
||||
pub enum Command {
|
||||
Exit,
|
||||
Play(String),
|
||||
PlayWhilePressing(String, String),
|
||||
Stop,
|
||||
@ -117,6 +118,13 @@ impl AudioHandler {
|
||||
pub fn on_playback(&mut self, event: Box<dyn PlaybackAction + Send>) {
|
||||
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 {
|
||||
@ -172,6 +180,10 @@ pub fn audio_thread(
|
||||
|
||||
for command in rx {
|
||||
match command {
|
||||
Command::Exit => {
|
||||
eprintln!("Exiting Soundloop.");
|
||||
break;
|
||||
}
|
||||
Command::Play(file_name) => {
|
||||
if let Ok(sink) = sink_mutex.lock() {
|
||||
play_file(&sink, file_name, &select_decoder);
|
||||
|
Loading…
Reference in New Issue
Block a user