mirror of
https://github.com/nekename/OpenDeck.git
synced 2026-10-28 18:02:23 +00:00
Merge pull request #7 from ninjadev64/elgato
Implement support for Elgato devices
This commit is contained in:
16 files changed
+386
-145
No files matched your search
Generated
+52
-5
@@ -106,6 +106,17 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.15.1"
|
||||
@@ -159,9 +170,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.5"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@@ -722,6 +733,18 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||
|
||||
[[package]]
|
||||
name = "elgato-streamdeck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa5a8005cf7a867a16d968803de4b182bc2815448af1d23865fcc025ad45ea38"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"hidapi",
|
||||
"image",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "2.4.0"
|
||||
@@ -1286,6 +1309,19 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hidapi"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b125253e27c9fd67beac20665348f4bfc5b488b5c8a1020610eeb7e6d205cde"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.25.2"
|
||||
@@ -1418,6 +1454,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"jpeg-decoder",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
@@ -1517,6 +1554,12 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.65"
|
||||
@@ -1990,7 +2033,11 @@ name = "opendeck"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.7",
|
||||
"elgato-streamdeck",
|
||||
"futures-util",
|
||||
"hidapi",
|
||||
"image",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"os_info",
|
||||
@@ -2227,7 +2274,7 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"base64 0.21.7",
|
||||
"indexmap 2.1.0",
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
@@ -2752,7 +2799,7 @@ version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"base64 0.21.7",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
@@ -3210,7 +3257,7 @@ version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"base64 0.21.7",
|
||||
"brotli",
|
||||
"ico",
|
||||
"json-patch",
|
||||
|
||||
@@ -14,13 +14,16 @@ tauri-build = { version = "1.5.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Large, fundamental libraries
|
||||
tauri = { version = "1.5.2", features = [ "shell-execute", "shell-open", "protocol-asset", "system-tray", "devtools" ] }
|
||||
tauri = { version = "1.5.2", features = [ "shell-execute", "shell-open", "system-tray", "devtools" ] }
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
serde_json = "1.0"
|
||||
serialport = "4.2.2"
|
||||
tokio = { version = "1.34.0", features = [ "full" ] }
|
||||
tokio-tungstenite = "0.20.1"
|
||||
tiny_http = "0.12.0"
|
||||
elgato-streamdeck = { version = "0.5.0", default-features = false, features = [ "async" ] }
|
||||
hidapi = { version = "2.5.0", default-features = false, features = [ "linux-static-hidraw", "macos-shared-device", "windows-native" ] }
|
||||
image = { version = "0.24.6", default-features = false, features = [ "bmp", "jpeg" ] }
|
||||
# Smaller utility libraries
|
||||
futures-util = "0.3.29"
|
||||
anyhow = "1.0.75"
|
||||
@@ -31,6 +34,7 @@ serde_with = "3.4.0"
|
||||
lazy_static = "1.4.0"
|
||||
os_info = "3.7.0"
|
||||
urlencoding = "2.1.3"
|
||||
base64 = "0.21.7"
|
||||
|
||||
[features]
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
@@ -0,0 +1,72 @@
|
||||
use crate::events::outbound::{keypad, encoder};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use elgato_streamdeck::{AsyncStreamDeck, DeviceStateUpdate, info};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use tokio::sync::Mutex;
|
||||
use base64::Engine as _;
|
||||
|
||||
lazy_static! {
|
||||
static ref ELGATO_DEVICES: Mutex<HashMap<String, AsyncStreamDeck>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub async fn update_image(context: &crate::shared::ActionContext, url: &str) -> Result<(), anyhow::Error> {
|
||||
if let Some(device) = ELGATO_DEVICES.lock().await.get(&context.device) {
|
||||
let data = url.split_once(',').unwrap().1;
|
||||
let bytes = base64::engine::general_purpose::STANDARD.decode(data)?;
|
||||
device.set_button_image(context.position, image::load_from_memory(&bytes)?).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clear_image(context: &crate::shared::ActionContext) -> Result<(), anyhow::Error> {
|
||||
if let Some(device) = ELGATO_DEVICES.lock().await.get(&context.device) {
|
||||
device.clear_button_image(context.position).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn init(device: AsyncStreamDeck) {
|
||||
let kind = device.kind();
|
||||
let device_type = match kind.product_id() {
|
||||
info::PID_STREAMDECK_ORIGINAL | info::PID_STREAMDECK_ORIGINAL_V2 | info::PID_STREAMDECK_MK2 => 0,
|
||||
info::PID_STREAMDECK_MINI | info::PID_STREAMDECK_MINI_MK2 => 1,
|
||||
info::PID_STREAMDECK_XL | info::PID_STREAMDECK_XL_V2 => 2,
|
||||
info::PID_STREAMDECK_PEDAL => 5,
|
||||
info::PID_STREAMDECK_PLUS => 7,
|
||||
_ => 7
|
||||
};
|
||||
let device_id = format!("sd-{}", device.serial_number().await.unwrap());
|
||||
super::DEVICES.lock().await.insert(device_id.clone(), super::DeviceInfo {
|
||||
id: device_id.clone(),
|
||||
name: device.product().await.unwrap(),
|
||||
rows: kind.row_count(),
|
||||
columns: kind.column_count(),
|
||||
sliders: kind.encoder_count(),
|
||||
r#type: device_type
|
||||
});
|
||||
|
||||
let reader = device.get_reader();
|
||||
ELGATO_DEVICES.lock().await.insert(device_id.clone(), device);
|
||||
loop {
|
||||
let updates = match reader.read(100.0).await {
|
||||
Ok(updates) => updates,
|
||||
Err(_) => continue
|
||||
};
|
||||
for update in updates {
|
||||
match match update {
|
||||
DeviceStateUpdate::ButtonDown(key) => keypad::key_down(&device_id, key).await,
|
||||
DeviceStateUpdate::ButtonUp(key) => keypad::key_up(&device_id, key).await,
|
||||
DeviceStateUpdate::EncoderTwist(dial, ticks) => encoder::dial_rotate(&device_id, dial, ticks.into()).await,
|
||||
DeviceStateUpdate::EncoderDown(dial) => encoder::dial_press(&device_id, "dialDown", dial).await,
|
||||
DeviceStateUpdate::EncoderUp(dial) => encoder::dial_press(&device_id, "dialUp", dial).await,
|
||||
_ => Ok(())
|
||||
} {
|
||||
Ok(_) => (),
|
||||
Err(error) => log::warn!("Failed to process device event {:?}: {}", update, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod prontokey;
|
||||
pub mod elgato;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -6,6 +7,7 @@ use lazy_static::lazy_static;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use serde::Serialize;
|
||||
use log::warn;
|
||||
|
||||
/// Metadata of a device.
|
||||
#[derive(Clone, Serialize)]
|
||||
@@ -28,11 +30,25 @@ pub fn initialise_devices() {
|
||||
for port in serialport::available_ports().unwrap_or_default() {
|
||||
if let serialport::SerialPortType::UsbPort(info) = port.port_type {
|
||||
if info.vid == 0x10c4 && info.pid == 0xea60 {
|
||||
tokio::spawn(prontokey::ProntoKeyDevice::init(port.port_name));
|
||||
tokio::spawn(prontokey::init(port.port_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through detected Elgato devices and attempt to register them.
|
||||
match elgato_streamdeck::new_hidapi() {
|
||||
Ok(hid) => {
|
||||
for (kind, serial) in elgato_streamdeck::asynchronous::list_devices_async(&hid) {
|
||||
match elgato_streamdeck::AsyncStreamDeck::connect(&hid, kind, &serial) {
|
||||
Ok(device) => { tokio::spawn(elgato::init(device)); },
|
||||
Err(error) => warn!("Failed to connect to Elgato device: {}", error)
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => warn!("Failed to initialise hidapi: {}", error)
|
||||
}
|
||||
|
||||
|
||||
// Create a virtual device for testing without a physical device.
|
||||
tokio::spawn(async move {
|
||||
let mut devices = DEVICES.lock().await;
|
||||
|
||||
@@ -8,102 +8,98 @@ use std::time::Duration;
|
||||
use serde_json::Value;
|
||||
use log::{warn, error};
|
||||
|
||||
pub struct ProntoKeyDevice {}
|
||||
/// Attempt to open a serial connection with the device and handle incoming data.
|
||||
pub async fn init(port: String) {
|
||||
let mut initialised = false;
|
||||
let mut device_id = "".to_owned();
|
||||
let mut last_key: u8 = 0;
|
||||
let mut last_sliders: Vec<i16> = vec![0; 2];
|
||||
|
||||
impl ProntoKeyDevice {
|
||||
/// Attempt to open a serial connection with the device and handle incoming data.
|
||||
pub async fn init(port: String) {
|
||||
let mut initialised = false;
|
||||
let mut device_id = "".to_owned();
|
||||
let mut last_key: u8 = 0;
|
||||
let mut last_sliders: Vec<i16> = vec![0; 2];
|
||||
|
||||
let mut port = match
|
||||
serialport::new(port, 57600)
|
||||
.timeout(Duration::from_millis(10))
|
||||
.open()
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(error) => {
|
||||
error!("Failed to open serial port: {}", error);
|
||||
panic!()
|
||||
}
|
||||
};
|
||||
let _ = port.write("register".as_bytes());
|
||||
|
||||
let mut serial_buf: Vec<u8> = vec![0; 1024];
|
||||
let mut holding_string = String::from("");
|
||||
|
||||
loop {
|
||||
match port.read(serial_buf.as_mut_slice()) {
|
||||
Ok(t) => {
|
||||
match std::str::from_utf8(&serial_buf[..t]) {
|
||||
Ok(data) => holding_string += data,
|
||||
Err(_) => break
|
||||
}
|
||||
// Iterate through JSON objects received from device that are being held in the buffer.
|
||||
while holding_string.contains('}') {
|
||||
let index = holding_string.find('}').unwrap_or_default();
|
||||
let chunk = holding_string[..=index].trim();
|
||||
let j: Value = match serde_json::from_str(chunk) {
|
||||
Ok(data) => data,
|
||||
Err(_) => continue
|
||||
};
|
||||
holding_string = holding_string[(index + 1)..].to_owned();
|
||||
|
||||
// If the device is uninitialised, attempt to read its MAC address and initialise.
|
||||
if !initialised {
|
||||
if let Value::String(address) = &j["address"] {
|
||||
initialised = true;
|
||||
device_id = format!("pk-{}", address);
|
||||
super::DEVICES.lock().await.insert(device_id.clone(), DeviceInfo {
|
||||
id: device_id.clone(),
|
||||
name: "ProntoKey".to_owned(),
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
sliders: 2,
|
||||
r#type: 7
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle key presses and releases.
|
||||
if let Value::Number(num) = &j["key"] {
|
||||
match num.as_u64().unwrap_or_default() as u8 {
|
||||
0 => {
|
||||
let _ = outbound::keypad::key_up(&device_id, last_key - 1).await;
|
||||
},
|
||||
val => {
|
||||
let _ = outbound::keypad::key_down(&device_id, val - 1).await;
|
||||
last_key = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle slider value changes.
|
||||
if let Value::Number(val) = &j["slider0"] {
|
||||
let val: i16 = match val.as_i64() {
|
||||
Some(v) => v as i16,
|
||||
_ => last_sliders[0]
|
||||
};
|
||||
let _ = outbound::encoder::dial_rotate(&device_id, 0, val - last_sliders[0]).await;
|
||||
last_sliders[0] = val;
|
||||
}
|
||||
if let Value::Number(val) = &j["slider1"] {
|
||||
let val: i16 = match val.as_i64() {
|
||||
Some(v) => v as i16,
|
||||
_ => last_sliders[1]
|
||||
};
|
||||
let _ = outbound::encoder::dial_rotate(&device_id, 1, val - last_sliders[1]).await;
|
||||
last_sliders[1] = val;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(ref error) if error.kind() == std::io::ErrorKind::TimedOut => (),
|
||||
Err(error) => warn!("Failed to decode serial message from ProntoKey device: {}", error)
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
let mut port = match
|
||||
serialport::new(port, 57600)
|
||||
.timeout(Duration::from_millis(10))
|
||||
.open()
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(error) => {
|
||||
error!("Failed to open serial port: {}", error);
|
||||
panic!()
|
||||
}
|
||||
};
|
||||
let _ = port.write("register".as_bytes());
|
||||
|
||||
let mut serial_buf: Vec<u8> = vec![0; 1024];
|
||||
let mut holding_string = String::from("");
|
||||
|
||||
loop {
|
||||
match port.read(serial_buf.as_mut_slice()) {
|
||||
Ok(t) => {
|
||||
match std::str::from_utf8(&serial_buf[..t]) {
|
||||
Ok(data) => holding_string += data,
|
||||
Err(_) => break
|
||||
}
|
||||
// Iterate through JSON objects received from device that are being held in the buffer.
|
||||
while holding_string.contains('}') {
|
||||
let index = holding_string.find('}').unwrap_or_default();
|
||||
let chunk = holding_string[..=index].trim();
|
||||
let j: Value = match serde_json::from_str(chunk) {
|
||||
Ok(data) => data,
|
||||
Err(_) => continue
|
||||
};
|
||||
holding_string = holding_string[(index + 1)..].to_owned();
|
||||
|
||||
// If the device is uninitialised, attempt to read its MAC address and initialise.
|
||||
if !initialised {
|
||||
if let Value::String(address) = &j["address"] {
|
||||
initialised = true;
|
||||
device_id = format!("pk-{}", address);
|
||||
super::DEVICES.lock().await.insert(device_id.clone(), DeviceInfo {
|
||||
id: device_id.clone(),
|
||||
name: "ProntoKey".to_owned(),
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
sliders: 2,
|
||||
r#type: 7
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle key presses and releases.
|
||||
if let Value::Number(num) = &j["key"] {
|
||||
match num.as_u64().unwrap_or_default() as u8 {
|
||||
0 => {
|
||||
let _ = outbound::keypad::key_up(&device_id, last_key - 1).await;
|
||||
},
|
||||
val => {
|
||||
let _ = outbound::keypad::key_down(&device_id, val - 1).await;
|
||||
last_key = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle slider value changes.
|
||||
if let Value::Number(val) = &j["slider0"] {
|
||||
let val: i16 = match val.as_i64() {
|
||||
Some(v) => v as i16,
|
||||
_ => last_sliders[0]
|
||||
};
|
||||
let _ = outbound::encoder::dial_rotate(&device_id, 0, val - last_sliders[0]).await;
|
||||
last_sliders[0] = val;
|
||||
}
|
||||
if let Value::Number(val) = &j["slider1"] {
|
||||
let val: i16 = match val.as_i64() {
|
||||
Some(v) => v as i16,
|
||||
_ => last_sliders[1]
|
||||
};
|
||||
let _ = outbound::encoder::dial_rotate(&device_id, 1, val - last_sliders[1]).await;
|
||||
last_sliders[1] = val;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(ref error) if error.kind() == std::io::ErrorKind::TimedOut => (),
|
||||
Err(error) => warn!("Failed to decode serial message from ProntoKey device: {}", error)
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
@@ -207,6 +207,15 @@ pub async fn switch_property_inspector(old: Option<ActionContext>, new: Option<A
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_image(context: ActionContext, image: String) {
|
||||
if context.device.starts_with("sd-") {
|
||||
if let Err(error) = crate::devices::elgato::update_image(&context, &image).await {
|
||||
log::warn!("Failed to update device image at context {} with image {}: {}", context, image, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_state(app: &tauri::AppHandle, instance: &ActionInstance) -> Result<(), anyhow::Error> {
|
||||
let window = app.get_window("main").unwrap();
|
||||
window.emit("update_state", serde_json::to_string(instance).unwrap())?;
|
||||
|
||||
@@ -22,6 +22,22 @@ struct DialRotateEvent {
|
||||
payload: DialRotatePayload
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DialPressPayload {
|
||||
controller: &'static str,
|
||||
settings: serde_json::Value,
|
||||
coordinates: Coordinates
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DialPressEvent {
|
||||
event: &'static str,
|
||||
action: String,
|
||||
context: ActionContext,
|
||||
device: String,
|
||||
payload: DialPressPayload
|
||||
}
|
||||
|
||||
pub async fn dial_rotate(device: &str, index: u8, ticks: i16) -> Result<(), anyhow::Error> {
|
||||
let instance = match get_instance(device, index, "Encoder").await? {
|
||||
Some(instance) => instance,
|
||||
@@ -44,3 +60,25 @@ pub async fn dial_rotate(device: &str, index: u8, ticks: i16) -> Result<(), anyh
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
pub async fn dial_press(device: &str, event: &'static str, index: u8) -> Result<(), anyhow::Error> {
|
||||
let instance = match get_instance(device, index, "Encoder").await? {
|
||||
Some(instance) => instance,
|
||||
None => return Ok(())
|
||||
};
|
||||
|
||||
send_to_plugin(&instance.action.plugin, &DialPressEvent {
|
||||
event,
|
||||
action: instance.action.uuid.clone(),
|
||||
context: instance.context.clone(),
|
||||
device: instance.context.device.clone(),
|
||||
payload: DialPressPayload {
|
||||
controller: "Encoder",
|
||||
settings: instance.settings.clone(),
|
||||
coordinates: Coordinates {
|
||||
row: instance.context.position / 3,
|
||||
column: instance.context.position % 3
|
||||
}
|
||||
}
|
||||
}).await
|
||||
}
|
||||
@@ -32,5 +32,11 @@ pub async fn will_disappear(instance: &ActionInstance) -> Result<(), anyhow::Err
|
||||
payload: GenericInstancePayload::new(instance)
|
||||
}).await?;
|
||||
|
||||
if instance.context.device.starts_with("sd-") {
|
||||
if let Err(error) = crate::devices::elgato::clear_image(&instance.context).await {
|
||||
log::warn!("Failed to clear device image at context {}: {}", instance.context, error);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -30,7 +30,8 @@ async fn main() {
|
||||
frontend::set_selected_profile,
|
||||
frontend::delete_profile,
|
||||
frontend::make_info,
|
||||
frontend::switch_property_inspector
|
||||
frontend::switch_property_inspector,
|
||||
frontend::update_image
|
||||
])
|
||||
.plugin(tauri_plugin_log::Builder::default()
|
||||
.targets([
|
||||
|
||||
@@ -310,20 +310,30 @@ async fn init_browser_server(prefix: path::PathBuf) {
|
||||
let content_type = mime(&extension);
|
||||
|
||||
if content_type.starts_with("text/") || content_type == "image/svg+xml" {
|
||||
let response = tiny_http::Response::from_string(fs::read_to_string(url).unwrap_or_default());
|
||||
let _ = request.respond(response.with_header(tiny_http::Header {
|
||||
let mut response = tiny_http::Response::from_string(fs::read_to_string(url).unwrap_or_default());
|
||||
response.add_header(tiny_http::Header {
|
||||
field: "Access-Control-Allow-Origin".parse().unwrap(),
|
||||
value: "*".parse().unwrap()
|
||||
});
|
||||
response.add_header(tiny_http::Header {
|
||||
field: "Content-Type".parse().unwrap(),
|
||||
value: content_type.parse().unwrap()
|
||||
}));
|
||||
});
|
||||
let _ = request.respond(response);
|
||||
} else {
|
||||
let response = tiny_http::Response::from_file(match fs::File::open(url) {
|
||||
let mut response = tiny_http::Response::from_file(match fs::File::open(url) {
|
||||
Ok(file) => file,
|
||||
Err(_) => continue
|
||||
});
|
||||
let _ = request.respond(response.with_header(tiny_http::Header {
|
||||
response.add_header(tiny_http::Header {
|
||||
field: "Access-Control-Allow-Origin".parse().unwrap(),
|
||||
value: "*".parse().unwrap()
|
||||
});
|
||||
response.add_header(tiny_http::Header {
|
||||
field: "Content-Type".parse().unwrap(),
|
||||
value: content_type.parse().unwrap()
|
||||
}));
|
||||
});
|
||||
let _ = request.respond(response);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -41,15 +41,9 @@
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: https://appstore.elgato.com; frame-src http://localhost:57118; connect-src https://appstore.elgato.com;"
|
||||
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://localhost:57118; https://appstore.elgato.com; frame-src http://localhost:57118; connect-src https://appstore.elgato.com;"
|
||||
},
|
||||
"allowlist": {
|
||||
"protocol": {
|
||||
"asset": true,
|
||||
"assetScope": [
|
||||
"$APPCONFIG/plugins/**/*"
|
||||
]
|
||||
},
|
||||
"shell": {
|
||||
"open": true,
|
||||
"execute": true
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
import type { ActionInstance } from "$lib/ActionInstance";
|
||||
|
||||
import { inspectedInstance } from "$lib/propertyInspector";
|
||||
import { getImage, renderImage } from "$lib/rendererHelper";
|
||||
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||
|
||||
export let context: string;
|
||||
export let instance: ActionInstance | null;
|
||||
|
||||
$: state = instance?.states[instance?.current_state];
|
||||
let oldImage: string;
|
||||
|
||||
listen("update_state", ({ payload }: { payload: string }) => {
|
||||
let i = JSON.parse(payload);
|
||||
@@ -55,20 +54,12 @@
|
||||
timeouts.push(setTimeout(() => showOk = 0, 2e3));
|
||||
});
|
||||
|
||||
function getImage(image: string): string {
|
||||
if (!image.startsWith("data:")) return convertFileSrc(image);
|
||||
const svgxmlre = /^data:image\/svg\+xml,(.+)/;
|
||||
const base64re = /^data:image\/(apng|avif|gif|jpeg|png|svg\+xml|webp|bmp|x-icon|tiff);base64,([A-Za-z0-9+/]+={0,2})?/;
|
||||
if (svgxmlre.test(image)) {
|
||||
image = "data:image/svg+xml;base64," + btoa(decodeURIComponent((svgxmlre.exec(image) as RegExpExecArray)[1].replace(/\;$/, "")));
|
||||
}
|
||||
if (base64re.test(image)) {
|
||||
let exec = base64re.exec(image)!;
|
||||
if (!exec[2]) return oldImage;
|
||||
else image = exec[0];
|
||||
}
|
||||
let oldImage: string;
|
||||
let image: string;
|
||||
$: {
|
||||
image = getImage(state?.image, oldImage);
|
||||
oldImage = image;
|
||||
return image;
|
||||
if (state) renderImage(context, state);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -81,7 +72,7 @@
|
||||
{#if instance && state}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<img
|
||||
src={getImage(state.image)}
|
||||
src={image}
|
||||
class="p-2 w-full rounded-xl"
|
||||
alt={instance.action.tooltip}
|
||||
on:click={clear} on:keyup={clear}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Action } from "$lib/Action";
|
||||
|
||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||
|
||||
export let action: Action;
|
||||
|
||||
function handleDragStart(event: DragEvent) {
|
||||
@@ -12,7 +10,7 @@
|
||||
|
||||
<div class="flex flex-row items-center mt-2 mb-2 space-x-2">
|
||||
<img
|
||||
src={convertFileSrc(action.icon)}
|
||||
src={"http://localhost:57118" + action.icon}
|
||||
alt={action.tooltip}
|
||||
class="w-12 h-12 rounded-sm"
|
||||
draggable
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import type { ActionInstance } from "$lib/ActionInstance";
|
||||
|
||||
import { inspectedInstance } from "$lib/propertyInspector";
|
||||
import { getImage } from "$lib/rendererHelper";
|
||||
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||
|
||||
export let context: string;
|
||||
export let instance: ActionInstance | null;
|
||||
@@ -54,13 +54,11 @@
|
||||
timeouts.push(setTimeout(() => showOk = 0, 2e3));
|
||||
});
|
||||
|
||||
function getImage(image: string): string {
|
||||
if (!image.startsWith("data:")) return convertFileSrc(image);
|
||||
const svgxmlre = /^data:image\/svg\+xml,(.+)/;
|
||||
if (svgxmlre.test(image)) {
|
||||
image = "data:image/svg+xml;base64," + btoa(decodeURIComponent((svgxmlre.exec(image) as RegExpExecArray)[1].replace(/\;$/, "")));
|
||||
}
|
||||
return image;
|
||||
let oldImage: string;
|
||||
let image: string;
|
||||
$: {
|
||||
image = getImage(state?.image, oldImage);
|
||||
oldImage = image;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -73,7 +71,7 @@
|
||||
{#if instance && state}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<img
|
||||
src={getImage(state.image)}
|
||||
src={image}
|
||||
class="p-2 w-full rounded-xl"
|
||||
alt={instance.action.tooltip}
|
||||
on:click={clear} on:keyup={clear}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type ActionState = {
|
||||
text: string,
|
||||
show: boolean,
|
||||
colour: string,
|
||||
alignment: string,
|
||||
alignment: "top" | "middle" | "bottom",
|
||||
style: string,
|
||||
size: string,
|
||||
underline: boolean
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
|
||||
import type { ActionState } from "./ActionState";
|
||||
|
||||
export function getImage(image: string | undefined, fallback: string): string {
|
||||
if (!image) return fallback;
|
||||
if (!image.startsWith("data:")) return "http://localhost:57118" + image;
|
||||
const svgxmlre = /^data:image\/svg\+xml,(.+)/;
|
||||
const base64re = /^data:image\/(apng|avif|gif|jpeg|png|svg\+xml|webp|bmp|x-icon|tiff);base64,([A-Za-z0-9+/]+={0,2})?/;
|
||||
if (svgxmlre.test(image)) {
|
||||
image = "data:image/svg+xml;base64," + btoa(decodeURIComponent((svgxmlre.exec(image) as RegExpExecArray)[1].replace(/\;$/, "")));
|
||||
}
|
||||
if (base64re.test(image)) {
|
||||
let exec = base64re.exec(image)!;
|
||||
if (!exec[2]) return fallback;
|
||||
else image = exec[0];
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
export async function renderImage(actionContext: string, state: ActionState) {
|
||||
// Create canvas
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = 144;
|
||||
canvas.height = 144;
|
||||
let context = canvas.getContext("2d");
|
||||
if (!context) return;
|
||||
|
||||
// Load image
|
||||
let image = document.createElement("img");
|
||||
image.crossOrigin = "anonymous";
|
||||
image.src = getImage(state.image, undefined!);
|
||||
if (image.src == undefined) return;
|
||||
await new Promise((resolve) => {
|
||||
image.onload = resolve;
|
||||
});
|
||||
|
||||
// Draw image
|
||||
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw text
|
||||
if (state.show) {
|
||||
context.textAlign = "center";
|
||||
context.font = `${state.size}px serif`;
|
||||
context.fillStyle = state.colour;
|
||||
let x = canvas.width / 2;
|
||||
let y = canvas.height / 2 + (parseInt(state.size) / 4);
|
||||
switch (state.alignment) {
|
||||
case "top": y = parseInt(state.size); break;
|
||||
case "bottom": y = canvas.height - 5; break;
|
||||
}
|
||||
context.fillText(state.text, x, y);
|
||||
if (state.underline) {
|
||||
let width = context.measureText(state.text).width;
|
||||
context.fillRect(x - (width / 2), y + 2, width, 3);
|
||||
}
|
||||
}
|
||||
|
||||
await invoke("update_image", { context: actionContext, image: canvas.toDataURL("image/jpeg") });
|
||||
canvas.remove();
|
||||
}
|
||||
Reference in new issue
Block a user