Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
61ed1afe9f | |||
543ea46ad0 | |||
b90a37d671 | |||
0003f0a420 | |||
e5b572fcc1 | |||
36d7a64148 | |||
090bea5805 | |||
307daaea7b |
2881
Cargo.lock
generated
2881
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@ -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"
|
||||||
|
4
build.rs
4
build.rs
@ -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
BIN
icons/soundboard.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 211 KiB |
@ -1 +1 @@
|
|||||||
icon-red ICON "icons/icon-red.ico"
|
icon-soundboard ICON "icons/soundboard.ico"
|
@ -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
251
src/bin/ui_icon.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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::{
|
||||||
|
@ -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
144
src/icon.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
32
src/main.rs
32
src/main.rs
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
112
src/ui.rs
@ -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() {}
|
||||||
|
@ -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
1
strinto/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/target
|
|
47
strinto/Cargo.lock
generated
47
strinto/Cargo.lock
generated
@ -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"
|
|
@ -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"
|
|
@ -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()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user