Compare commits

..

8 Commits

19 changed files with 2281 additions and 1460 deletions

2881
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,25 +14,25 @@ dotenvy = "0.15.7"
enigo = "0.2.1" enigo = "0.2.1"
log = "0.4.20" log = "0.4.20"
minijinja = { version = "2.0.1", features = ["loader"] } minijinja = { version = "2.0.1", features = ["loader"] }
minimp3 = "0.5.1"
mp3-duration = "0.1.10" mp3-duration = "0.1.10"
regex = "1.9.0" regex = "1.9.0"
rodio = "0.18.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" }
eframe = "0.27.2" eframe = "0.27.2"
tray-item = "0.10" tray-item = { version = "0.10" }
winit = "0.30"
once_cell = "1.19.0" once_cell = "1.19.0"
tray-icon = "0.14.3"
raw-window-handle = "0.6.2" raw-window-handle = "0.6.2"
image = "0.25.1"
[build-dependencies] [target.'cfg(target_os = "linux")'.dependencies]
embed-resource = "2.3" tray-item = { version = "0.10", features = ["ksni"] }
[dependencies.windows] [target.'cfg(windows)'.dependencies]
winit = "0.30"
[target.'cfg(windows)'.dependencies.windows]
version = "0.57.0" version = "0.57.0"
features = [ features = [
"Data_Xml_Dom", "Data_Xml_Dom",
@ -40,4 +40,19 @@ features = [
"Win32_Security", "Win32_Security",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
"Win32_System_LibraryLoader",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Controls",
"Win32_Graphics_Dwm",
"Win32_UI_Shell",
"Win32_System_Console", # attaching to console
] ]
#[target.'cfg(windows)'.dependencies.windows-sys]
#version = "0.52.0"
[target.'cfg(windows)'.build-dependencies]
embed-resource = "2.3"

View File

@ -1,5 +1,9 @@
#[cfg(windows)]
extern crate embed_resource; extern crate embed_resource;
fn main() { fn main() {
#[cfg(windows)]
{
embed_resource::compile("soundboard.rc", embed_resource::NONE); embed_resource::compile("soundboard.rc", embed_resource::NONE);
} }
}

BIN
icons/soundboard.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

View File

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

View File

@ -1,68 +0,0 @@
///
/// Trying to get around "".into() for String values.
/// Or "".to_owned().
/// Or String::from("").
/// Or "".to_string().
/// Choose your church.
///
/// This is as far as you will get declaratively.
#[macro_export]
macro_rules! strinto {
($struct:ident { $($field:ident : $value:expr),* $(,)? }) => {
$struct {
$(
$field: $crate::strinto!(@convert $value),
)*
}
};
(@convert $value:literal) => {
match () {
_ if stringify!($value).starts_with("\"") => {
$value.to_string()
},
_ => $value.into(), // <-- no getting rid of the into!
}
};
}
struct SomeStruct {
first_name: String,
//last_name: String, // NOPE because of @convert.
//age: usize, // reason of .into() in the first place.
}
pub fn main() {
let x = strinto!(SomeStruct {
first_name: "First",
//last_name: String::from("Last"), // NOPE 2.
//age: 1, // NOPE 1. But I went further.
});
}
// while this compiles for only &str "", it is also useless compared to into!
// the reason is, that you cannot type check in the declarative macros.
// while yes, you would conditionally run stringify!($value) in the match, but the match expansion
// will always lead you to type errors if you don't do an .into() in the other arm.
// also in the end it fails to compile for anything but all members of the struct being Strings.
// the last idea was going with a helper trait, but that will bleed into the runtime code, and yet again only work on pure String structs.
// I guess I have to embrace String::from(), .to_string(), .to_owned(), .into() madness, for something every human reader can deduce in a second,
// if you would just implicitly convert &str to String if literally written in the code.
// It is kinda amusing, that the other solution, to use new() kind of turns me off, because I cannot be explicit about the parameter name in the call.
// I would literally love the option to be more explicit in function param names.
// while builder patterns feel a bit bloated for static runtime options, i will probably look into automations for that.
// this is probably the only aesthetic decision in rust I probably will hate forever.
// It does not make sense, Strings as literals already are special in your code.
// Because numbers are as well, you dont have to write 123.into() either.
// I know I probably made some really harsh logical mistakes in my opinion here, and maybe it can be proven, that I am wrong, and I would love to hear that
// However it kind of feels like an excuse to not simplify assigning declaratively written &str to Strings in the code.
// And it makes sense to be explicit about creating a String Buffer sometimes, but it does not make sense mostly.
// Anyway, I will still try a procedural macro for this, just for fun.

251
src/bin/ui_icon.rs Normal file
View File

@ -0,0 +1,251 @@
use eframe::{egui, NativeOptions};
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr};
use windows::{
core::PCWSTR,
Win32::{
System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::{LoadImageW, IMAGE_ICON, LR_DEFAULTCOLOR},
},
};
// https://github.com/emilk/egui/issues/920
#[tokio::main]
async fn main() {
// Run the UI on the main thread
run_ui();
}
fn run_ui() {
let app = NativeApp {};
// Attempt to load the icon from resources
let icon_handle = match load_icon_from_resource("icon-soundboard") {
Ok(handle) => Some(handle as isize),
Err(e) => {
eprintln!("Failed to load icon: {}", e);
None
}
};
// load from icon resource, cross platform.
// let icon = load_icon_static();
// load from exe resource
let icon = from_windows::load_app_icon("icon-soundboard");
let native_options = NativeOptions {
viewport: egui::ViewportBuilder::default().with_icon(icon),
..Default::default()
};
eframe::run_native(
"Custom window frame", // unused title
native_options,
Box::new(|_cc| Box::new(app)),
)
.unwrap();
}
struct NativeApp {}
impl eframe::App for NativeApp {
fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Testing Software");
ui.horizontal(|ui| {
ui.label("Blah:");
});
if ui.button("Apply").clicked() {
eprintln!("Apply clicked");
}
});
}
}
fn to_wstring(str: &str) -> Vec<u16> {
OsStr::new(str)
.encode_wide()
.chain(Some(0).into_iter())
.collect::<Vec<_>>()
}
fn load_icon_from_resource(resource_name: &str) -> Result<isize, String> {
let icon = unsafe {
let hmodule = if let Ok(hmodule) = GetModuleHandleW(None) {
hmodule
} else {
return Err("Error getting windows module handle".to_owned());
};
let handle = if let Ok(handle) = LoadImageW(
hmodule,
PCWSTR(to_wstring(resource_name).as_ptr()),
IMAGE_ICON,
64,
64,
LR_DEFAULTCOLOR,
) {
handle
} else {
return Err("Error getting image handle".to_owned());
};
handle
};
Ok(icon.0)
}
fn load_icon(path: &str) -> egui::IconData {
let (icon_rgba, icon_width, icon_height) = {
let image = image::open(path)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
egui::IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
}
}
pub(crate) fn load_icon_static() -> egui::IconData {
let (icon_rgba, icon_width, icon_height) = {
let icon = include_bytes!("../../icons/soundboard.ico");
let image = image::load_from_memory(icon)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
egui::IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
}
}
mod from_windows {
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr};
use eframe::egui;
use windows::{
core::PCWSTR,
Win32::{
Graphics::Gdi::{
CreateCompatibleDC, DeleteDC, GetDIBits, GetObjectA, SelectObject, BITMAP,
BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS,
},
System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::{
GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR,
},
},
};
fn to_wstring(str: &str) -> Vec<u16> {
OsStr::new(str)
.encode_wide()
.chain(Some(0).into_iter())
.collect::<Vec<_>>()
}
// Grab the icon from the exe and hand it over to egui
pub fn load_app_icon(icon_name: &str) -> egui::IconData {
let (mut buffer, width, height) = unsafe {
let resource_name = to_wstring(icon_name);
let h_instance = GetModuleHandleW(None).unwrap();
let icon = LoadImageW(
h_instance,
PCWSTR(resource_name.as_ptr()),
IMAGE_ICON,
512,
512,
LR_DEFAULTCOLOR,
)
.unwrap();
let mut icon_info = ICONINFO::default();
GetIconInfo(HICON(icon.0), &mut icon_info as *mut _).expect("Failed to load icon info");
let mut bitmap = BITMAP::default();
GetObjectA(
icon_info.hbmColor,
std::mem::size_of::<BITMAP>() as i32,
Some(&mut bitmap as *mut _ as *mut _),
);
let width = bitmap.bmWidth;
let height = bitmap.bmHeight;
let b_size = (width * height * 4) as usize;
let mut buffer = Vec::<u8>::with_capacity(b_size);
let h_dc = CreateCompatibleDC(None);
let h_bitmap = SelectObject(h_dc, icon_info.hbmColor);
let mut bitmap_info = BITMAPINFO::default();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
bitmap_info.bmiHeader.biWidth = width;
bitmap_info.bmiHeader.biHeight = height;
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32;
bitmap_info.bmiHeader.biCompression = 0;
bitmap_info.bmiHeader.biSizeImage = 0;
let res = GetDIBits(
h_dc,
icon_info.hbmColor,
0,
height as u32,
Some(buffer.spare_capacity_mut().as_mut_ptr() as *mut _),
&mut bitmap_info as *mut _,
DIB_RGB_COLORS,
);
if res == 0 {
panic!("Failed to get RGB DI bits");
}
SelectObject(h_dc, h_bitmap);
let _ = DeleteDC(h_dc);
assert_eq!(
bitmap_info.bmiHeader.biSizeImage as usize, b_size,
"returned biSizeImage must equal to b_size"
);
// set the new size
buffer.set_len(bitmap_info.bmiHeader.biSizeImage as usize);
(buffer, width as u32, height as u32)
};
// RGBA -> BGRA
for pixel in buffer.as_mut_slice().chunks_mut(4) {
pixel.swap(0, 2);
}
// Flip the image vertically
let row_size = width as usize * 4; // number of pixels in each row
let row_count = buffer.len() as usize / row_size; // number of rows in the image
for row in 0..row_count / 2 {
// loop through half of the rows
let start = row * row_size; // index of the start of the current row
let end = (row_count - row - 1) * row_size; // index of the end of the current row
for i in 0..row_size {
buffer.swap(start + i, end + i);
}
}
egui::IconData {
rgba: buffer,
width,
height,
}
}
}

