UI for Server base code #1

Merged
g4borg merged 6 commits from server-ui into main 2024-06-08 00:22:53 +02:00
7 changed files with 612 additions and 0 deletions
Showing only changes of commit 0abcf8351a - Show all commits

5
build.rs Normal file
View File

@ -0,0 +1,5 @@
extern crate embed_resource;
fn main() {
embed_resource::compile("soundboard.rc", embed_resource::NONE);
}

1
soundboard.rc Normal file
View File

@ -0,0 +1 @@
icon-red ICON "icons/icon-red.ico"

116
src/bin/ui_test.rs Normal file
View File

@ -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<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 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<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");
}
}
});
}
}

View File

@ -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<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 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<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
}

62
src/handlers.rs Normal file
View 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()
}

111
src/server.rs Normal file
View File

@ -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<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("/", 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<Mutex<bool>>) {
eprintln!("Monitoring shutdown...");
while *should_run.lock().unwrap() {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
eprintln!("Shutdown completed.");
}

130
src/ui.rs Normal file
View File

@ -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<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");
}
}
}