Merge pull request 'UI for Server base code' (#1) from server-ui into main
Reviewed-on: #1
This commit is contained in:
commit
307daaea7b
3957
Cargo.lock
generated
3957
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
29
Cargo.toml
@ -7,18 +7,37 @@ default-run = "soundboard"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.6.18"
|
axum = "0.7.5"
|
||||||
axum-template = { version = "0.19.0", features = ["minijinja"] }
|
axum-template = { version = "2.3.0", features = ["minijinja"] }
|
||||||
cpal = "0.15.2"
|
cpal = "0.15.2"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
enigo = "0.1.3"
|
enigo = "0.2.1"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
minijinja = { version = "1.0.3", features = ["loader"] }
|
minijinja = { version = "2.0.1", features = ["loader"] }
|
||||||
minimp3 = "0.5.1"
|
minimp3 = "0.5.1"
|
||||||
mp3-duration = "0.1.10"
|
mp3-duration = "0.1.10"
|
||||||
regex = "1.9.0"
|
regex = "1.9.0"
|
||||||
rodio = "0.17.1"
|
rodio = "0.18.1"
|
||||||
serde = { version = "1.0.171", features = ["derive"] }
|
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.30"
|
||||||
|
once_cell = "1.19.0"
|
||||||
|
tray-icon = "0.14.3"
|
||||||
|
raw-window-handle = "0.6.2"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
embed-resource = "2.3"
|
||||||
|
|
||||||
|
[dependencies.windows]
|
||||||
|
version = "0.57.0"
|
||||||
|
features = [
|
||||||
|
"Data_Xml_Dom",
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
]
|
||||||
|
5
build.rs
Normal file
5
build.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
extern crate embed_resource;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
embed_resource::compile("soundboard.rc", embed_resource::NONE);
|
||||||
|
}
|
1
soundboard.rc
Normal file
1
soundboard.rc
Normal file
@ -0,0 +1 @@
|
|||||||
|
icon-red ICON "icons/icon-red.ico"
|
111
src/bin/ui_test.rs
Normal file
111
src/bin/ui_test.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
use axum::{
|
||||||
|
routing::{self, get},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use eframe::egui;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
use tokio::{net::TcpListener, sync::mpsc};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct AppState {
|
||||||
|
settings: Arc<Mutex<ServerSettings>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ServerSettings>) {
|
||||||
|
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 listener = TcpListener::bind(&addr).await.unwrap();
|
||||||
|
let server = axum::serve(listener, app.into_make_service());
|
||||||
|
|
||||||
|
server.await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ui(settings: Arc<Mutex<ServerSettings>>, tx: mpsc::Sender<ServerSettings>) {
|
||||||
|
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<Mutex<ServerSettings>>,
|
||||||
|
tx: mpsc::Sender<ServerSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
182
src/bin/ui_test_safe_restart.rs
Normal file
182
src/bin/ui_test_safe_restart.rs
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
use axum::{
|
||||||
|
routing::{self, get},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use eframe::egui;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tokio::{net::TcpListener, sync::mpsc};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct AppState {
|
||||||
|
settings: Arc<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ServerMessage>) {
|
||||||
|
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 listener = TcpListener::bind(&addr).await.unwrap();
|
||||||
|
let server = axum::serve(listener, app.into_make_service());
|
||||||
|
|
||||||
|
server.await.unwrap();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn monitor_shutdown(should_run: Arc<Mutex<bool>>) {
|
||||||
|
while *should_run.lock().unwrap() {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ui(
|
||||||
|
settings: Arc<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
tx: mpsc::Sender<ServerMessage>,
|
||||||
|
) {
|
||||||
|
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<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
tx: mpsc::Sender<ServerMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
237
src/bin/ui_tray_icon.rs
Normal file
237
src/bin/ui_tray_icon.rs
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
use axum::{
|
||||||
|
routing::{self, get},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use eframe::egui;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tokio::{net::TcpListener, sync::mpsc};
|
||||||
|
use tray_item::TrayItem;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct AppState {
|
||||||
|
settings: Arc<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atomic bool to track UI visibility
|
||||||
|
let ui_visible = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
|
// Set up the system tray icon
|
||||||
|
let (tray_tx, tray_rx) = std::sync::mpsc::channel();
|
||||||
|
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();
|
||||||
|
move || {
|
||||||
|
tray_tx.send(()).unwrap();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
tray.add_menu_item("Quit", move || {
|
||||||
|
std::process::exit(0);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Thread to handle tray icon interactions
|
||||||
|
let ui_visible_clone = ui_visible.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
for _ in tray_rx {
|
||||||
|
let visible = ui_visible_clone.load(Ordering::SeqCst);
|
||||||
|
println!("visup");
|
||||||
|
ui_visible_clone.store(!visible, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the UI on the main thread
|
||||||
|
run_ui(settings, should_run, tx, ui_visible);
|
||||||
|
|
||||||
|
// Wait for the server thread to finish
|
||||||
|
server_thread.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>) {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("/", get(home))
|
||||||
|
.with_state(server_app_state.clone());
|
||||||
|
let listener = TcpListener::bind(&addr).await.unwrap();
|
||||||
|
let server = axum::serve(listener, app.into_make_service());
|
||||||
|
|
||||||
|
server.await.unwrap();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn monitor_shutdown(should_run: Arc<Mutex<bool>>) {
|
||||||
|
while *should_run.lock().unwrap() {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ui(
|
||||||
|
settings: Arc<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
tx: mpsc::Sender<ServerMessage>,
|
||||||
|
ui_visible: Arc<AtomicBool>,
|
||||||
|
) {
|
||||||
|
let app = NativeApp {
|
||||||
|
settings,
|
||||||
|
should_run,
|
||||||
|
tx,
|
||||||
|
ui_visible,
|
||||||
|
};
|
||||||
|
let native_options = eframe::NativeOptions::default();
|
||||||
|
eframe::run_native("Yo", native_options, Box::new(|_cc| Box::new(app)));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NativeApp {
|
||||||
|
settings: Arc<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
tx: mpsc::Sender<ServerMessage>,
|
||||||
|
ui_visible: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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!");
|
||||||
|
// Do not display the central panel if the UI should be hidden
|
||||||
|
//self.ui_visible.store(true, Ordering::SeqCst); // Reset visibility state for next show
|
||||||
|
} else {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum ServerMessage {
|
||||||
|
UpdateSettings(ServerSettings),
|
||||||
|
Interact,
|
||||||
|
Shutdown,
|
||||||
|
// Add other message types here
|
||||||
|
}
|
261
src/bin/ui_tray_icon_windows.rs
Normal file
261
src/bin/ui_tray_icon_windows.rs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
use axum::{routing::get, Router};
|
||||||
|
use eframe::egui;
|
||||||
|
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tokio::{net::TcpListener, sync::mpsc};
|
||||||
|
use tray_item::TrayItem;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct AppState {
|
||||||
|
settings: Arc<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct ServerSettings {
|
||||||
|
port: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ServerMessage>) {
|
||||||
|
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("/", get(home))
|
||||||
|
.with_state(server_app_state.clone());
|
||||||
|
let listener = TcpListener::bind(&addr).await.unwrap();
|
||||||
|
let server = axum::serve(listener, app.into_make_service());
|
||||||
|
|
||||||
|
server.await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn monitor_shutdown(should_run: Arc<Mutex<bool>>) {
|
||||||
|
while *should_run.lock().unwrap() {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ui(
|
||||||
|
settings: Arc<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
tx: mpsc::Sender<ServerMessage>,
|
||||||
|
) {
|
||||||
|
// Create an atomic bool to track window visibility
|
||||||
|
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",
|
||||||
|
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<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) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum ServerMessage {
|
||||||
|
UpdateSettings(ServerSettings),
|
||||||
|
Interact,
|
||||||
|
Shutdown,
|
||||||
|
// Add other message types here
|
||||||
|
}
|
62
src/handlers.rs
Normal file
62
src/handlers.rs
Normal file
@ -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<soundclips::SoundClip>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop_handler(state: State<AppState>) -> String {
|
||||||
|
let player = state.player.clone();
|
||||||
|
player
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.listener
|
||||||
|
.send(vbplay::Command::Stop)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
"".to_owned()
|
||||||
|
}
|
@ -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;
|
||||||
|
104
src/main.rs
104
src/main.rs
@ -1,25 +1,24 @@
|
|||||||
use axum::extract::State;
|
use axum_template::engine::Engine;
|
||||||
use axum::{routing, Router};
|
|
||||||
use axum_template::{engine::Engine, Key, RenderHtml};
|
|
||||||
use minijinja::{path_loader, Environment};
|
use minijinja::{path_loader, Environment};
|
||||||
use state::{AppState, TemplateEngine};
|
use state::AppState;
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
use log::info;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use vbplay::{example_handler, AudioThread, DeviceSelection, PlaybackAction, SoundDecoder};
|
use vbplay::{example_handler, DeviceSelection, SoundDecoder};
|
||||||
|
|
||||||
|
mod handlers;
|
||||||
|
mod server;
|
||||||
mod soundclips;
|
mod soundclips;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod ui;
|
||||||
mod vbplay;
|
mod vbplay;
|
||||||
|
|
||||||
#[tokio::main]
|
fn main() {
|
||||||
async fn main() {
|
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|
||||||
let device_pattern = env::var("DEVICE_PATTERN").expect("Need a device pattern");
|
let device_pattern = env::var("DEVICE_PATTERN").expect("Need a device pattern");
|
||||||
let folder = env::var("DIRECTORY").expect("Need a directory in env file.");
|
let folder = env::var("DIRECTORY").expect("Need a directory in env file.");
|
||||||
@ -36,78 +35,41 @@ 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
|
let server_handle = rt.spawn(async move {
|
||||||
let app = Router::new()
|
server::run_server(app_state, ui_to_server_rx).await;
|
||||||
.route("/", routing::get(home))
|
});
|
||||||
.route("/play/:hash", routing::get(play_handler))
|
|
||||||
.route("/stop", routing::get(stop_handler))
|
|
||||||
.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
|
if let Err(e) = rt.block_on(server_handle) {
|
||||||
axum::Server::bind(&addr)
|
eprintln!("Server task error: {:?}", e);
|
||||||
.serve(app.into_make_service())
|
} else {
|
||||||
.await
|
println!("Server task completed successfully");
|
||||||
.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 {
|
rt.shutdown_timeout(tokio::time::Duration::from_secs(5));
|
||||||
let player = state.player.clone();
|
|
||||||
player
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.listener
|
|
||||||
.send(vbplay::Command::Stop)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
"".to_owned()
|
audio.lock().unwrap().exit();
|
||||||
|
drop(audio);
|
||||||
|
|
||||||
|
eprintln!("Reached end of program.");
|
||||||
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
94
src/server.rs
Normal file
94
src/server.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use crate::handlers;
|
||||||
|
use crate::state::AppState;
|
||||||
|
use axum::{routing, Router};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use tokio::{net::TcpListener, sync::mpsc};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServerSettings {
|
||||||
|
pub port: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ServerMessage {
|
||||||
|
UpdateSettings(ServerSettings),
|
||||||
|
Interact,
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>) {
|
||||||
|
loop {
|
||||||
|
let should_run = {
|
||||||
|
let guard = app_state.should_run.lock().unwrap();
|
||||||
|
*guard
|
||||||
|
};
|
||||||
|
|
||||||
|
if !should_run {
|
||||||
|
eprintln!("Should run is false.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let server_handle = 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 listener = TcpListener::bind(&addr).await.unwrap();
|
||||||
|
axum::serve(listener, app.into_make_service()).await
|
||||||
|
});
|
||||||
|
let server_abort_handle = server_handle.abort_handle();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = server_handle => {
|
||||||
|
// Server has stopped
|
||||||
|
eprintln!("Server stopped.");
|
||||||
|
let mut should_run_guard = app_state.should_run.lock().unwrap();
|
||||||
|
*should_run_guard = false;
|
||||||
|
drop(should_run_guard);
|
||||||
|
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;
|
||||||
|
drop(settings_guard); // Ensure the lock is released before starting the new server
|
||||||
|
// Restart the server with new settings
|
||||||
|
println!("Aborting current server...");
|
||||||
|
server_abort_handle.abort();
|
||||||
|
},
|
||||||
|
ServerMessage::Shutdown => {
|
||||||
|
// Handle server shutdown
|
||||||
|
println!("Shutting down server...");
|
||||||
|
let mut should_run_guard = app_state.should_run.lock().unwrap();
|
||||||
|
*should_run_guard = false;
|
||||||
|
drop(should_run_guard);
|
||||||
|
server_abort_handle.abort();
|
||||||
|
println!("Server shutdown.");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
ServerMessage::Interact => {
|
||||||
|
// Example interaction: print current settings
|
||||||
|
let settings_guard = app_state.settings.lock().unwrap();
|
||||||
|
println!("Current settings: {:?}", *settings_guard);
|
||||||
|
server_abort_handle.abort();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!("Exiting run_server.");
|
||||||
|
}
|
@ -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 {
|
||||||
|
133
src/ui.rs
Normal file
133
src/ui.rs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
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<Mutex<ServerSettings>>,
|
||||||
|
should_run: Arc<Mutex<bool>>,
|
||||||
|
tx: mpsc::Sender<ServerMessage>,
|
||||||
|
) {
|
||||||
|
// Create an atomic bool to track window visibility
|
||||||
|
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",
|
||||||
|
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("Soundboard", 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let my_tx = tx.clone();
|
||||||
|
tray.add_menu_item("Quit", move || {
|
||||||
|
my_tx.try_send(ServerMessage::Shutdown).unwrap();
|
||||||
|
//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) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(settings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
@ -128,18 +136,23 @@ pub mod example_handler {
|
|||||||
|
|
||||||
impl PressMouseForward {
|
impl PressMouseForward {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let enigo = Enigo::new();
|
let enigo_settings = Settings::default();
|
||||||
|
let enigo = Enigo::new(&enigo_settings).unwrap();
|
||||||
PressMouseForward { enigo: enigo }
|
PressMouseForward { enigo: enigo }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::PlaybackAction for PressMouseForward {
|
impl super::PlaybackAction for PressMouseForward {
|
||||||
fn before_playback(&mut self) {
|
fn before_playback(&mut self) {
|
||||||
self.enigo.mouse_down(MouseButton::Forward);
|
self.enigo
|
||||||
|
.button(Button::Forward, Direction::Press)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn after_playback(&mut self) {
|
fn after_playback(&mut self) {
|
||||||
self.enigo.mouse_up(MouseButton::Forward);
|
self.enigo
|
||||||
|
.button(Button::Forward, Direction::Release)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,6 +185,14 @@ pub fn audio_thread(
|
|||||||
|
|
||||||
for command in rx {
|
for command in rx {
|
||||||
match command {
|
match command {
|
||||||
|
Command::Exit => {
|
||||||
|
eprintln!("Exiting Soundloop.");
|
||||||
|
if let Ok(sink) = sink_mutex.lock() {
|
||||||
|
sink.stop();
|
||||||
|
}
|
||||||
|
drop(stream_handle);
|
||||||
|
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);
|
||||||
@ -212,6 +233,7 @@ pub fn audio_thread(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
eprintln!("Exiting audio_thread");
|
||||||
});
|
});
|
||||||
|
|
||||||
tx
|
tx
|
||||||
|
Loading…
Reference in New Issue
Block a user