View File

@ -1,7 +1,4 @@
use axum::{ use axum::{routing::get, Router};
routing::{self, get},
Router,
};
use eframe::egui; use eframe::egui;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{

View File

@ -1,7 +1,4 @@
use axum::{ use axum::{routing::get, Router};
routing::{self, get},
Router,
};
use eframe::egui; use eframe::egui;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -56,7 +53,11 @@ async fn main() {
// Set up the system tray icon // Set up the system tray icon
let (tray_tx, tray_rx) = std::sync::mpsc::channel(); let (tray_tx, tray_rx) = std::sync::mpsc::channel();
let mut tray = TrayItem::new("App Name", tray_item::IconSource::Resource("icon-red")).unwrap(); let mut tray = TrayItem::new(
"App Name",
tray_item::IconSource::Resource("icon-soundboard"),
)
.unwrap();
tray.add_label("Server Control").unwrap(); tray.add_label("Server Control").unwrap();
tray.add_menu_item("Show/Hide", { tray.add_menu_item("Show/Hide", {
let tray_tx = tray_tx.clone(); let tray_tx = tray_tx.clone();

144
src/icon.rs Normal file
View File

@ -0,0 +1,144 @@
// https://github.com/emilk/egui/issues/920
#[cfg(not(windows))]
pub fn load_app_icon(_icon_name: &str) -> eframe::egui::IconData {
let (icon_rgba, icon_width, icon_height) = {
let icon = include_bytes!("../icons/soundboard.ico");
let image = image::load_from_memory(icon)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
eframe::egui::IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
}
}
#[cfg(windows)]
pub use from_windows::load_app_icon;
#[cfg(windows)]
mod from_windows {
use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
use eframe::egui;
use windows::{
core::PCWSTR,
Win32::{
Graphics::Gdi::{
CreateCompatibleDC, DeleteDC, GetDIBits, GetObjectA, SelectObject, BITMAP,
BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS,
},
System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::{
GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR,
},
},
};
fn to_wstring(str: &str) -> Vec<u16> {
OsStr::new(str)
.encode_wide()
.chain(Some(0).into_iter())
.collect::<Vec<_>>()
}
// Grab the icon from the exe and hand it over to egui
pub fn load_app_icon(icon_name: &str) -> egui::IconData {
let (mut buffer, width, height) = unsafe {
let resource_name = to_wstring(icon_name);
let h_instance = GetModuleHandleW(None).unwrap();
let icon = LoadImageW(
h_instance,
PCWSTR(resource_name.as_ptr()),
IMAGE_ICON,
512,
512,
LR_DEFAULTCOLOR,
)
.unwrap();
let mut icon_info = ICONINFO::default();
GetIconInfo(HICON(icon.0), &mut icon_info as *mut _).expect("Failed to load icon info");
let mut bitmap = BITMAP::default();
GetObjectA(
icon_info.hbmColor,
std::mem::size_of::<BITMAP>() as i32,
Some(&mut bitmap as *mut _ as *mut _),
);
let width = bitmap.bmWidth;
let height = bitmap.bmHeight;
let b_size = (width * height * 4) as usize;
let mut buffer = Vec::<u8>::with_capacity(b_size);
let h_dc = CreateCompatibleDC(None);
let h_bitmap = SelectObject(h_dc, icon_info.hbmColor);
let mut bitmap_info = BITMAPINFO::default();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
bitmap_info.bmiHeader.biWidth = width;
bitmap_info.bmiHeader.biHeight = height;
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32;
bitmap_info.bmiHeader.biCompression = 0;
bitmap_info.bmiHeader.biSizeImage = 0;
let res = GetDIBits(
h_dc,
icon_info.hbmColor,
0,
height as u32,
Some(buffer.spare_capacity_mut().as_mut_ptr() as *mut _),
&mut bitmap_info as *mut _,
DIB_RGB_COLORS,
);
if res == 0 {
panic!("Failed to get RGB DI bits");
}
SelectObject(h_dc, h_bitmap);
let _ = DeleteDC(h_dc);
assert_eq!(
bitmap_info.bmiHeader.biSizeImage as usize, b_size,
"returned biSizeImage must equal to b_size"
);
// set the new size
buffer.set_len(bitmap_info.bmiHeader.biSizeImage as usize);
(buffer, width as u32, height as u32)
};
// RGBA -> BGRA
for pixel in buffer.as_mut_slice().chunks_mut(4) {
pixel.swap(0, 2);
}
// Flip the image vertically
let row_size = width as usize * 4; // number of pixels in each row
let row_count = buffer.len() as usize / row_size; // number of rows in the image
for row in 0..row_count / 2 {
// loop through half of the rows
let start = row * row_size; // index of the start of the current row
let end = (row_count - row - 1) * row_size; // index of the end of the current row
for i in 0..row_size {
buffer.swap(start + i, end + i);
}
}
egui::IconData {
rgba: buffer,
width,
height,
}
}
}

View File

@ -1,5 +1,7 @@
pub mod handlers; pub mod handlers;
pub mod icon;
pub mod server; pub mod server;
pub mod soundclips; pub mod soundclips;
pub mod state; pub mod state;
pub mod ui;
pub mod vbplay; pub mod vbplay;

View File

@ -1,3 +1,5 @@
#![cfg_attr(not(test), windows_subsystem = "windows")] // not(debug_assertions) would hide the console only on release builds.
use axum_template::engine::Engine; use axum_template::engine::Engine;
use minijinja::{path_loader, Environment}; use minijinja::{path_loader, Environment};
use state::AppState; use state::AppState;
@ -10,6 +12,7 @@ use dotenvy::dotenv;
use vbplay::{example_handler, DeviceSelection, SoundDecoder}; use vbplay::{example_handler, DeviceSelection, SoundDecoder};
mod handlers; mod handlers;
mod icon;
mod server; mod server;
mod soundclips; mod soundclips;
mod state; mod state;
@ -18,6 +21,16 @@ mod vbplay;
fn main() { fn main() {
dotenv().ok(); dotenv().ok();
#[cfg(target_os = "windows")]
{
// if you run this from a windows console, we attach to the parent process.
use windows::Win32::System::Console::{AttachConsole, ATTACH_PARENT_PROCESS};
unsafe {
AttachConsole(ATTACH_PARENT_PROCESS).unwrap_or_default();
}
}
let rt = tokio::runtime::Runtime::new().unwrap(); 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");
@ -31,12 +44,13 @@ fn main() {
DeviceSelection::FindByPattern(device_pattern), DeviceSelection::FindByPattern(device_pattern),
SoundDecoder::Detect, SoundDecoder::Detect,
); );
let event_handler = Box::new(example_handler::PressMouseForward::new()); let event_handler = Box::new(example_handler::PressMouseForward::new());
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 { let server_settings = Arc::new(Mutex::new(server::ServerSettings {
port: "3000".to_string(), port: "3311".to_string(),
})); }));
let should_run = Arc::new(Mutex::new(true)); let should_run = Arc::new(Mutex::new(true));
@ -50,14 +64,20 @@ fn main() {
should_run: should_run.clone(), should_run: should_run.clone(),
}; };
let (ui_to_server_tx, ui_to_server_rx) = mpsc::channel(32); let (tx_ui_to_server, rx_ui_to_server) = mpsc::channel(32);
let (tx_server_to_ui, rx_server_to_ui) = mpsc::channel(32);
let server_handle = rt.spawn(async move { let server_handle = rt.spawn(async move {
server::run_server(app_state, ui_to_server_rx).await; server::run_server(app_state, rx_ui_to_server, tx_server_to_ui).await;
}); });
// Run the UI on the main thread // Run the UI on the main thread
ui::run_ui(server_settings, should_run, ui_to_server_tx); ui::run_ui(
server_settings,
should_run,
tx_ui_to_server,
rx_server_to_ui,
);
if let Err(e) = rt.block_on(server_handle) { if let Err(e) = rt.block_on(server_handle) {
eprintln!("Server task error: {:?}", e); eprintln!("Server task error: {:?}", e);
@ -65,11 +85,13 @@ fn main() {
println!("Server task completed successfully"); println!("Server task completed successfully");
} }
println!("Setting shutdown timeout...");
rt.shutdown_timeout(tokio::time::Duration::from_secs(5)); rt.shutdown_timeout(tokio::time::Duration::from_secs(5));
audio.lock().unwrap().exit(); audio.lock().unwrap().exit();
drop(audio); drop(audio);
eprintln!("Reached end of program."); eprintln!("Reached end of program.");
std::process::exit(0); //std::process::exit(0);
} }

View File

@ -1,5 +1,6 @@
use crate::handlers; use crate::handlers;
use crate::state::AppState; use crate::state::AppState;
use crate::ui::UIMessage;
use axum::{routing, Router}; use axum::{routing, Router};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::SocketAddr; use std::net::SocketAddr;
@ -17,7 +18,11 @@ pub enum ServerMessage {
Shutdown, Shutdown,
} }
pub async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessage>) { pub async fn run_server(
app_state: AppState,
mut rx: mpsc::Receiver<ServerMessage>,
tx: mpsc::Sender<UIMessage>,
) {
loop { loop {
let should_run = { let should_run = {
let guard = app_state.should_run.lock().unwrap(); let guard = app_state.should_run.lock().unwrap();
@ -90,5 +95,6 @@ pub async fn run_server(app_state: AppState, mut rx: mpsc::Receiver<ServerMessag
} }
} }
} }
tx.try_send(UIMessage::ServerHasQuit).unwrap_or_default();
eprintln!("Exiting run_server."); eprintln!("Exiting run_server.");
} }

