Introduce new Context type to identify entire slot

This commit is contained in:
ninjadev64 committed 2024-04-07 11:34:48 +01:00
1 parent 4dbafce137
commit c623ababf9
20 files changed
+332 -209

No files matched your search

+2 -2
View File
@@ -10,7 +10,7 @@ use tokio::sync::Mutex;
static ELGATO_DEVICES: Lazy<Mutex<HashMap<String, AsyncStreamDeck>>> = Lazy::new(|| Mutex::new(HashMap::new()));
pub async fn update_image(context: &crate::shared::ActionContext, url: &str) -> Result<(), anyhow::Error> {
pub async fn update_image(context: &crate::shared::Context, 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)?;
@@ -19,7 +19,7 @@ pub async fn update_image(context: &crate::shared::ActionContext, url: &str) ->
Ok(())
}
pub async fn clear_image(context: &crate::shared::ActionContext) -> Result<(), StreamDeckError> {
pub async fn clear_image(context: &crate::shared::Context) -> Result<(), StreamDeckError> {
if let Some(device) = ELGATO_DEVICES.lock().await.get(&context.device) {
device.clear_button_image(context.position).await?;
}
+69 -81
View File
@@ -1,6 +1,6 @@
use crate::devices::DEVICES;
use crate::shared::{Action, ActionContext, ActionInstance, CATEGORIES};
use crate::store::profiles::{get_device_profiles, DEVICE_STORES, PROFILE_STORES};
use crate::shared::{Action, ActionContext, ActionInstance, Context, CATEGORIES};
use crate::store::profiles::{get_device_profiles, get_slot, lock_mutexes, save_profile, Locks, DEVICE_STORES, PROFILE_STORES};
use std::collections::HashMap;
@@ -58,17 +58,17 @@ pub async fn set_selected_profile(app: tauri::AppHandle, device: String, id: Str
if store.value.selected_profile != id {
let old_profile = &profile_stores.get_profile_store(devices.get(&device).unwrap(), &store.value.selected_profile, &app)?.value;
for slot in old_profile.keys.iter().chain(&old_profile.sliders).flat_map(|x| x) {
for slot in old_profile.keys.iter().chain(&old_profile.sliders) {
for instance in slot {
let _ = crate::events::outbound::will_appear::will_disappear(instance).await;
let _ = crate::events::outbound::will_appear::will_disappear(instance, slot.len() > 1).await;
}
}
}
let new_profile = &profile_stores.get_profile_store(devices.get(&device).unwrap(), &id, &app)?.value;
for slot in new_profile.keys.iter().chain(&new_profile.sliders).flat_map(|x| x) {
for slot in new_profile.keys.iter().chain(&new_profile.sliders) {
for instance in slot {
let _ = crate::events::outbound::will_appear::will_appear(instance).await;
let _ = crate::events::outbound::will_appear::will_appear(instance, slot.len() > 1).await;
}
}
@@ -85,108 +85,84 @@ pub async fn delete_profile(app: tauri::AppHandle, device: String, profile: Stri
}
#[tauri::command]
pub async fn create_instance(app: tauri::AppHandle, action: Action, context: ActionContext) -> Result<Option<ActionInstance>, Error> {
pub async fn create_instance(action: Action, context: Context) -> Result<Option<Vec<ActionInstance>>, Error> {
if !action.controllers.contains(&context.controller) {
return Ok(None);
}
let mut locks = lock_mutexes().await;
let slot = get_slot(&context, &mut locks).await?;
let index = match slot.last() {
None => 0,
Some(instance) => instance.context.index + 1,
};
let instance = ActionInstance {
action: action.clone(),
context: context.clone(),
context: ActionContext::from_context(context.clone(), index),
states: action.states.clone(),
current_state: 0,
settings: serde_json::Value::Object(serde_json::Map::new()),
};
let mut profile_stores = PROFILE_STORES.lock().await;
let store = profile_stores.get_profile_store(DEVICES.lock().await.get(&context.device).unwrap(), &context.profile, &app)?;
slot.push(instance.clone());
let slot = slot.clone();
if context.controller == "Encoder" {
if let Some(old) = &store.value.sliders[context.position as usize] {
for instance in old {
let _ = crate::events::outbound::will_appear::will_disappear(instance).await;
}
}
store.value.sliders[context.position as usize] = Some(vec![instance.clone()]);
} else {
if let Some(old) = &store.value.keys[context.position as usize] {
for instance in old {
let _ = crate::events::outbound::will_appear::will_disappear(instance).await;
}
}
store.value.keys[context.position as usize] = Some(vec![instance.clone()]);
}
save_profile(&context.device, &mut locks).await?;
let _ = crate::events::outbound::will_appear::will_appear(&instance, index != 0).await;
let _ = crate::events::outbound::will_appear::will_appear(&instance).await;
store.save()?;
Ok(Some(instance))
Ok(Some(slot))
}
#[tauri::command]
pub async fn move_instance(app: tauri::AppHandle, mut instance: ActionInstance, context: ActionContext) -> Result<Option<ActionInstance>, Error> {
if !instance.action.controllers.contains(&context.controller) {
pub async fn move_slot(source: Context, destination: Context) -> Result<Option<Vec<ActionInstance>>, Error> {
if source.controller != destination.controller {
return Ok(None);
}
let mut profile_stores = PROFILE_STORES.lock().await;
let store = profile_stores.get_profile_store(DEVICES.lock().await.get(&context.device).unwrap(), &context.profile, &app)?;
let mut locks = lock_mutexes().await;
let src = get_slot(&source, &mut locks).await?;
let multi_action = src.len() > 1;
let _ = crate::events::outbound::will_appear::will_disappear(&instance).await;
if instance.context.controller == "Encoder" {
store.value.sliders[instance.context.position as usize] = None;
} else {
store.value.keys[instance.context.position as usize] = None;
let mut vec: Vec<ActionInstance> = vec![];
for (index, instance) in src.iter_mut().enumerate() {
let mut new = instance.clone();
new.context = ActionContext::from_context(destination.clone(), index as u16);
vec.push(new);
}
if context.controller == "Encoder" {
if let Some(old) = &store.value.sliders[context.position as usize] {
for instance in old {
let _ = crate::events::outbound::will_appear::will_disappear(instance).await;
}
}
instance.context = context.clone();
store.value.sliders[context.position as usize] = Some(vec![instance.clone()]);
} else {
if let Some(old) = &store.value.keys[context.position as usize] {
for instance in old {
let _ = crate::events::outbound::will_appear::will_disappear(instance).await;
}
}
instance.context = context.clone();
store.value.keys[context.position as usize] = Some(vec![instance.clone()]);
let dst = get_slot(&destination, &mut locks).await?;
if !dst.is_empty() {
return Ok(None);
}
*dst = vec.clone();
let src = get_slot(&source, &mut locks).await?;
for old in &*src {
let _ = crate::events::outbound::will_appear::will_disappear(old, multi_action).await;
}
*src = vec![];
for new in &vec {
let _ = crate::events::outbound::will_appear::will_appear(new, multi_action).await;
}
let _ = crate::events::outbound::will_appear::will_appear(&instance).await;
save_profile(&destination.device, &mut locks).await?;
store.save()?;
Ok(Some(instance))
Ok(Some(vec))
}
#[tauri::command]
pub async fn clear_slot(app: tauri::AppHandle, context: ActionContext) -> Result<(), Error> {
let mut profile_stores = PROFILE_STORES.lock().await;
let store = profile_stores.get_profile_store(DEVICES.lock().await.get(&context.device).unwrap(), &context.profile, &app)?;
pub async fn clear_slot(context: Context) -> Result<(), Error> {
let mut locks = lock_mutexes().await;
let slot = get_slot(&context, &mut locks).await?;
if context.controller == "Encoder" {
if let Some(slot) = &store.value.sliders[context.position as usize] {
for instance in slot {
let _ = crate::events::outbound::will_appear::will_disappear(instance).await;
}
}
store.value.sliders[context.position as usize] = None;
} else {
if let Some(slot) = &store.value.keys[context.position as usize] {
for instance in slot {
let _ = crate::events::outbound::will_appear::will_disappear(instance).await;
}
}
store.value.keys[context.position as usize] = None;
for instance in &*slot {
let _ = crate::events::outbound::will_appear::will_disappear(instance, slot.len() > 1).await;
}
store.save()?;
*slot = vec![];
save_profile(&context.device, &mut locks).await?;
Ok(())
}
@@ -222,16 +198,28 @@ pub async fn switch_property_inspector(old: Option<ActionContext>, new: Option<A
}
#[tauri::command]
pub async fn update_image(context: ActionContext, image: String) {
pub async fn update_image(context: Context, 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);
log::warn!("Failed to update device image: {}", error);
}
}
}
pub async fn update_state(app: &tauri::AppHandle, instance: &ActionInstance) -> Result<(), tauri::Error> {
#[derive(Clone, serde::Serialize)]
struct UpdateStateEvent {
context: Context,
contents: Vec<ActionInstance>,
}
pub async fn update_state(app: &tauri::AppHandle, context: Context, locks: &mut Locks<'_>) -> Result<(), anyhow::Error> {
let window = app.get_window("main").unwrap();
window.emit("update_state", instance)?;
window.emit(
"update_state",
UpdateStateEvent {
contents: get_slot(&context, locks).await?.clone(),
context,
},
)?;
Ok(())
}
+3 -3
View File
@@ -4,7 +4,7 @@ use crate::store::profiles::{get_instance, lock_mutexes, save_profile};
pub async fn set_settings(event: super::ContextAndPayloadEvent<serde_json::Value>, from_property_inspector: bool) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
if let Some(instance) = get_instance(&event.context.device, &event.context.controller, event.context.position, event.context.index, &mut locks).await? {
if let Some(instance) = get_instance(&event.context, &mut locks).await? {
instance.settings = event.payload;
outbound::did_receive_settings(instance, !from_property_inspector).await?;
save_profile(&event.context.device, &mut locks).await?;
@@ -16,8 +16,8 @@ pub async fn set_settings(event: super::ContextAndPayloadEvent<serde_json::Value
pub async fn get_settings(event: super::ContextEvent, from_property_inspector: bool) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
if let Some(instance) = get_instance(&event.context.device, &event.context.controller, event.context.position, event.context.index, &mut locks).await? {
outbound::did_receive_settings(&instance, from_property_inspector).await?;
if let Some(instance) = get_instance(&event.context, &mut locks).await? {
outbound::did_receive_settings(instance, from_property_inspector).await?;
}
Ok(())
+6 -6
View File
@@ -25,7 +25,7 @@ pub struct SetStatePayload {
pub async fn set_title(event: ContextAndPayloadEvent<SetTitlePayload>) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
if let Some(instance) = get_instance(&event.context.device, &event.context.controller, event.context.position, event.context.index, &mut locks).await? {
if let Some(instance) = get_instance(&event.context, &mut locks).await? {
if let Some(state) = event.payload.state {
instance.states[state as usize].text = event.payload.title.unwrap_or(instance.action.states[state as usize].text.clone());
} else {
@@ -33,7 +33,7 @@ pub async fn set_title(event: ContextAndPayloadEvent<SetTitlePayload>) -> Result
state.text = event.payload.title.clone().unwrap_or(instance.action.states[index].text.clone());
}
}
update_state(crate::APP_HANDLE.get().unwrap(), instance).await?;
update_state(crate::APP_HANDLE.get().unwrap(), (&instance.context).into(), &mut locks).await?;
}
save_profile(&event.context.device, &mut locks).await?;
@@ -43,7 +43,7 @@ pub async fn set_title(event: ContextAndPayloadEvent<SetTitlePayload>) -> Result
pub async fn set_image(event: ContextAndPayloadEvent<SetImagePayload>) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
if let Some(instance) = get_instance(&event.context.device, &event.context.controller, event.context.position, event.context.index, &mut locks).await? {
if let Some(instance) = get_instance(&event.context, &mut locks).await? {
if let Some(state) = event.payload.state {
instance.states[state as usize].image = event.payload.image.unwrap_or(instance.action.states[state as usize].image.clone());
} else {
@@ -51,7 +51,7 @@ pub async fn set_image(event: ContextAndPayloadEvent<SetImagePayload>) -> Result
state.image = event.payload.image.clone().unwrap_or(instance.action.states[index].image.clone());
}
}
update_state(crate::APP_HANDLE.get().unwrap(), instance).await?;
update_state(crate::APP_HANDLE.get().unwrap(), (&instance.context).into(), &mut locks).await?;
}
save_profile(&event.context.device, &mut locks).await?;
@@ -61,9 +61,9 @@ pub async fn set_image(event: ContextAndPayloadEvent<SetImagePayload>) -> Result
pub async fn set_state(event: ContextAndPayloadEvent<SetStatePayload>) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
if let Some(instance) = get_instance(&event.context.device, &event.context.controller, event.context.position, event.context.index, &mut locks).await? {
if let Some(instance) = get_instance(&event.context, &mut locks).await? {
instance.current_state = event.payload.state;
update_state(crate::APP_HANDLE.get().unwrap(), instance).await?;
update_state(crate::APP_HANDLE.get().unwrap(), (&instance.context).into(), &mut locks).await?;
}
save_profile(&event.context.device, &mut locks).await?;
+3 -2
View File
@@ -12,8 +12,9 @@ use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tokio_tungstenite::{tungstenite::Message, WebSocketStream};
static PLUGIN_SOCKETS: Lazy<Mutex<HashMap<String, SplitSink<WebSocketStream<TcpStream>, Message>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static PROPERTY_INSPECTOR_SOCKETS: Lazy<Mutex<HashMap<String, SplitSink<WebSocketStream<TcpStream>, Message>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
type Sockets = Lazy<Mutex<HashMap<String, SplitSink<WebSocketStream<TcpStream>, Message>>>>;
static PLUGIN_SOCKETS: Sockets = Lazy::new(|| Mutex::new(HashMap::new()));
static PROPERTY_INSPECTOR_SOCKETS: Sockets = Lazy::new(|| Mutex::new(HashMap::new()));
static PLUGIN_QUEUES: Lazy<Mutex<HashMap<String, Vec<Message>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static PROPERTY_INSPECTOR_QUEUES: Lazy<Mutex<HashMap<String, Vec<Message>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
+16 -6
View File
@@ -40,10 +40,15 @@ struct DialPressEvent {
pub async fn dial_rotate(device: &str, index: u8, ticks: i16) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
let instance = match get_instance(device, "Encoder", index, 0, &mut locks).await? {
Some(instance) => instance,
None => return Ok(()),
let profile = locks.device_stores.get_device_store(device, crate::APP_HANDLE.get().unwrap())?.value.selected_profile.clone();
let context = ActionContext {
device: device.to_owned(),
profile,
controller: "Keypad".to_owned(),
position: index,
index: 0,
};
let Some(instance) = get_instance(&context, &mut locks).await? else { return Ok(()) };
send_to_plugin(
&instance.action.plugin,
@@ -68,10 +73,15 @@ pub async fn dial_rotate(device: &str, index: u8, ticks: i16) -> Result<(), anyh
pub async fn dial_press(device: &str, event: &'static str, index: u8) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
let instance = match get_instance(device, "Encoder", index, 0, &mut locks).await? {
Some(instance) => instance,
None => return Ok(()),
let profile = locks.device_stores.get_device_store(device, crate::APP_HANDLE.get().unwrap())?.value.selected_profile.clone();
let context = ActionContext {
device: device.to_owned(),
profile,
controller: "Keypad".to_owned(),
position: index,
index: 0,
};
let Some(instance) = get_instance(&context, &mut locks).await? else { return Ok(()) };
send_to_plugin(
&instance.action.plugin,
+68 -25
View File
@@ -1,6 +1,8 @@
use std::time::Duration;
use super::{send_to_plugin, GenericInstancePayload};
use crate::shared::ActionContext;
use crate::shared::{ActionContext, Context};
use crate::store::profiles::{get_slot, lock_mutexes, save_profile};
use serde::Serialize;
@@ -16,12 +18,17 @@ struct KeyEvent {
pub async fn key_down(device: &str, key: u8) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
let slot = match get_slot(device, "Keypad", key, &mut locks).await? {
Some(slot) => slot,
None => return Ok(()),
let profile = locks.device_stores.get_device_store(device, crate::APP_HANDLE.get().unwrap())?.value.selected_profile.clone();
let context = Context {
device: device.to_owned(),
profile,
controller: "Keypad".to_owned(),
position: key,
};
let slot = get_slot(&context, &mut locks).await?;
for instance in slot {
if slot.len() == 1 {
let instance = &slot[0];
send_to_plugin(
&instance.action.plugin,
&KeyEvent {
@@ -29,10 +36,40 @@ pub async fn key_down(device: &str, key: u8) -> Result<(), anyhow::Error> {
action: instance.action.uuid.clone(),
context: instance.context.clone(),
device: instance.context.device.clone(),
payload: GenericInstancePayload::new(&instance),
payload: GenericInstancePayload::new(instance, false),
},
)
.await?;
} else {
for instance in slot {
send_to_plugin(
&instance.action.plugin,
&KeyEvent {
event: "keyDown",
action: instance.action.uuid.clone(),
context: instance.context.clone(),
device: instance.context.device.clone(),
payload: GenericInstancePayload::new(instance, true),
},
)
.await?;
tokio::time::sleep(Duration::from_millis(100)).await;
send_to_plugin(
&instance.action.plugin,
&KeyEvent {
event: "keyUp",
action: instance.action.uuid.clone(),
context: instance.context.clone(),
device: instance.context.device.clone(),
payload: GenericInstancePayload::new(instance, true),
},
)
.await?;
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
Ok(())
@@ -40,30 +77,36 @@ pub async fn key_down(device: &str, key: u8) -> Result<(), anyhow::Error> {
pub async fn key_up(device: &str, key: u8) -> Result<(), anyhow::Error> {
let mut locks = lock_mutexes().await;
let slot = match get_slot(device, "Keypad", key, &mut locks).await? {
Some(slot) => slot,
None => return Ok(()),
let profile = locks.device_stores.get_device_store(device, crate::APP_HANDLE.get().unwrap())?.value.selected_profile.clone();
let context = Context {
device: device.to_owned(),
profile,
controller: "Keypad".to_owned(),
position: key,
};
for instance in slot {
instance.current_state = (instance.current_state + 1) % (instance.states.len() as u16);
let _ = crate::events::frontend::update_state(crate::APP_HANDLE.get().unwrap(), instance).await;
send_to_plugin(
&instance.action.plugin,
&KeyEvent {
event: "keyUp",
action: instance.action.uuid.clone(),
context: instance.context.clone(),
device: instance.context.device.clone(),
payload: GenericInstancePayload::new(instance),
},
)
.await?;
let slot = get_slot(&context, &mut locks).await?;
if slot.len() != 1 {
return Ok(());
}
let instance = &mut slot[0];
instance.current_state = (instance.current_state + 1) % (instance.states.len() as u16);
send_to_plugin(
&instance.action.plugin,
&KeyEvent {
event: "keyUp",
action: instance.action.uuid.clone(),
context: instance.context.clone(),
device: instance.context.device.clone(),
payload: GenericInstancePayload::new(instance, false),
},
)
.await?;
save_profile(device, &mut locks).await?;
let _ = crate::events::frontend::update_state(crate::APP_HANDLE.get().unwrap(), context, &mut locks).await;
Ok(())
}
+4 -1
View File
@@ -15,15 +15,17 @@ struct Coordinates {
}
#[derive(Serialize)]
#[allow(non_snake_case)]
struct GenericInstancePayload {
settings: serde_json::Value,
coordinates: Coordinates,
controller: String,
state: u16,
isInMultiAction: bool,
}
impl GenericInstancePayload {
fn new(instance: &crate::shared::ActionInstance) -> Self {
fn new(instance: &crate::shared::ActionInstance, multi_action: bool) -> Self {
let coordinates = match &instance.context.controller[..] {
"Encoder" => Coordinates {
row: 0,
@@ -40,6 +42,7 @@ impl GenericInstancePayload {
coordinates,
controller: instance.context.controller.clone(),
state: instance.current_state,
isInMultiAction: multi_action,
}
}
}
@@ -20,7 +20,7 @@ struct PropertyInspectorDidAppearEvent {
}
pub async fn send_to_property_inspector(context: ActionContext, message: serde_json::Value) -> Result<(), anyhow::Error> {
if let Some(instance) = crate::store::profiles::get_instance(&context.device, &context.controller, context.position, context.index, &mut lock_mutexes().await).await? {
if let Some(instance) = crate::store::profiles::get_instance(&context, &mut lock_mutexes().await).await? {
super::send_to_property_inspector(
&context,
&SendToEvent {
@@ -37,7 +37,7 @@ pub async fn send_to_property_inspector(context: ActionContext, message: serde_j
}
pub async fn send_to_plugin(context: ActionContext, message: serde_json::Value) -> Result<(), anyhow::Error> {
if let Some(instance) = crate::store::profiles::get_instance(&context.device, &context.controller, context.position, context.index, &mut lock_mutexes().await).await? {
if let Some(instance) = crate::store::profiles::get_instance(&context, &mut lock_mutexes().await).await? {
super::send_to_plugin(
&instance.action.plugin,
&SendToEvent {
@@ -54,7 +54,7 @@ pub async fn send_to_plugin(context: ActionContext, message: serde_json::Value)
}
pub async fn property_inspector_did_appear(context: ActionContext, event: &'static str) -> Result<(), anyhow::Error> {
if let Some(instance) = crate::store::profiles::get_instance(&context.device, &context.controller, context.position, context.index, &mut lock_mutexes().await).await? {
if let Some(instance) = crate::store::profiles::get_instance(&context, &mut lock_mutexes().await).await? {
if instance.action.property_inspector.is_empty() {
return Ok(());
}
+5 -5
View File
@@ -11,7 +11,7 @@ struct AppearEvent {
payload: GenericInstancePayload,
}
pub async fn will_appear(instance: &ActionInstance) -> Result<(), anyhow::Error> {
pub async fn will_appear(instance: &ActionInstance, multi_action: bool) -> Result<(), anyhow::Error> {
send_to_plugin(
&instance.action.plugin,
&AppearEvent {
@@ -19,7 +19,7 @@ pub async fn will_appear(instance: &ActionInstance) -> Result<(), anyhow::Error>
action: instance.action.uuid.clone(),
context: instance.context.clone(),
device: instance.context.device.clone(),
payload: GenericInstancePayload::new(instance),
payload: GenericInstancePayload::new(instance, multi_action),
},
)
.await?;
@@ -27,7 +27,7 @@ pub async fn will_appear(instance: &ActionInstance) -> Result<(), anyhow::Error>
Ok(())
}
pub async fn will_disappear(instance: &ActionInstance) -> Result<(), anyhow::Error> {
pub async fn will_disappear(instance: &ActionInstance, multi_action: bool) -> Result<(), anyhow::Error> {
send_to_plugin(
&instance.action.plugin,
&AppearEvent {
@@ -35,13 +35,13 @@ pub async fn will_disappear(instance: &ActionInstance) -> Result<(), anyhow::Err
action: instance.action.uuid.clone(),
context: instance.context.clone(),
device: instance.context.device.clone(),
payload: GenericInstancePayload::new(instance),
payload: GenericInstancePayload::new(instance, multi_action),
},
)
.await?;
if instance.context.device.starts_with("sd-") {
if let Err(error) = crate::devices::elgato::clear_image(&instance.context).await {
if let Err(error) = crate::devices::elgato::clear_image(&(&instance.context).into()).await {
log::warn!("Failed to clear device image at context {}: {}", instance.context, error);
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ async fn main() {
frontend::get_devices,
frontend::get_categories,
frontend::create_instance,
frontend::move_instance,
frontend::move_slot,
frontend::clear_slot,
frontend::get_profiles,
frontend::get_selected_profile,
+41 -3
View File
@@ -93,7 +93,16 @@ pub struct Action {
pub states: Vec<ActionState>,
}
/// Information about the slot an instance is located in.
/// Location metadata of a slot.
#[derive(Clone, Serialize, Deserialize)]
pub struct Context {
pub device: String,
pub profile: String,
pub controller: String,
pub position: u8,
}
/// Information about the slot and index an instance is located in.
#[derive(Clone, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
pub struct ActionContext {
pub device: String,
@@ -128,6 +137,35 @@ impl std::str::FromStr for ActionContext {
}
}
impl ActionContext {
pub fn from_context(context: Context, index: u16) -> Self {
Self {
device: context.device,
profile: context.profile,
controller: context.controller,
position: context.position,
index,
}
}
}
impl From<ActionContext> for Context {
fn from(value: ActionContext) -> Self {
Self {
device: value.device,
profile: value.profile,
controller: value.controller,
position: value.position,
}
}
}
impl From<&ActionContext> for Context {
fn from(value: &ActionContext) -> Self {
Self::from(value.clone())
}
}
/// An instance of an action.
#[derive(Clone, Serialize, Deserialize)]
pub struct ActionInstance {
@@ -141,8 +179,8 @@ pub struct ActionInstance {
#[derive(Clone, Serialize, Deserialize)]
pub struct Profile {
pub id: String,
pub keys: Vec<Option<Vec<ActionInstance>>>,
pub sliders: Vec<Option<Vec<ActionInstance>>>,
pub keys: Vec<Vec<ActionInstance>>,
pub sliders: Vec<Vec<ActionInstance>>,
}
/// A map of category names to a list of actions in that category.
+15 -16
View File
@@ -24,8 +24,8 @@ impl ProfileStores {
} else {
let default = Profile {
id: id.to_owned(),
keys: repeat_with(|| None).take((device.rows * device.columns).into()).collect(),
sliders: repeat_with(|| None).take(device.sliders.into()).collect(),
keys: repeat_with(Vec::new).take((device.rows * device.columns).into()).collect(),
sliders: repeat_with(Vec::new).take(device.sliders.into()).collect(),
};
let store = Store::new(&path, app.path_resolver().app_config_dir().unwrap(), default).with_context(|| format!("Failed to create store for profile {}", path))?;
@@ -47,7 +47,7 @@ impl ProfileStores {
pub fn all_from_plugin(&self, plugin: &str) -> Vec<crate::shared::ActionContext> {
let mut all = vec![];
for store in self.stores.values() {
for slot in store.value.keys.iter().chain(&store.value.sliders).flatten() {
for slot in store.value.keys.iter().chain(&store.value.sliders) {
for instance in slot {
if instance.action.plugin == plugin {
all.push(instance.context.clone());
@@ -132,26 +132,25 @@ pub async fn lock_mutexes() -> Locks<'static> {
}
}
pub async fn get_slot<'a>(device: &str, controller: &str, position: u8, locks: &'a mut Locks<'_>) -> Result<Option<&'a mut Vec<crate::shared::ActionInstance>>, anyhow::Error> {
let selected_profile = &locks.device_stores.get_device_store(device, crate::APP_HANDLE.get().unwrap())?.value.selected_profile;
let device = locks.devices.get(device).unwrap();
let store = locks.profile_stores.get_profile_store(device, selected_profile, crate::APP_HANDLE.get().unwrap())?;
pub async fn get_slot<'a>(context: &crate::shared::Context, locks: &'a mut Locks<'_>) -> Result<&'a mut Vec<crate::shared::ActionInstance>, anyhow::Error> {
let device = locks.devices.get(&context.device).unwrap();
let store = locks.profile_stores.get_profile_store(device, &context.profile, crate::APP_HANDLE.get().unwrap())?;
let profile = &mut store.value;
let configured = match controller {
"Encoder" => profile.sliders[position as usize].as_mut(),
_ => profile.keys[position as usize].as_mut(),
let configured = match &context.controller[..] {
"Encoder" => &mut profile.sliders[context.position as usize],
_ => &mut profile.keys[context.position as usize],
};
Ok(configured)
}
pub async fn get_instance<'a>(device: &str, controller: &str, position: u8, index: u16, locks: &'a mut Locks<'_>) -> Result<Option<&'a mut crate::shared::ActionInstance>, anyhow::Error> {
let slot = get_slot(device, controller, position, locks).await?;
match slot {
Some(slot) => Ok(Some(&mut slot[index as usize])),
None => Ok(None),
pub async fn get_instance<'a>(context: &crate::shared::ActionContext, locks: &'a mut Locks<'_>) -> Result<Option<&'a mut crate::shared::ActionInstance>, anyhow::Error> {
let slot = get_slot(&(context.into()), locks).await?;
if (context.index as usize) < slot.len() {
Ok(Some(&mut slot[context.index as usize]))
} else {
Ok(None)
}
}
+24 -20
View File
@@ -19,18 +19,21 @@
}
async function handleDrop({ dataTransfer }: DragEvent, controller: string, position: number) {
let context = `${device.id}.${profile.id}.${controller}.${position}.0`;
let context = { device: device.id, profile: profile.id, controller, position };
let array = controller == "Encoder" ? profile.sliders : profile.keys;
if (dataTransfer?.getData("action")) {
array[position] = [await invoke("create_instance", { context, action: JSON.parse(dataTransfer?.getData("action")) })];
array[position] = await invoke("create_instance", { context, action: JSON.parse(dataTransfer?.getData("action")) });
profile = profile;
} else if (dataTransfer?.getData("controller")) {
let oldArray = dataTransfer?.getData("controller") == "Encoder" ? profile.sliders : profile.keys;
let oldPosition = parseInt(dataTransfer?.getData("position"));
let response: ActionInstance = await invoke("move_instance", { context, instance: (oldArray[oldPosition] ?? [null])[0] });
let response: ActionInstance[] = await invoke("move_slot", {
source: { device: device.id, profile: profile.id, controller: dataTransfer?.getData("controller"), position: oldPosition },
destination: context
});
if (response) {
array[position] = [response];
oldArray[oldPosition] = null;
array[position] = response;
oldArray[oldPosition] = [];
profile = profile;
}
}
@@ -125,14 +128,13 @@
}
});
const nonNull = <T>(o: T | null): o is T => o != null;
$: nonEmptySlots = profile.keys.filter(nonNull).concat(profile.sliders.filter(nonNull));
$: nonEmptySlots = profile.keys.filter((v) => v.length > 0).concat(profile.sliders.filter((v) => v.length > 0));
</script>
<div class="flex flex-row">
{#each { length: device.sliders } as _, i}
<Slider
context="{device.id}.{profile.id}.Encoder.{i}.0"
context={{ device: device.id, profile: profile.id, controller: "Encoder", position: i }}
bind:slot={profile.sliders[i]}
on:dragover={handleDragOver}
on:drop={(event) => handleDrop(event, "Encoder", i)}
@@ -145,7 +147,7 @@
<div class="flex flex-row">
{#each { length: device.columns } as _, c}
<Key
context="{device.id}.{profile.id}.Keypad.{(r * device.columns) + c}.0"
context={{ device: device.id, profile: profile.id, controller: "Keypad", position: (r * device.columns) + c }}
bind:slot={profile.keys[(r * device.columns) + c]}
on:dragover={handleDragOver}
on:drop={(event) => handleDrop(event, "Keypad", (r * device.columns) + c)}
@@ -166,16 +168,18 @@
✕
</button>
{#each nonEmptySlots as slot (slot[0].context)}
{#if slot[0].action.property_inspector}
<iframe
title="Property inspector"
class="w-full h-full hidden"
class:!block={$inspectedInstance == slot[0].context}
src={"http://localhost:57118" + slot[0].action.property_inspector + "|opendeck_property_inspector"}
name={slot[0].context}
bind:this={iframes[slot[0].context]}
on:load={() => iframeOnLoad(slot[0])}
/>
{/if}
{#each slot as instance}
{#if instance.action.property_inspector}
<iframe
title="Property inspector"
class="w-full h-full hidden"
class:!block={$inspectedInstance == instance.context}
src={"http://localhost:57118" + instance.action.property_inspector + "|opendeck_property_inspector"}
name={instance.context}
bind:this={iframes[instance.context]}
on:load={() => iframeOnLoad(instance)}
/>
{/if}
{/each}
{/each}
</div>
+42 -16
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import type { ActionInstance } from "$lib/ActionInstance";
import type { ActionState } from "$lib/ActionState";
import type { Context } from "$lib/Context";
import { inspectedInstance } from "$lib/propertyInspector";
import { getImage, renderImage } from "$lib/rendererHelper";
@@ -7,33 +9,51 @@
import { invoke } from "@tauri-apps/api";
import { listen } from "@tauri-apps/api/event";
export let context: string;
export let slot: ActionInstance[] | null;
export let context: Context;
export let slot: ActionInstance[];
$: instance = (slot ?? [null])[0];
$: state = instance?.states[instance?.current_state];
let state: ActionState | undefined;
$: {
if (!slot.length) {
state = undefined;
} else if (slot.length > 1) {
// @ts-expect-error
state = {
image: "/multi-action.png",
name: "Multi Action",
show: false
};
} else {
state = slot[0].states[slot[0].current_state];
}
}
listen("update_state", ({ payload }: { payload: ActionInstance }) => {
if (payload.context == context) slot = [payload];
listen("update_state", ({ payload }: { payload: { context: Context, contents: ActionInstance[] }}) => {
if (JSON.stringify(payload.context) == JSON.stringify(context)) slot = payload.contents;
});
function select() {
inspectedInstance.set(context);
if (!slot || slot.length == 0) return;
if (slot.length > 1) {
// TODO
} else {
inspectedInstance.set(slot[0].context);
}
}
async function clear(event: MouseEvent) {
event.preventDefault();
if (event.ctrlKey) return;
await invoke("clear_slot", { context });
instance = null;
if ($inspectedInstance == context) inspectedInstance.set(null);
slot.forEach((instance) => { if ($inspectedInstance == instance.context) inspectedInstance.set(null); });
slot = [];
}
let showAlert = 0;
let showOk = 0;
let timeouts: number[] = [];
listen("show_alert", ({ payload }: { payload: string }) => {
if (payload != context) return;
if (slot.length != 1 || payload != slot[0].context) return;
timeouts.forEach(clearTimeout);
showOk = 0;
showAlert = 1;
@@ -41,7 +61,7 @@
timeouts.push(setTimeout(() => showAlert = 0, 2e3));
});
listen("show_ok", ({ payload }: { payload: string }) => {
if (payload != context) return;
if (slot.length != 1 || payload != slot[0].context) return;
timeouts.forEach(clearTimeout);
showAlert = 0;
showOk = 1;
@@ -51,9 +71,15 @@
let image: string;
$: {
let fallback = instance?.action.states[instance?.current_state].image ?? instance?.action.icon;
image = getImage(state?.image, fallback);
if (state) renderImage(context, state, fallback, showOk > 0, showAlert > 0);
if (slot.length > 1) {
image = state?.image!;
renderImage(context, state!, null!, false, false, false);
} else if (slot.length) {
let instance = slot[0];
let fallback = instance.action.states[instance.current_state].image ?? instance.action.icon;
image = getImage(state?.image, fallback);
if (state) renderImage(context, state, fallback, showOk > 0, showAlert > 0);
}
}
</script>
@@ -63,12 +89,12 @@
draggable on:dragstart
role="cell" tabindex="-1"
>
{#if instance && state}
{#if state}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<img
src={image}
class="p-2 w-full rounded-xl"
alt={instance.action.tooltip}
alt={slot.length == 1 ? slot[0].action.tooltip : "Multi Action"}
on:click={select} on:keyup={select}
on:contextmenu={clear}
/>
+18 -14
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import type { ActionInstance } from "$lib/ActionInstance";
import type { Context } from "$lib/Context";
import { inspectedInstance } from "$lib/propertyInspector";
import { getImage } from "$lib/rendererHelper";
@@ -7,33 +8,32 @@
import { invoke } from "@tauri-apps/api";
import { listen } from "@tauri-apps/api/event";
export let context: string;
export let slot: ActionInstance[] | null;
export let context: Context;
export let slot: ActionInstance[];
$: instance = (slot ?? [null])[0];
$: state = instance?.states[instance?.current_state];
$: state = slot[0]?.states[slot[0]?.current_state];
listen("update_state", ({ payload }: { payload: ActionInstance }) => {
if (payload.context == context) slot = [payload];
listen("update_state", ({ payload }: { payload: { context: Context, contents: ActionInstance[] }}) => {
if (JSON.stringify(payload.context) == JSON.stringify(context)) slot = payload.contents;
});
function select() {
inspectedInstance.set(context);
inspectedInstance.set(`${context.device}.${context.profile}.${context.controller}.${context.position}.0`);
}
async function clear(event: MouseEvent) {
event.preventDefault();
if (event.ctrlKey) return;
await invoke("clear_slot", { context });
instance = null;
if ($inspectedInstance == context) inspectedInstance.set(null);
slot.forEach((instance) => { if ($inspectedInstance == instance.context) inspectedInstance.set(null); });
slot = [];
}
let showAlert = 0;
let showOk = 0;
let timeouts: number[] = [];
listen("show_alert", ({ payload }: { payload: string }) => {
if (payload != context) return;
if (slot.length != 1 || payload != slot[0].context) return;
timeouts.forEach(clearTimeout);
showOk = 0;
showAlert = 1;
@@ -41,7 +41,7 @@
timeouts.push(setTimeout(() => showAlert = 0, 2e3));
});
listen("show_ok", ({ payload }: { payload: string }) => {
if (payload != context) return;
if (slot.length != 1 || payload != slot[0].context) return;
timeouts.forEach(clearTimeout);
showAlert = 0;
showOk = 1;
@@ -50,7 +50,11 @@
});
let image: string;
$: image = getImage(state?.image, instance?.action.states[instance?.current_state].image ?? instance?.action.icon);
$: {
if (slot.length) {
image = getImage(state?.image, slot[0].action.states[slot[0].current_state].image ?? slot[0].action.icon);
}
}
</script>
<div
@@ -59,12 +63,12 @@
draggable on:dragstart
role="cell" tabindex="-1"
>
{#if instance && state}
{#if state}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<img
src={image}
class="p-2 w-full rounded-xl"
alt={instance.action.tooltip}
alt={slot.length == 1 ? slot[0].action.tooltip : ""}
on:click={select} on:keyup={select}
on:contextmenu={clear}
/>
+6
View File
@@ -0,0 +1,6 @@
export type Context = {
device: string,
profile: string,
controller: string,
position: number
};
+2 -2
View File
@@ -3,6 +3,6 @@ import type { ActionInstance } from "./ActionInstance";
export type Profile = {
device: string,
id: string,
keys: (ActionInstance[] | null)[],
sliders: (ActionInstance[] | null)[]
keys: ActionInstance[][],
sliders: ActionInstance[][]
};
+4 -3
View File
@@ -1,6 +1,7 @@
import { invoke } from "@tauri-apps/api";
import type { ActionState } from "./ActionState";
import type { Context } from "./Context";
export function getImage(image: string | undefined, fallback: string | undefined): string {
if (!image) return fallback ? getImage(fallback, undefined) : "/alert.png";
@@ -18,7 +19,7 @@ export function getImage(image: string | undefined, fallback: string | undefined
return image;
}
export async function renderImage(actionContext: string, state: ActionState, fallback: string | undefined, showOk: boolean, showAlert: boolean) {
export async function renderImage(slotContext: Context, state: ActionState, fallback: string | undefined, showOk: boolean, showAlert: boolean, processImage: boolean = true) {
// Create canvas
let canvas = document.createElement("canvas");
canvas.width = 144;
@@ -29,7 +30,7 @@ export async function renderImage(actionContext: string, state: ActionState, fal
// Load image
let image = document.createElement("img");
image.crossOrigin = "anonymous";
image.src = getImage(state.image, fallback);
image.src = processImage ? getImage(state.image, fallback) : state.image;
if (image.src == undefined) return;
await new Promise((resolve) => {
image.onload = resolve;
@@ -76,6 +77,6 @@ export async function renderImage(actionContext: string, state: ActionState, fal
context.drawImage(alertImage, 0, 0, canvas.width, canvas.height);
}
await invoke("update_image", { context: actionContext, image: canvas.toDataURL("image/jpeg") });
await invoke("update_image", { context: slotContext, image: canvas.toDataURL("image/jpeg") });
canvas.remove();
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB