mirror of
https://github.com/nekename/OpenDeck.git
synced 2026-10-28 18:02:23 +00:00
Implement assigning actions to slots and persisting profiles
This commit is contained in:
1 parent
c77d00c545
commit
c30c00c34e
21 files changed
+452
-54
No files matched your search
@@ -14,7 +14,7 @@ tauri-build = { version = "1.5.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Large, fundamental libraries
|
||||
tauri = { version = "1.5.2", features = [ "system-tray", "api-all" ] }
|
||||
tauri = { version = "1.5.2", features = [ "system-tray", "api-all", "devtools" ] }
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
serde_json = "1.0"
|
||||
serialport = "4.2.2"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
mod prontokey;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Mutex;
|
||||
|
||||
@@ -48,7 +50,7 @@ impl DeviceInfo {
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DEVICES: Mutex<Vec<DeviceInfo>> = Mutex::new(vec![]);
|
||||
pub static ref DEVICES: Mutex<HashMap<String, DeviceInfo>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
/// Attempt to initialise all connected devices.
|
||||
|
||||
@@ -76,7 +76,7 @@ impl ProntoKeyDevice {
|
||||
if let Value::String(address) = &j["address"] {
|
||||
device = Some(ProntoKeyDevice { address: address.clone() });
|
||||
if let Some(device) = &device {
|
||||
super::DEVICES.lock().unwrap().push(super::DeviceInfo::new(device));
|
||||
super::DEVICES.lock().unwrap().insert(device.id(), super::DeviceInfo::new(device));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
use crate::store::profiles::{PROFILE_STORES, DEVICE_STORES, get_device_profiles};
|
||||
use crate::devices::DEVICES;
|
||||
use crate::shared::{Action, ActionContext, ActionInstance, CATEGORIES};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct Error {
|
||||
pub description: String
|
||||
}
|
||||
|
||||
fn serialise_mutex_hashmap<T>(map: &std::sync::Mutex<HashMap<String, T>>) -> String where T: serde::Serialize {
|
||||
// Here, we "duplicate" the HashMap so it isn't captured in a MutexGuard, allowing it to be serialised
|
||||
let mut hash_map: HashMap<String, &T> = HashMap::new();
|
||||
let locked = map.lock().unwrap();
|
||||
|
||||
for key in locked.keys() {
|
||||
hash_map.insert(key.to_owned(), locked.get(key).unwrap());
|
||||
}
|
||||
serde_json::to_string(&hash_map).unwrap()
|
||||
}
|
||||
|
||||
// Strings are returned from many of these commands as their return values often reference static Mutexes.
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_devices() -> String {
|
||||
serialise_mutex_hashmap(&*DEVICES)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_categories() -> String {
|
||||
serialise_mutex_hashmap(&*CATEGORIES)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_profiles(app: tauri::AppHandle, device: &str) -> String {
|
||||
match get_device_profiles(device, &app) {
|
||||
Ok(profiles) => serde_json::to_string(&profiles).unwrap(),
|
||||
Err(error) => serde_json::to_string(&Error { description: error.to_string() }).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_selected_profile(app: tauri::AppHandle, device: &str) -> String {
|
||||
match DEVICE_STORES.lock().unwrap().get_device_store(device, &app) {
|
||||
Ok(store) => {
|
||||
match PROFILE_STORES.lock().unwrap().get_profile_store(
|
||||
DEVICES.lock().unwrap().get(device).unwrap(),
|
||||
&store.value.selected_profile,
|
||||
&app
|
||||
) {
|
||||
Ok(store) => serde_json::to_string(&store.value).unwrap(),
|
||||
Err(error) => serde_json::to_string(&Error { description: error.to_string() }).unwrap()
|
||||
}
|
||||
},
|
||||
Err(error) => serde_json::to_string(&Error { description: error.to_string() }).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_selected_profile(app: tauri::AppHandle, device: &str, profile: &str) {
|
||||
let mut device_stores = DEVICE_STORES.lock().unwrap();
|
||||
let store = device_stores.get_device_store(device, &app).unwrap();
|
||||
store.value.selected_profile = profile.to_owned();
|
||||
let _ = store.save();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_instance(app: tauri::AppHandle, action: Action, context: ActionContext) -> String {
|
||||
let instance = ActionInstance {
|
||||
action: action.clone(),
|
||||
context: context.clone(),
|
||||
states: action.states.clone(),
|
||||
current_state: 0,
|
||||
settings: serde_json::Value::Null
|
||||
};
|
||||
|
||||
let mut profile_stores = PROFILE_STORES.lock().unwrap();
|
||||
let store = match profile_stores.get_profile_store(
|
||||
DEVICES.lock().unwrap().get(&context.device).unwrap(),
|
||||
&context.profile,
|
||||
&app
|
||||
) {
|
||||
Ok(store) => store,
|
||||
Err(error) => return serde_json::to_string(&Error { description: error.to_string() }).unwrap()
|
||||
};
|
||||
|
||||
store.value.keys[context.position as usize] = Some(instance);
|
||||
if let Err(error) = store.save() {
|
||||
return serde_json::to_string(&Error { description: error.to_string() }).unwrap();
|
||||
}
|
||||
|
||||
serde_json::to_string(&store.value.keys[context.position as usize]).unwrap()
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod inbound;
|
||||
pub mod frontend;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
+12
-20
@@ -1,39 +1,31 @@
|
||||
// Prevents additional console window on Windows in release.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod shared;
|
||||
mod store;
|
||||
mod events;
|
||||
mod plugins;
|
||||
mod devices;
|
||||
|
||||
#[tauri::command]
|
||||
fn get_devices() -> Vec<devices::DeviceInfo> {
|
||||
devices::DEVICES.lock().unwrap().to_vec()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_categories() -> String {
|
||||
// Here, we "duplicate" the HashMap so it isn't captured in a MutexGuard, allowing it to be serialised
|
||||
let mut hash_map: HashMap<String, &Vec<shared::Action>> = HashMap::new();
|
||||
let categories = shared::CATEGORIES.lock().unwrap();
|
||||
for key in categories.keys() {
|
||||
hash_map.insert(key.to_string(), categories.get(key).unwrap());
|
||||
}
|
||||
serde_json::to_string(&hash_map).unwrap()
|
||||
}
|
||||
use events::frontend;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = match
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![get_devices, get_categories])
|
||||
let app = match tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
frontend::get_devices,
|
||||
frontend::get_categories,
|
||||
frontend::create_instance,
|
||||
frontend::get_profiles,
|
||||
frontend::get_selected_profile,
|
||||
frontend::set_selected_profile
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
{
|
||||
Ok(app) => app,
|
||||
Err(error) => panic!("Failed to create Tauri application: {}", error)
|
||||
};
|
||||
|
||||
devices::initialise_devices();
|
||||
plugins::initialise_plugins(app.handle());
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ fn initialise_plugin(path: path::PathBuf, app: &AppHandle, categories: &mut Hash
|
||||
.with_context(|| { format!("Failed to parse manifest of plugin at {}", manifest_path.display()) })?;
|
||||
|
||||
for action in &mut manifest.actions {
|
||||
action.plugin = plugin_uuid.to_string();
|
||||
action.plugin = plugin_uuid.to_owned();
|
||||
|
||||
let mut action_icon_path = path.clone();
|
||||
action_icon_path.push(action.icon.clone());
|
||||
action.icon = convert_icon(action_icon_path.to_str().unwrap().to_string());
|
||||
action.icon = convert_icon(action_icon_path.to_str().unwrap().to_owned());
|
||||
|
||||
for state in &mut action.states {
|
||||
if state.image == "actionDefaultImage" {
|
||||
@@ -40,7 +40,7 @@ fn initialise_plugin(path: path::PathBuf, app: &AppHandle, categories: &mut Hash
|
||||
} else {
|
||||
let mut state_icon = path.clone();
|
||||
state_icon.push(state.image.clone());
|
||||
state.image = convert_icon(state_icon.to_str().unwrap().to_string());
|
||||
state.image = convert_icon(state_icon.to_str().unwrap().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ fn initialise_plugin(path: path::PathBuf, app: &AppHandle, categories: &mut Hash
|
||||
}
|
||||
|
||||
let mut devices: Vec<info_param::DeviceInfo> = vec![];
|
||||
for device in crate::devices::DEVICES.lock().unwrap().iter() {
|
||||
for device in crate::devices::DEVICES.lock().unwrap().values() {
|
||||
devices.push(info_param::DeviceInfo::new(device));
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ fn initialise_plugin(path: path::PathBuf, app: &AppHandle, categories: &mut Hash
|
||||
version: env!("CARGO_PKG_VERSION").to_owned()
|
||||
},
|
||||
plugin: info_param::PluginInfo {
|
||||
uuid: plugin_uuid.to_string(),
|
||||
uuid: plugin_uuid.to_owned(),
|
||||
version: manifest.version
|
||||
},
|
||||
devicePixelRatio: 0,
|
||||
@@ -155,7 +155,7 @@ fn initialise_plugin(path: path::PathBuf, app: &AppHandle, categories: &mut Hash
|
||||
.args([
|
||||
code_path,
|
||||
String::from("-port"), 57116.to_string(),
|
||||
String::from("-pluginUUID"), plugin_uuid.to_string(),
|
||||
String::from("-pluginUUID"), plugin_uuid.to_owned(),
|
||||
String::from("-registerEvent"), String::from("register"),
|
||||
String::from("-info"), serde_json::to_string(&info).unwrap()
|
||||
])
|
||||
@@ -169,7 +169,7 @@ fn initialise_plugin(path: path::PathBuf, app: &AppHandle, categories: &mut Hash
|
||||
.current_dir(&path)
|
||||
.args([
|
||||
String::from("-port"), 57116.to_string(),
|
||||
String::from("-pluginUUID"), plugin_uuid.to_string(),
|
||||
String::from("-pluginUUID"), plugin_uuid.to_owned(),
|
||||
String::from("-registerEvent"), String::from("register"),
|
||||
String::from("-info"), serde_json::to_string(&info).unwrap()
|
||||
])
|
||||
|
||||
+12
-4
@@ -20,7 +20,7 @@ pub fn convert_icon(path: String) -> String {
|
||||
|
||||
/// A state of an action.
|
||||
#[serde_inline_default]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ActionState {
|
||||
#[serde(alias = "Image")]
|
||||
pub image: String,
|
||||
@@ -60,7 +60,7 @@ pub struct ActionState {
|
||||
|
||||
/// An action, deserialised from the plugin manifest.
|
||||
#[serde_inline_default]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Action {
|
||||
#[serde(alias = "Name")]
|
||||
pub name: String,
|
||||
@@ -94,7 +94,7 @@ pub struct Action {
|
||||
}
|
||||
|
||||
/// Information about the slot an instance is located in.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ActionContext {
|
||||
pub device: String,
|
||||
pub profile: String,
|
||||
@@ -116,7 +116,15 @@ pub struct ActionInstance {
|
||||
pub context: ActionContext,
|
||||
pub states: Vec<ActionState>,
|
||||
pub current_state: u16,
|
||||
pub settings: String
|
||||
pub settings: serde_json::Value
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Profile {
|
||||
pub device: String,
|
||||
pub id: String,
|
||||
pub keys: Vec<Option<ActionInstance>>,
|
||||
pub sliders: Vec<Option<ActionInstance>>
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
pub mod profiles;
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
/// Allows for easy persistence of values using JSON files.
|
||||
pub struct Store<T> where T: Serialize + for<'a> Deserialize<'a> {
|
||||
pub id: String,
|
||||
pub value: T,
|
||||
path: PathBuf
|
||||
}
|
||||
|
||||
impl<T> Store<T> where T: Serialize + for<'a> Deserialize<'a> {
|
||||
/// Create a new Store given an ID and storage directory.
|
||||
pub fn new(id: &str, config_dir: PathBuf, default: T) -> Result<Self, anyhow::Error> {
|
||||
let mut path = config_dir.clone();
|
||||
path.push(format!("{}.json", id));
|
||||
|
||||
if path.exists() {
|
||||
let file_contents = fs::read(&path)
|
||||
.with_context(|| { format!("Failed to read contents of file at {}", &path.display()) })?;
|
||||
let existing_value: T = serde_json::from_slice(&file_contents)
|
||||
.with_context(|| { format!("Failed to parse config file at {}", &path.display()) })?;
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
id: String::from(id),
|
||||
value: existing_value
|
||||
})
|
||||
} else {
|
||||
Ok(Self {
|
||||
path,
|
||||
id: String::from(id),
|
||||
value: default
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Save the relevant Store as a file.
|
||||
pub fn save(&self) -> anyhow::Result<(), anyhow::Error> {
|
||||
fs::create_dir_all(self.path.parent().unwrap())
|
||||
.with_context(|| { format!("Failed to create directories at {}", self.path.display()) })?;
|
||||
fs::write(&self.path, serde_json::to_string_pretty(&self.value).unwrap())
|
||||
.with_context(|| { format!("Failed to write to file at {}", self.path.display()) })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use super::Store;
|
||||
use crate::shared::Profile;
|
||||
|
||||
use std::fs;
|
||||
use std::sync::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::repeat_with;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use anyhow::Context;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
pub struct ProfileStores {
|
||||
stores: HashMap<String, Store<Profile>>
|
||||
}
|
||||
|
||||
impl ProfileStores {
|
||||
pub fn get_profile_store(&mut self, device: &crate::devices::DeviceInfo, id: &str, app: &tauri::AppHandle) -> Result<&mut Store<Profile>, anyhow::Error> {
|
||||
let path = format!("profiles/{}/{}", device.id, id);
|
||||
|
||||
if self.stores.contains_key(&path) {
|
||||
Ok(self.stores.get_mut(&path).unwrap())
|
||||
} else {
|
||||
let default = Profile {
|
||||
device: device.id.clone(),
|
||||
id: id.to_owned(),
|
||||
keys: repeat_with(|| None).take((device.rows * device.columns).into()).collect(),
|
||||
sliders: repeat_with(|| None).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) })?;
|
||||
|
||||
store.save().with_context(|| { format!("Failed to save store for profile {}", path) })?;
|
||||
|
||||
self.stores.insert(path.clone(), store);
|
||||
Ok(self.stores.get_mut(&path).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DeviceConfig {
|
||||
pub selected_profile: String
|
||||
}
|
||||
|
||||
pub struct DeviceStores {
|
||||
stores: HashMap<String, Store<DeviceConfig>>
|
||||
}
|
||||
|
||||
impl DeviceStores {
|
||||
pub fn get_device_store(&mut self, device: &str, app: &tauri::AppHandle) -> Result<&mut Store<DeviceConfig>, anyhow::Error> {
|
||||
if self.stores.contains_key(device) {
|
||||
Ok(self.stores.get_mut(device).unwrap())
|
||||
} else {
|
||||
let default = DeviceConfig {
|
||||
selected_profile: String::from("Default")
|
||||
};
|
||||
|
||||
let store =
|
||||
Store::new(&format!("profiles/{}", device), app.path_resolver().app_config_dir().unwrap(), default)
|
||||
.with_context(|| { format!("Failed to create config store for device {}", device) })?;
|
||||
|
||||
store.save().with_context(|| { format!("Failed to save config store for device {}", device) })?;
|
||||
|
||||
self.stores.insert(device.to_owned(), store);
|
||||
Ok(self.stores.get_mut(device).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_device_profiles(device: &str, app: &tauri::AppHandle) -> Result<Vec<String>, anyhow::Error> {
|
||||
let mut profiles: Vec<String> = vec![];
|
||||
|
||||
let device_path = app.path_resolver().app_config_dir().unwrap().join(format!("profiles/{}", device));
|
||||
fs::create_dir_all(&device_path).with_context(|| { format!("Failed to create directories for device {}", device) })?;
|
||||
|
||||
let entries =
|
||||
fs::read_dir(device_path)
|
||||
.with_context(|| { format!("Failed to read directory for device {}", device) })?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
if entry.metadata().unwrap().is_file() && entry.file_name() != "config.json" {
|
||||
profiles.push(entry.file_name().to_string_lossy()[..entry.file_name().len() - 5].to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if profiles.is_empty() {
|
||||
profiles.push("Default".to_owned());
|
||||
}
|
||||
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// A singleton object to contain all active Store instances that hold a profile.
|
||||
pub static ref PROFILE_STORES: Mutex<ProfileStores> = Mutex::new(ProfileStores { stores: HashMap::new() });
|
||||
|
||||
/// A singleton object to manage Store instances for device configurations.
|
||||
pub static ref DEVICE_STORES: Mutex<DeviceStores> = Mutex::new(DeviceStores { stores: HashMap::new() });
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
]
|
||||
},
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' data: https://appstore.elgato.com/; connect-src https://appstore.elgato.com/;"
|
||||
"csp": "default-src 'self'; img-src 'self' data: asset: https://appstore.elgato.com/; connect-src https://appstore.elgato.com/;"
|
||||
},
|
||||
"allowlist": {
|
||||
"all": true,
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
|
||||
let devices: DeviceInfo[] = [];
|
||||
let devices: { [id: string]: DeviceInfo } = {};
|
||||
|
||||
async function refreshDevices() {
|
||||
devices = await invoke("get_devices");
|
||||
devices = JSON.parse(await invoke("get_devices"));
|
||||
}
|
||||
|
||||
let value: string;
|
||||
export let device;
|
||||
function change() {
|
||||
if (value == "refresh_devices") {
|
||||
refreshDevices();
|
||||
value = "placeholder";
|
||||
} else {
|
||||
dispatch("select", devices[parseInt(value)]);
|
||||
device = devices[value];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -24,8 +22,8 @@
|
||||
<select bind:value on:change={change} class="w-full">
|
||||
<option value="placeholder" disabled selected> Choose a device... </option>
|
||||
|
||||
{#each devices as device, index}
|
||||
<option value={index}> {device.name} </option>
|
||||
{#each Object.entries(devices) as [ id, device ]}
|
||||
<option value={id}> {device.name} </option>
|
||||
{/each}
|
||||
|
||||
<option value="refresh_devices"> Refresh devices </option>
|
||||
|
||||
@@ -3,18 +3,37 @@
|
||||
import Slider from "./Slider.svelte";
|
||||
|
||||
export let device: DeviceInfo;
|
||||
export let profile: Profile;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row">
|
||||
{#each { length: device.sliders } as _}
|
||||
<Slider />
|
||||
{#each { length: device.sliders } as _, i}
|
||||
<Slider
|
||||
context={{
|
||||
device: device.id,
|
||||
profile: profile.id,
|
||||
controller: "Encoder",
|
||||
position: i,
|
||||
index: 0
|
||||
}}
|
||||
instance={profile.sliders[i]}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#each { length: device.rows } as _}
|
||||
{#each { length: device.rows } as _, r}
|
||||
<div class="flex flex-row">
|
||||
{#each { length: device.columns } as _}
|
||||
<Key />
|
||||
{#each { length: device.columns } as _, c}
|
||||
<Key
|
||||
context={{
|
||||
device: device.id,
|
||||
profile: profile.id,
|
||||
controller: "Keypad",
|
||||
position: (r * device.columns) + c,
|
||||
index: 0
|
||||
}}
|
||||
instance={profile.keys[(r * device.columns) + c]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1 +1,33 @@
|
||||
<div class="m-2 w-32 h-32 border-2 rounded-md"></div>
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||
|
||||
export let context: ActionContext;
|
||||
export let instance: ActionInstance | null;
|
||||
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleDrop({ dataTransfer }: DragEvent) {
|
||||
let action = dataTransfer?.getData("action");
|
||||
if (!action) return;
|
||||
instance = JSON.parse(await invoke("create_instance", { context, action: JSON.parse(action) }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="m-2 w-32 h-32 border-2 rounded-md"
|
||||
on:dragover={handleDragOver}
|
||||
on:drop={handleDrop}
|
||||
role="cell" tabindex="-1"
|
||||
>
|
||||
{#if instance}
|
||||
<img
|
||||
src={convertFileSrc(instance.states[instance.current_state].image)}
|
||||
class="p-2 w-full rounded-xl"
|
||||
alt={instance.action.tooltip}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,9 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||
export let action: Action;
|
||||
|
||||
function handleDragStart(event: DragEvent) {
|
||||
event.dataTransfer?.setData("action", JSON.stringify(action));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row items-center mt-2 mb-2 space-x-2">
|
||||
<img src={convertFileSrc(action.icon)} alt={action.tooltip} class="w-12 h-12 rounded-sm" />
|
||||
<img
|
||||
src={convertFileSrc(action.icon)}
|
||||
alt={action.tooltip}
|
||||
class="w-12 h-12 rounded-sm"
|
||||
draggable
|
||||
on:dragstart={handleDragStart}
|
||||
/>
|
||||
<span> {action.name} </span>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
|
||||
let profiles: string[] = [];
|
||||
async function getProfiles(device: DeviceInfo) {
|
||||
profiles = JSON.parse(await invoke("get_profiles", { device: device.id }));
|
||||
value = profiles[0];
|
||||
}
|
||||
|
||||
export let profile: Profile;
|
||||
async function setProfile(id: string) {
|
||||
if (!device || !id) return;
|
||||
await invoke("set_selected_profile", { device: device.id, profile: id });
|
||||
profile = JSON.parse(await invoke("get_selected_profile", { device: device.id }));
|
||||
}
|
||||
|
||||
let value: string;
|
||||
$: setProfile(value);
|
||||
|
||||
export let device: DeviceInfo;
|
||||
$: getProfiles(device);
|
||||
</script>
|
||||
|
||||
<select bind:value class="mt-1 w-full">
|
||||
{#each profiles as profile}
|
||||
<option value={profile}> {profile} </option>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -1 +1,33 @@
|
||||
<div class="m-2 w-20 h-144 border-2 rounded-md"></div>
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||
|
||||
export let context: ActionContext;
|
||||
export let instance: ActionInstance | null;
|
||||
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleDrop({ dataTransfer }: DragEvent) {
|
||||
let action = dataTransfer?.getData("action");
|
||||
if (!action) return;
|
||||
instance = JSON.parse(await invoke("create_instance", { context, action: JSON.parse(action) }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center m-2 w-20 h-144 border-2 rounded-md"
|
||||
on:dragover={handleDragOver}
|
||||
on:drop={handleDrop}
|
||||
role="cell" tabindex="-1"
|
||||
>
|
||||
{#if instance}
|
||||
<img
|
||||
src={convertFileSrc(instance.states[instance.current_state].image)}
|
||||
class="p-2 w-full rounded-xl"
|
||||
alt={instance.action.tooltip}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
type ActionContext = {
|
||||
device: string,
|
||||
profile: string,
|
||||
controller: string,
|
||||
position: number,
|
||||
index: number
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
type ActionInstance = {
|
||||
action: Action,
|
||||
context: ActionContext,
|
||||
states: ActionState[],
|
||||
current_state: number,
|
||||
settings: any
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
type Profile = {
|
||||
device: string,
|
||||
id: string,
|
||||
keys: (ActionInstance | null)[],
|
||||
sliders: (ActionInstance | null)[]
|
||||
};
|
||||
+11
-3
@@ -2,17 +2,25 @@
|
||||
import ActionList from "../components/ActionList.svelte";
|
||||
import DeviceSelector from "../components/DeviceSelector.svelte";
|
||||
import DeviceView from "../components/DeviceView.svelte";
|
||||
import ProfileSelector from "../components/ProfileSelector.svelte";
|
||||
|
||||
let selectedDevice: DeviceInfo;
|
||||
$: _selectedDevice = selectedDevice;
|
||||
|
||||
let selectedProfile: Profile;
|
||||
$: _selectedProfile = selectedProfile;
|
||||
</script>
|
||||
|
||||
<div class="grow">
|
||||
{#if selectedDevice}
|
||||
<DeviceView device={selectedDevice} />
|
||||
{#if selectedDevice && selectedProfile}
|
||||
<DeviceView bind:device={_selectedDevice} bind:profile={_selectedProfile} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col p-2 grow max-w-[18rem] h-full border-l">
|
||||
<DeviceSelector on:select={({ detail }) => selectedDevice = detail} />
|
||||
<DeviceSelector bind:device={selectedDevice} />
|
||||
{#if selectedDevice}
|
||||
<ProfileSelector bind:device={_selectedDevice} bind:profile={selectedProfile} />
|
||||
{/if}
|
||||
<ActionList />
|
||||
</div>
|
||||
Reference in new issue
Block a user