112
src/ui.rs
View File

@ -1,67 +1,90 @@
use eframe::egui; use eframe::egui;
use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use std::sync::{Arc, Mutex}; use std::borrow::BorrowMut;
use std::sync::Mutex;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tray_item::TrayItem; use tray_item::TrayItem;
use crate::server::{ServerMessage, ServerSettings}; use crate::server::{ServerMessage, ServerSettings};
pub enum UIMessage {
ServerHasQuit,
}
pub fn run_ui( pub fn run_ui(
settings: Arc<Mutex<ServerSettings>>, settings: Arc<Mutex<ServerSettings>>,
should_run: Arc<Mutex<bool>>, should_run: Arc<Mutex<bool>>,
tx: mpsc::Sender<ServerMessage>, tx: mpsc::Sender<ServerMessage>,
rx: mpsc::Receiver<UIMessage>,
) { ) {
// Create an atomic bool to track window visibility // Initialize visibility as AtomicBool
static VISIBLE: once_cell::sync::Lazy<Mutex<bool>> = let visible = Arc::new(AtomicBool::new(true));
once_cell::sync::Lazy::new(|| Mutex::new(true));
let icon = crate::icon::load_app_icon("icon-soundboard");
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_icon(icon),
..Default::default()
};
let visible_for_tray = Arc::clone(&visible);
let native_options = eframe::NativeOptions::default();
eframe::run_native( eframe::run_native(
"App", "App",
native_options, native_options,
Box::new(|cc| { Box::new(|cc| {
// Set up the tray icon event handler
let window_handle = cc.window_handle().unwrap(); let window_handle = cc.window_handle().unwrap();
let window_handle = window_handle.as_raw(); let window_handle = window_handle.as_raw();
let mut tray = let mut tray = TrayItem::new(
TrayItem::new("Soundboard", tray_item::IconSource::Resource("icon-red")).unwrap(); "Soundboard",
tray_item::IconSource::Resource("icon-soundboard"),
)
.unwrap();
if let RawWindowHandle::Win32(handle) = window_handle { if let RawWindowHandle::Win32(handle) = window_handle {
// Windows Only. #[cfg(windows)]
{
use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::{ use windows::Win32::UI::WindowsAndMessaging::{
ShowWindow, ShowWindow, SW_HIDE, SW_RESTORE,
SW_HIDE,
SW_RESTORE, // SW_SHOWDEFAULT, SW_SHOWNORMAL,
}; };
tray.add_label("Server Control").unwrap(); 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()); let window_handle = HWND(handle.hwnd.into());
tray.add_menu_item("Show/Hide", {
//let visible_for_tray = Arc::clone(&visible_for_tray);
move || {
let is_visible = visible_for_tray.load(Ordering::SeqCst);
if *visible_lock { if is_visible {
unsafe { unsafe {
_ = ShowWindow(window_handle, SW_HIDE); _ = ShowWindow(window_handle, SW_HIDE);
} }
*visible_lock = false; visible_for_tray.store(false, Ordering::SeqCst);
} else { } else {
unsafe { unsafe {
_ = ShowWindow(window_handle, SW_RESTORE); _ = ShowWindow(window_handle, SW_RESTORE);
} }
*visible_lock = true; visible_for_tray.store(true, Ordering::SeqCst);
} }
} }
}) })
.unwrap(); .unwrap();
}
} else { } else {
println!("Unsupported platform for tray icon."); println!("Unsupported platform for tray icon.");
} }
let my_tx = tx.clone(); let my_tx = tx.clone();
tray.add_menu_item("Quit", move || { tray.add_menu_item("Quit", move || {
my_tx.try_send(ServerMessage::Shutdown).unwrap(); my_tx.try_send(ServerMessage::Shutdown).unwrap_or_else(|_| {
eprintln!("Failed to send Shutdown message");
});
//std::process::exit(0); //std::process::exit(0);
}) })
.unwrap(); .unwrap();
@ -70,6 +93,7 @@ pub fn run_ui(
settings, settings,
should_run, should_run,
tx, tx,
rx,
tray, tray,
}; };
@ -84,17 +108,67 @@ struct NativeApp {
#[allow(dead_code)] #[allow(dead_code)]
should_run: Arc<Mutex<bool>>, should_run: Arc<Mutex<bool>>,
tx: mpsc::Sender<ServerMessage>, tx: mpsc::Sender<ServerMessage>,
rx: mpsc::Receiver<UIMessage>,
#[allow(dead_code)] #[allow(dead_code)]
tray: TrayItem, tray: TrayItem,
} }
impl eframe::App for NativeApp { impl eframe::App for NativeApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if let Ok(message) = self.rx.try_recv() {
match message {
UIMessage::ServerHasQuit => {
eprintln!("Server has quit!!!?");
}
_ => {}
}
}
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
egui::TopBottomPanel::top("top_panel")
.resizable(true)
.min_height(32.0)
.show_inside(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.vertical_centered(|ui| {
ui.heading("Soundboard");
});
});
});
egui::SidePanel::left("left_panel")
.resizable(true)
.default_width(180.0)
.width_range(80.0..=220.0)
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.vertical(|ui| {
ui.heading("Menu");
if ui.button("Status").clicked() {}
if ui.button("Devices").clicked() {}
if ui.button("Pages").clicked() {}
if ui.button("Server Settings").clicked() {}
if ui.button("Sound Settings").clicked() {}
});
});
egui::ScrollArea::vertical().show(ui, |ui| {});
});
let mut settings = self.settings.lock().unwrap(); let mut settings = self.settings.lock().unwrap();
ui.heading("Server Settings"); ui.heading("Server Settings");
egui::Frame {
inner_margin: 12.0.into(),
outer_margin: 24.0.into(),
rounding: 14.0.into(),
shadow: egui::epaint::Shadow::NONE,
fill: egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128),
stroke: egui::Stroke::new(1.0, egui::Color32::GRAY),
}
.show(ui, |ui| {
ui.label(egui::RichText::new("3311").color(egui::Color32::WHITE));
});
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Port:"); ui.label("Port:");
if ui.text_edit_singleline(&mut settings.port).changed() {} if ui.text_edit_singleline(&mut settings.port).changed() {}

