Compare commits
No commits in common. "main" and "server-ui" have entirely different histories.
2873
Cargo.lock
generated
2873
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@ -14,25 +14,25 @@ dotenvy = "0.15.7"
|
||||
enigo = "0.2.1"
|
||||
log = "0.4.20"
|
||||
minijinja = { version = "2.0.1", features = ["loader"] }
|
||||
minimp3 = "0.5.1"
|
||||
mp3-duration = "0.1.10"
|
||||
regex = "1.9.0"
|
||||
rodio = "0.18.1"
|
||||
serde = { version = "1.0.171", features = ["derive"] }
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
xxhash-rust = { version = "0.8.6", features = ["xxh3", "const_xxh3"] }
|
||||
strinto = { path = "./strinto" }
|
||||
eframe = "0.27.2"
|
||||
tray-item = { version = "0.10" }
|
||||
once_cell = "1.19.0"
|
||||
raw-window-handle = "0.6.2"
|
||||
image = "0.25.1"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tray-item = { version = "0.10", features = ["ksni"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tray-item = "0.10"
|
||||
winit = "0.30"
|
||||
once_cell = "1.19.0"
|
||||
tray-icon = "0.14.3"
|
||||
raw-window-handle = "0.6.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows]
|
||||
[build-dependencies]
|
||||
embed-resource = "2.3"
|
||||
|
||||
[dependencies.windows]
|
||||
version = "0.57.0"
|
||||
features = [
|
||||
"Data_Xml_Dom",
|
||||
@ -40,19 +40,4 @@ features = [
|
||||
"Win32_Security",
|
||||
"Win32_System_Threading",
|
||||
"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"
|
||||
|
6
build.rs
6
build.rs
@ -1,9 +1,5 @@
|
||||
#[cfg(windows)]
|
||||
extern crate embed_resource;
|
||||
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
embed_resource::compile("soundboard.rc", embed_resource::NONE);
|
||||
}
|
||||
embed_resource::compile("soundboard.rc", embed_resource::NONE);
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 211 KiB |
@ -1 +1 @@
|
||||
icon-soundboard ICON "icons/soundboard.ico"
|
||||
icon-red ICON "icons/icon-red.ico"
|
68
src/bin/strinto_declarative.rs
Normal file
68
src/bin/strinto_declarative.rs
Normal file
@ -0,0 +1,68 @@
|
||||
///
|
||||
/// 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.
|
@ -1,251 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
use axum::{routing::get, Router};
|
||||
use axum::{
|
||||
routing::{self, get},
|
||||
Router,
|
||||
};
|
||||
use eframe::egui;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
|
@ -1,4 +1,7 @@
|
||||
use axum::{routing::get, Router};
|
||||
use axum::{
|
||||
routing::{self, get},
|
||||
Router,
|
||||
};
|
||||
use eframe::egui;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@ -53,11 +56,7 @@ async fn main() {
|
||||
|
||||
// Set up the system tray icon
|
||||
let (tray_tx, tray_rx) = std::sync::mpsc::channel();
|
||||
let mut tray = TrayItem::new(
|
||||
"App Name",
|
||||
tray_item::IconSource::Resource("icon-soundboard"),
|
||||
)
|
||||
.unwrap();
|
||||
let mut tray = TrayItem::new("App Name", tray_item::IconSource::Resource("icon-red")).unwrap();
|
||||
tray.add_label("Server Control").unwrap();
|
||||
tray.add_menu_item("Show/Hide", {
|
||||
let tray_tx = tray_tx.clone();
|
||||
|
144
src/icon.rs
144
src/icon.rs
@ -1,144 +0,0 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
pub mod handlers;
|
||||
pub mod icon;
|
||||
pub mod server;
|
||||
pub mod soundclips;
|
||||
pub mod state;
|
||||
pub mod ui;
|
||||
pub mod vbplay;
|
||||
|
32
src/main.rs
32
src/main.rs
@ -1,5 +1,3 @@
|
||||
#![cfg_attr(not(test), windows_subsystem = "windows")] // not(debug_assertions) would hide the console only on release builds.
|
||||
|
||||
use axum_template::engine::Engine;
|
||||
use minijinja::{path_loader, Environment};
|
||||
use state::AppState;
|
||||
@ -12,7 +10,6 @@ use dotenvy::dotenv;
|
||||
use vbplay::{example_handler, DeviceSelection, SoundDecoder};
|
||||
|
||||
mod handlers;
|
||||
mod icon;
|
||||
mod server;
|
||||
mod soundclips;
|
||||
mod state;
|
||||
@ -21,16 +18,6 @@ mod vbplay;
|
||||
|
||||
fn main() {
|
||||
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 device_pattern = env::var("DEVICE_PATTERN").expect("Need a device pattern");
|
||||
@ -44,13 +31,12 @@ fn main() {
|
||||
DeviceSelection::FindByPattern(device_pattern),
|
||||
SoundDecoder::Detect,
|
||||
);
|
||||
|
||||
let event_handler = Box::new(example_handler::PressMouseForward::new());
|
||||
audio.on_playback(event_handler);
|
||||
let audio = Arc::new(Mutex::new(audio));
|
||||
|
||||
let server_settings = Arc::new(Mutex::new(server::ServerSettings {
|
||||
port: "3311".to_string(),
|
||||
port: "3000".to_string(),
|
||||
}));
|
||||
let should_run = Arc::new(Mutex::new(true));
|
||||
|
||||
@ -64,20 +50,14 @@ fn main() {
|
||||
should_run: should_run.clone(),
|
||||
};
|
||||
|
||||
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 (ui_to_server_tx, ui_to_server_rx) = mpsc::channel(32);
|
||||
|
||||
let server_handle = rt.spawn(async move {
|
||||
server::run_server(app_state, rx_ui_to_server, tx_server_to_ui).await;
|
||||
server::run_server(app_state, ui_to_server_rx).await;
|
||||
});
|
||||
|
||||
// Run the UI on the main thread
|
||||
ui::run_ui(
|
||||
server_settings,
|
||||
should_run,
|
||||
tx_ui_to_server,
|
||||
rx_server_to_ui,
|
||||
);
|
||||
ui::run_ui(server_settings, should_run, ui_to_server_tx);
|
||||
|
||||
if let Err(e) = rt.block_on(server_handle) {
|
||||
eprintln!("Server task error: {:?}", e);
|
||||
@ -85,13 +65,11 @@ fn main() {
|
||||
println!("Server task completed successfully");
|
||||
}
|
||||
|
||||
println!("Setting shutdown timeout...");
|
||||
|
||||
rt.shutdown_timeout(tokio::time::Duration::from_secs(5));
|
||||
|
||||
audio.lock().unwrap().exit();
|
||||
drop(audio);
|
||||
|
||||
eprintln!("Reached end of program.");
|
||||
//std::process::exit(0);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::handlers;
|
||||
use crate::state::AppState;
|
||||
use crate::ui::UIMessage;
|
||||
use axum::{routing, Router};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
@ -18,11 +17,7 @@ pub enum ServerMessage {
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub async fn run_server(
|
||||
app_state: AppState,
|
||||
mut rx: mpsc::Receiver<ServerMessage>,
|
||||
tx: mpsc::Sender<UIMessage>,
|
||||
) {
|
||||
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();
|
||||
@ -95,6 +90,5 @@ pub async fn run_server(
|
||||
}
|
||||
}
|
||||
}
|
||||
tx.try_send(UIMessage::ServerHasQuit).unwrap_or_default();
|
||||
eprintln!("Exiting run_server.");
|
||||
}
|
||||
|
140
src/ui.rs
140
src/ui.rs
@ -1,90 +1,67 @@
|
||||
use eframe::egui;
|
||||
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use std::borrow::BorrowMut;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::mpsc;
|
||||
use tray_item::TrayItem;
|
||||
|
||||
use crate::server::{ServerMessage, ServerSettings};
|
||||
|
||||
pub enum UIMessage {
|
||||
ServerHasQuit,
|
||||
}
|
||||
|
||||
pub fn run_ui(
|
||||
settings: Arc<Mutex<ServerSettings>>,
|
||||
should_run: Arc<Mutex<bool>>,
|
||||
tx: mpsc::Sender<ServerMessage>,
|
||||
rx: mpsc::Receiver<UIMessage>,
|
||||
) {
|
||||
// Initialize visibility as AtomicBool
|
||||
let visible = Arc::new(AtomicBool::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);
|
||||
// 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-soundboard"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut tray =
|
||||
TrayItem::new("Soundboard", tray_item::IconSource::Resource("icon-red")).unwrap();
|
||||
if let RawWindowHandle::Win32(handle) = window_handle {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
ShowWindow, SW_HIDE, SW_RESTORE,
|
||||
};
|
||||
// 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_label("Server Control").unwrap();
|
||||
|
||||
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);
|
||||
tray.add_menu_item("Show/Hide", {
|
||||
move || {
|
||||
let mut visible_lock = VISIBLE.lock().unwrap();
|
||||
let window_handle = HWND(handle.hwnd.into());
|
||||
|
||||
if is_visible {
|
||||
unsafe {
|
||||
_ = ShowWindow(window_handle, SW_HIDE);
|
||||
}
|
||||
visible_for_tray.store(false, Ordering::SeqCst);
|
||||
} else {
|
||||
unsafe {
|
||||
_ = ShowWindow(window_handle, SW_RESTORE);
|
||||
}
|
||||
visible_for_tray.store(true, Ordering::SeqCst);
|
||||
if *visible_lock {
|
||||
unsafe {
|
||||
_ = ShowWindow(window_handle, SW_HIDE);
|
||||
}
|
||||
*visible_lock = false;
|
||||
} else {
|
||||
unsafe {
|
||||
_ = ShowWindow(window_handle, SW_RESTORE);
|
||||
}
|
||||
*visible_lock = true;
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.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_or_else(|_| {
|
||||
eprintln!("Failed to send Shutdown message");
|
||||
});
|
||||
my_tx.try_send(ServerMessage::Shutdown).unwrap();
|
||||
//std::process::exit(0);
|
||||
})
|
||||
.unwrap();
|
||||
@ -93,7 +70,6 @@ pub fn run_ui(
|
||||
settings,
|
||||
should_run,
|
||||
tx,
|
||||
rx,
|
||||
tray,
|
||||
};
|
||||
|
||||
@ -108,67 +84,17 @@ struct NativeApp {
|
||||
#[allow(dead_code)]
|
||||
should_run: Arc<Mutex<bool>>,
|
||||
tx: mpsc::Sender<ServerMessage>,
|
||||
rx: mpsc::Receiver<UIMessage>,
|
||||
#[allow(dead_code)]
|
||||
tray: TrayItem,
|
||||
}
|
||||
|
||||
impl eframe::App for NativeApp {
|
||||
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::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();
|
||||
|
||||
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.label("Port:");
|
||||
if ui.text_edit_singleline(&mut settings.port).changed() {}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use cpal::{Device, Host};
|
||||
use log::{debug, error, info};
|
||||
use minimp3::{Decoder, Error};
|
||||
use regex::Regex;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
@ -16,6 +17,7 @@ use std::thread;
|
||||
#[derive(Clone)]
|
||||
pub enum SoundDecoder {
|
||||
Detect,
|
||||
Mp3Mini,
|
||||
Rodio,
|
||||
}
|
||||
|
||||
@ -198,7 +200,7 @@ pub fn audio_thread(
|
||||
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() {
|
||||
play_file(&sink, file_name, &select_decoder);
|
||||
} else {
|
||||
@ -237,17 +239,45 @@ pub fn audio_thread(
|
||||
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?
|
||||
// This function MUST NOT return SoundDecoder::Detect
|
||||
match sound_decoder {
|
||||
SoundDecoder::Detect => SoundDecoder::Rodio, // Mp3Mini seems bugged.
|
||||
SoundDecoder::Detect => SoundDecoder::Mp3Mini,
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn play_file(sink: &rodio::Sink, file_name: String, sound_decoder: &SoundDecoder) {
|
||||
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 => {
|
||||
// Rodio currently supports Ogg, Mp3, WAV and Flac
|
||||
let file = File::open(&file_name).unwrap();
|
||||
|
1
strinto/.gitignore
vendored
Normal file
1
strinto/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
47
strinto/Cargo.lock
generated
Normal file
47
strinto/Cargo.lock
generated
Normal file
@ -0,0 +1,47 @@
|
||||
# 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"
|
14
strinto/Cargo.toml
Normal file
14
strinto/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[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"
|
62
strinto/src/lib.rs
Normal file
62
strinto/src/lib.rs
Normal file
@ -0,0 +1,62 @@
|
||||
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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user