View File

@ -1,7 +1,6 @@
use cpal::traits::{DeviceTrait, HostTrait}; use cpal::traits::{DeviceTrait, HostTrait};
use cpal::{Device, Host}; use cpal::{Device, Host};
use log::{debug, error, info}; use log::{debug, error, info};
use minimp3::{Decoder, Error};
use regex::Regex; use regex::Regex;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
@ -17,7 +16,6 @@ use std::thread;
#[derive(Clone)] #[derive(Clone)]
pub enum SoundDecoder { pub enum SoundDecoder {
Detect, Detect,
Mp3Mini,
Rodio, Rodio,
} }
@ -200,7 +198,7 @@ pub fn audio_thread(
error!("Mutex Lock Failure while trying to play sound.") error!("Mutex Lock Failure while trying to play sound.")
} }
} }
Command::PlayWhilePressing(file_name, press_button) => { Command::PlayWhilePressing(file_name, _press_button) => {
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);
} else { } else {
@ -239,45 +237,17 @@ pub fn audio_thread(
tx tx
} }
fn detect_decoder(file_name: &str, sound_decoder: &SoundDecoder) -> SoundDecoder { fn detect_decoder(_file_name: &str, sound_decoder: &SoundDecoder) -> SoundDecoder {
// TODO: File detection via ending or whitelisting? // TODO: File detection via ending or whitelisting?
// This function MUST NOT return SoundDecoder::Detect // This function MUST NOT return SoundDecoder::Detect
match sound_decoder { match sound_decoder {
SoundDecoder::Detect => SoundDecoder::Mp3Mini, SoundDecoder::Detect => SoundDecoder::Rodio, // Mp3Mini seems bugged.
other => other.clone(), other => other.clone(),
} }
} }
fn play_file(sink: &rodio::Sink, file_name: String, sound_decoder: &SoundDecoder) { fn play_file(sink: &rodio::Sink, file_name: String, sound_decoder: &SoundDecoder) {
match detect_decoder(&file_name, sound_decoder) { match detect_decoder(&file_name, sound_decoder) {
SoundDecoder::Mp3Mini => {
// MP3 Mini provides low latency playback
let mut sound_file = File::open(file_name).unwrap();
let mut file_data = Vec::new();
sound_file.read_to_end(&mut file_data).unwrap();
// Iterate over the MP3 frames and play them
let mut decoder = Decoder::new(Cursor::new(file_data));
loop {
match decoder.next_frame() {
Ok(frame) => {
let source = rodio::buffer::SamplesBuffer::new(
2,
frame.sample_rate.try_into().unwrap(),
&*frame.data,
);
sink.append(source);
}
Err(Error::Eof) => {
debug!("EOF");
break;
}
Err(e) => {
error!("{:?}", e);
break;
}
}
}
}
SoundDecoder::Rodio => { SoundDecoder::Rodio => {
// Rodio currently supports Ogg, Mp3, WAV and Flac // Rodio currently supports Ogg, Mp3, WAV and Flac
let file = File::open(&file_name).unwrap(); let file = File::open(&file_name).unwrap();

1
strinto/.gitignore vendored
View File

@ -1 +0,0 @@
/target

47
strinto/Cargo.lock generated
View File

@ -1,47 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strinto"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

View File

@ -1,14 +0,0 @@
[package]
name = "strinto"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
[dev-dependencies]
proc-macro2 = "1.0"

View File

@ -1,62 +0,0 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Expr, ExprStruct};
/// # literal strings in a struct insantiation are converted .into().
///
/// ```rust
/// use strinto::strinto;
/// #[derive(Debug)]
/// struct TestStruct {
/// name: String,
/// title: String,
/// description: String,
/// age: usize,
/// }
///
/// let descr = "description";
///
/// let output = strinto!(TestStruct {
/// name: "John", // Literal string.
/// title: String::from("Wicked"),
/// description: descr.to_string(),
/// age: 30,
/// });
///
/// let output_string = format!("{:?}", output);
/// assert_eq!(
/// output_string,
/// "TestStruct { name: \"John\", title: \"Wicked\", description: \"description\", age: 30 }"
/// );
/// ```
#[proc_macro]
pub fn strinto(input: TokenStream) -> TokenStream {
let expr_struct = parse_macro_input!(input as ExprStruct);
// Extract struct name and fields
let struct_name = &expr_struct.path;
let fields = expr_struct.fields.iter().map(|field| {
let field_name = field.member.clone();
let field_value = &field.expr;
// Determine if the field value is a string literal and transform it
if let Expr::Lit(expr_lit) = field_value {
if let syn::Lit::Str(_) = expr_lit.lit {
quote! { #field_name: #field_value.into() }
} else {
quote! { #field_name: #field_value }
}
} else {
quote! { #field_name: #field_value }
}
});
let expanded = quote! {
#struct_name {
#(#fields,)*
}
};
expanded.into()
}