accelerators sservice
This commit is contained in:
307
src-tauri/src/services/accelerators.rs
Normal file
307
src-tauri/src/services/accelerators.rs
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
use device_query::Keycode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Type)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum AcceleratorAction {
|
||||||
|
SceneInteractivity,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Type)]
|
||||||
|
pub struct KeyboardAccelerator {
|
||||||
|
#[serde(default)]
|
||||||
|
pub modifiers: Vec<AcceleratorModifier>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub key: Option<AcceleratorKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyboardAccelerator {
|
||||||
|
pub fn normalized(mut self) -> Self {
|
||||||
|
self.modifiers.sort_unstable();
|
||||||
|
self.modifiers.dedup();
|
||||||
|
|
||||||
|
if self.modifiers.is_empty() {
|
||||||
|
return Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Type)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum AcceleratorModifier {
|
||||||
|
Cmd,
|
||||||
|
Alt,
|
||||||
|
Ctrl,
|
||||||
|
Shift,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeyboardAccelerator {
|
||||||
|
fn default() -> Self {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
modifiers: vec![AcceleratorModifier::Cmd],
|
||||||
|
key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
modifiers: vec![AcceleratorModifier::Alt],
|
||||||
|
key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_accelerator_for_action(action: AcceleratorAction) -> KeyboardAccelerator {
|
||||||
|
match action {
|
||||||
|
AcceleratorAction::SceneInteractivity => KeyboardAccelerator::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_accelerators() -> std::collections::BTreeMap<AcceleratorAction, KeyboardAccelerator>
|
||||||
|
{
|
||||||
|
let mut map = std::collections::BTreeMap::new();
|
||||||
|
map.insert(
|
||||||
|
AcceleratorAction::SceneInteractivity,
|
||||||
|
default_accelerator_for_action(AcceleratorAction::SceneInteractivity),
|
||||||
|
);
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize_accelerators(
|
||||||
|
mut accelerators: std::collections::BTreeMap<AcceleratorAction, KeyboardAccelerator>,
|
||||||
|
) -> std::collections::BTreeMap<AcceleratorAction, KeyboardAccelerator> {
|
||||||
|
for value in accelerators.values_mut() {
|
||||||
|
*value = value.clone().normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
for action in [AcceleratorAction::SceneInteractivity] {
|
||||||
|
accelerators
|
||||||
|
.entry(action)
|
||||||
|
.or_insert_with(|| default_accelerator_for_action(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
accelerators
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Type)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum AcceleratorKey {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
C,
|
||||||
|
D,
|
||||||
|
E,
|
||||||
|
F,
|
||||||
|
G,
|
||||||
|
H,
|
||||||
|
I,
|
||||||
|
J,
|
||||||
|
K,
|
||||||
|
L,
|
||||||
|
M,
|
||||||
|
N,
|
||||||
|
O,
|
||||||
|
P,
|
||||||
|
Q,
|
||||||
|
R,
|
||||||
|
S,
|
||||||
|
T,
|
||||||
|
U,
|
||||||
|
V,
|
||||||
|
W,
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Z,
|
||||||
|
Num0,
|
||||||
|
Num1,
|
||||||
|
Num2,
|
||||||
|
Num3,
|
||||||
|
Num4,
|
||||||
|
Num5,
|
||||||
|
Num6,
|
||||||
|
Num7,
|
||||||
|
Num8,
|
||||||
|
Num9,
|
||||||
|
F1,
|
||||||
|
F2,
|
||||||
|
F3,
|
||||||
|
F4,
|
||||||
|
F5,
|
||||||
|
F6,
|
||||||
|
F7,
|
||||||
|
F8,
|
||||||
|
F9,
|
||||||
|
F10,
|
||||||
|
F11,
|
||||||
|
F12,
|
||||||
|
Enter,
|
||||||
|
Space,
|
||||||
|
Escape,
|
||||||
|
Tab,
|
||||||
|
Backspace,
|
||||||
|
Delete,
|
||||||
|
Insert,
|
||||||
|
Home,
|
||||||
|
End,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
Minus,
|
||||||
|
Equal,
|
||||||
|
LeftBracket,
|
||||||
|
RightBracket,
|
||||||
|
BackSlash,
|
||||||
|
Semicolon,
|
||||||
|
Apostrophe,
|
||||||
|
Comma,
|
||||||
|
Dot,
|
||||||
|
Slash,
|
||||||
|
Grave,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_any(keys: &[Keycode], candidates: &[Keycode]) -> bool {
|
||||||
|
candidates.iter().any(|candidate| keys.contains(candidate))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_modifier(keys: &[Keycode], modifier: AcceleratorModifier) -> bool {
|
||||||
|
match modifier {
|
||||||
|
AcceleratorModifier::Cmd => contains_any(
|
||||||
|
keys,
|
||||||
|
&[
|
||||||
|
Keycode::Command,
|
||||||
|
Keycode::RCommand,
|
||||||
|
Keycode::LMeta,
|
||||||
|
Keycode::RMeta,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AcceleratorModifier::Alt => contains_any(
|
||||||
|
keys,
|
||||||
|
&[
|
||||||
|
Keycode::LAlt,
|
||||||
|
Keycode::RAlt,
|
||||||
|
Keycode::LOption,
|
||||||
|
Keycode::ROption,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AcceleratorModifier::Ctrl => contains_any(keys, &[Keycode::LControl, Keycode::RControl]),
|
||||||
|
AcceleratorModifier::Shift => contains_any(keys, &[Keycode::LShift, Keycode::RShift]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keycodes_for(key: AcceleratorKey) -> &'static [Keycode] {
|
||||||
|
match key {
|
||||||
|
AcceleratorKey::A => &[Keycode::A],
|
||||||
|
AcceleratorKey::B => &[Keycode::B],
|
||||||
|
AcceleratorKey::C => &[Keycode::C],
|
||||||
|
AcceleratorKey::D => &[Keycode::D],
|
||||||
|
AcceleratorKey::E => &[Keycode::E],
|
||||||
|
AcceleratorKey::F => &[Keycode::F],
|
||||||
|
AcceleratorKey::G => &[Keycode::G],
|
||||||
|
AcceleratorKey::H => &[Keycode::H],
|
||||||
|
AcceleratorKey::I => &[Keycode::I],
|
||||||
|
AcceleratorKey::J => &[Keycode::J],
|
||||||
|
AcceleratorKey::K => &[Keycode::K],
|
||||||
|
AcceleratorKey::L => &[Keycode::L],
|
||||||
|
AcceleratorKey::M => &[Keycode::M],
|
||||||
|
AcceleratorKey::N => &[Keycode::N],
|
||||||
|
AcceleratorKey::O => &[Keycode::O],
|
||||||
|
AcceleratorKey::P => &[Keycode::P],
|
||||||
|
AcceleratorKey::Q => &[Keycode::Q],
|
||||||
|
AcceleratorKey::R => &[Keycode::R],
|
||||||
|
AcceleratorKey::S => &[Keycode::S],
|
||||||
|
AcceleratorKey::T => &[Keycode::T],
|
||||||
|
AcceleratorKey::U => &[Keycode::U],
|
||||||
|
AcceleratorKey::V => &[Keycode::V],
|
||||||
|
AcceleratorKey::W => &[Keycode::W],
|
||||||
|
AcceleratorKey::X => &[Keycode::X],
|
||||||
|
AcceleratorKey::Y => &[Keycode::Y],
|
||||||
|
AcceleratorKey::Z => &[Keycode::Z],
|
||||||
|
AcceleratorKey::Num0 => &[Keycode::Key0],
|
||||||
|
AcceleratorKey::Num1 => &[Keycode::Key1],
|
||||||
|
AcceleratorKey::Num2 => &[Keycode::Key2],
|
||||||
|
AcceleratorKey::Num3 => &[Keycode::Key3],
|
||||||
|
AcceleratorKey::Num4 => &[Keycode::Key4],
|
||||||
|
AcceleratorKey::Num5 => &[Keycode::Key5],
|
||||||
|
AcceleratorKey::Num6 => &[Keycode::Key6],
|
||||||
|
AcceleratorKey::Num7 => &[Keycode::Key7],
|
||||||
|
AcceleratorKey::Num8 => &[Keycode::Key8],
|
||||||
|
AcceleratorKey::Num9 => &[Keycode::Key9],
|
||||||
|
AcceleratorKey::F1 => &[Keycode::F1],
|
||||||
|
AcceleratorKey::F2 => &[Keycode::F2],
|
||||||
|
AcceleratorKey::F3 => &[Keycode::F3],
|
||||||
|
AcceleratorKey::F4 => &[Keycode::F4],
|
||||||
|
AcceleratorKey::F5 => &[Keycode::F5],
|
||||||
|
AcceleratorKey::F6 => &[Keycode::F6],
|
||||||
|
AcceleratorKey::F7 => &[Keycode::F7],
|
||||||
|
AcceleratorKey::F8 => &[Keycode::F8],
|
||||||
|
AcceleratorKey::F9 => &[Keycode::F9],
|
||||||
|
AcceleratorKey::F10 => &[Keycode::F10],
|
||||||
|
AcceleratorKey::F11 => &[Keycode::F11],
|
||||||
|
AcceleratorKey::F12 => &[Keycode::F12],
|
||||||
|
AcceleratorKey::Enter => &[Keycode::Enter, Keycode::NumpadEnter],
|
||||||
|
AcceleratorKey::Space => &[Keycode::Space],
|
||||||
|
AcceleratorKey::Escape => &[Keycode::Escape],
|
||||||
|
AcceleratorKey::Tab => &[Keycode::Tab],
|
||||||
|
AcceleratorKey::Backspace => &[Keycode::Backspace],
|
||||||
|
AcceleratorKey::Delete => &[Keycode::Delete],
|
||||||
|
AcceleratorKey::Insert => &[Keycode::Insert],
|
||||||
|
AcceleratorKey::Home => &[Keycode::Home],
|
||||||
|
AcceleratorKey::End => &[Keycode::End],
|
||||||
|
AcceleratorKey::PageUp => &[Keycode::PageUp],
|
||||||
|
AcceleratorKey::PageDown => &[Keycode::PageDown],
|
||||||
|
AcceleratorKey::ArrowUp => &[Keycode::Up],
|
||||||
|
AcceleratorKey::ArrowDown => &[Keycode::Down],
|
||||||
|
AcceleratorKey::ArrowLeft => &[Keycode::Left],
|
||||||
|
AcceleratorKey::ArrowRight => &[Keycode::Right],
|
||||||
|
AcceleratorKey::Minus => &[Keycode::Minus],
|
||||||
|
AcceleratorKey::Equal => &[Keycode::Equal],
|
||||||
|
AcceleratorKey::LeftBracket => &[Keycode::LeftBracket],
|
||||||
|
AcceleratorKey::RightBracket => &[Keycode::RightBracket],
|
||||||
|
AcceleratorKey::BackSlash => &[Keycode::BackSlash],
|
||||||
|
AcceleratorKey::Semicolon => &[Keycode::Semicolon],
|
||||||
|
AcceleratorKey::Apostrophe => &[Keycode::Apostrophe],
|
||||||
|
AcceleratorKey::Comma => &[Keycode::Comma],
|
||||||
|
AcceleratorKey::Dot => &[Keycode::Dot],
|
||||||
|
AcceleratorKey::Slash => &[Keycode::Slash],
|
||||||
|
AcceleratorKey::Grave => &[Keycode::Grave],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_key(keys: &[Keycode], key: AcceleratorKey) -> bool {
|
||||||
|
contains_any(keys, keycodes_for(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pressed_modifiers(keys: &[Keycode]) -> Vec<AcceleratorModifier> {
|
||||||
|
let mut modifiers = Vec::new();
|
||||||
|
|
||||||
|
for modifier in [
|
||||||
|
AcceleratorModifier::Cmd,
|
||||||
|
AcceleratorModifier::Alt,
|
||||||
|
AcceleratorModifier::Ctrl,
|
||||||
|
AcceleratorModifier::Shift,
|
||||||
|
] {
|
||||||
|
if has_modifier(keys, modifier) {
|
||||||
|
modifiers.push(modifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_accelerator_active(keys: &[Keycode], accelerator: &KeyboardAccelerator) -> bool {
|
||||||
|
if pressed_modifiers(keys) != accelerator.modifiers {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
accelerator.key.map_or(true, |key| has_key(keys, key))
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
mod store;
|
mod store;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -8,216 +10,65 @@ use thiserror::Error;
|
|||||||
pub use store::{load_app_config, save_app_config};
|
pub use store::{load_app_config, save_app_config};
|
||||||
pub use window::open_config_window;
|
pub use window::open_config_window;
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
|
pub use crate::services::accelerators::{
|
||||||
|
default_accelerator_for_action, default_accelerators, normalize_accelerators,
|
||||||
|
AcceleratorAction, KeyboardAccelerator,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone, Debug, Type)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub api_base_url: Option<String>,
|
pub api_base_url: Option<String>,
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub scene_interactivity_hotkey: SceneInteractivityHotkey,
|
pub accelerators: BTreeMap<AcceleratorAction, KeyboardAccelerator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug, PartialEq, Eq, Type)]
|
impl AppConfig {
|
||||||
pub struct SceneInteractivityHotkey {
|
|
||||||
#[serde(default)]
|
|
||||||
pub modifiers: Vec<SceneInteractivityModifier>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub key: Option<SceneInteractivityKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SceneInteractivityHotkey {
|
|
||||||
pub fn normalized(mut self) -> Self {
|
pub fn normalized(mut self) -> Self {
|
||||||
self.modifiers.sort_unstable();
|
self.accelerators = normalize_accelerators(self.accelerators);
|
||||||
self.modifiers.dedup();
|
|
||||||
|
|
||||||
if self.modifiers.is_empty() {
|
|
||||||
return Self::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_legacy(value: LegacySceneInteractivityHotkey) -> Self {
|
pub fn accelerator_for(&self, action: AcceleratorAction) -> KeyboardAccelerator {
|
||||||
let modifiers = match value {
|
self.accelerators
|
||||||
LegacySceneInteractivityHotkey::CmdAlt => {
|
.get(&action)
|
||||||
vec![
|
.cloned()
|
||||||
SceneInteractivityModifier::Cmd,
|
.unwrap_or_else(|| default_accelerator_for_action(action))
|
||||||
SceneInteractivityModifier::Alt,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
LegacySceneInteractivityHotkey::CmdCtrl => {
|
|
||||||
vec![
|
|
||||||
SceneInteractivityModifier::Cmd,
|
|
||||||
SceneInteractivityModifier::Ctrl,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
LegacySceneInteractivityHotkey::AltCtrl => {
|
|
||||||
vec![
|
|
||||||
SceneInteractivityModifier::Alt,
|
|
||||||
SceneInteractivityModifier::Ctrl,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
LegacySceneInteractivityHotkey::Cmd => vec![SceneInteractivityModifier::Cmd],
|
|
||||||
LegacySceneInteractivityHotkey::Alt => vec![SceneInteractivityModifier::Alt],
|
|
||||||
LegacySceneInteractivityHotkey::Ctrl => vec![SceneInteractivityModifier::Ctrl],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
modifiers,
|
api_base_url: None,
|
||||||
key: None,
|
debug_mode: false,
|
||||||
|
accelerators: default_accelerators(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for SceneInteractivityHotkey {
|
impl<'de> Deserialize<'de> for AppConfig {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SceneInteractivityHotkeyV2 {
|
struct AppConfigSerde {
|
||||||
|
api_base_url: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
modifiers: Vec<SceneInteractivityModifier>,
|
debug_mode: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
key: Option<SceneInteractivityKey>,
|
accelerators: BTreeMap<AcceleratorAction, KeyboardAccelerator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
let value = AppConfigSerde::deserialize(deserializer)?;
|
||||||
#[serde(untagged)]
|
|
||||||
enum SceneInteractivityHotkeySerde {
|
|
||||||
V2(SceneInteractivityHotkeyV2),
|
|
||||||
Legacy(LegacySceneInteractivityHotkey),
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = SceneInteractivityHotkeySerde::deserialize(deserializer)?;
|
Ok(Self {
|
||||||
let normalized = match value {
|
api_base_url: value.api_base_url,
|
||||||
SceneInteractivityHotkeySerde::V2(v2) => Self {
|
debug_mode: value.debug_mode,
|
||||||
modifiers: v2.modifiers,
|
accelerators: value.accelerators,
|
||||||
key: v2.key,
|
|
||||||
}
|
|
||||||
.normalized(),
|
|
||||||
SceneInteractivityHotkeySerde::Legacy(legacy) => Self::from_legacy(legacy).normalized(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(normalized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Type)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum SceneInteractivityModifier {
|
|
||||||
Cmd,
|
|
||||||
Alt,
|
|
||||||
Ctrl,
|
|
||||||
Shift,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Type)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum SceneInteractivityKey {
|
|
||||||
A,
|
|
||||||
B,
|
|
||||||
C,
|
|
||||||
D,
|
|
||||||
E,
|
|
||||||
F,
|
|
||||||
G,
|
|
||||||
H,
|
|
||||||
I,
|
|
||||||
J,
|
|
||||||
K,
|
|
||||||
L,
|
|
||||||
M,
|
|
||||||
N,
|
|
||||||
O,
|
|
||||||
P,
|
|
||||||
Q,
|
|
||||||
R,
|
|
||||||
S,
|
|
||||||
T,
|
|
||||||
U,
|
|
||||||
V,
|
|
||||||
W,
|
|
||||||
X,
|
|
||||||
Y,
|
|
||||||
Z,
|
|
||||||
Num0,
|
|
||||||
Num1,
|
|
||||||
Num2,
|
|
||||||
Num3,
|
|
||||||
Num4,
|
|
||||||
Num5,
|
|
||||||
Num6,
|
|
||||||
Num7,
|
|
||||||
Num8,
|
|
||||||
Num9,
|
|
||||||
F1,
|
|
||||||
F2,
|
|
||||||
F3,
|
|
||||||
F4,
|
|
||||||
F5,
|
|
||||||
F6,
|
|
||||||
F7,
|
|
||||||
F8,
|
|
||||||
F9,
|
|
||||||
F10,
|
|
||||||
F11,
|
|
||||||
F12,
|
|
||||||
Enter,
|
|
||||||
Space,
|
|
||||||
Escape,
|
|
||||||
Tab,
|
|
||||||
Backspace,
|
|
||||||
Delete,
|
|
||||||
Insert,
|
|
||||||
Home,
|
|
||||||
End,
|
|
||||||
PageUp,
|
|
||||||
PageDown,
|
|
||||||
ArrowUp,
|
|
||||||
ArrowDown,
|
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
Minus,
|
|
||||||
Equal,
|
|
||||||
LeftBracket,
|
|
||||||
RightBracket,
|
|
||||||
BackSlash,
|
|
||||||
Semicolon,
|
|
||||||
Apostrophe,
|
|
||||||
Comma,
|
|
||||||
Dot,
|
|
||||||
Slash,
|
|
||||||
Grave,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
enum LegacySceneInteractivityHotkey {
|
|
||||||
CmdAlt,
|
|
||||||
CmdCtrl,
|
|
||||||
AltCtrl,
|
|
||||||
Cmd,
|
|
||||||
Alt,
|
|
||||||
Ctrl,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SceneInteractivityHotkey {
|
|
||||||
fn default() -> Self {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
modifiers: vec![SceneInteractivityModifier::Cmd],
|
|
||||||
key: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
modifiers: vec![SceneInteractivityModifier::Alt],
|
|
||||||
key: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.normalized())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use url::Url;
|
|||||||
|
|
||||||
use crate::get_app_handle;
|
use crate::get_app_handle;
|
||||||
|
|
||||||
use super::{AppConfig, ClientConfigError, SceneInteractivityHotkey};
|
use super::{AppConfig, ClientConfigError};
|
||||||
|
|
||||||
const CONFIG_FILENAME: &str = "client_config.json";
|
const CONFIG_FILENAME: &str = "client_config.json";
|
||||||
const DEFAULT_API_BASE_URL: &str = "https://api.friendolls.adamcv.com";
|
const DEFAULT_API_BASE_URL: &str = "https://api.friendolls.adamcv.com";
|
||||||
@@ -48,16 +48,14 @@ fn sanitize(mut config: AppConfig) -> AppConfig {
|
|||||||
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
|
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
|
||||||
.map(|v| strip_trailing_slash(&v));
|
.map(|v| strip_trailing_slash(&v));
|
||||||
|
|
||||||
config.scene_interactivity_hotkey = config.scene_interactivity_hotkey.normalized();
|
config.normalized()
|
||||||
|
|
||||||
config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_app_config() -> AppConfig {
|
pub fn default_app_config() -> AppConfig {
|
||||||
AppConfig {
|
AppConfig {
|
||||||
api_base_url: Some(DEFAULT_API_BASE_URL.to_string()),
|
api_base_url: Some(DEFAULT_API_BASE_URL.to_string()),
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
scene_interactivity_hotkey: SceneInteractivityHotkey::default(),
|
..AppConfig::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod app_data;
|
|||||||
pub mod app_events;
|
pub mod app_events;
|
||||||
pub mod app_menu;
|
pub mod app_menu;
|
||||||
pub mod app_update;
|
pub mod app_update;
|
||||||
|
pub mod accelerators;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod client_config;
|
pub mod client_config;
|
||||||
pub mod cursor;
|
pub mod cursor;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use device_query::{DeviceQuery, DeviceState, Keycode};
|
use device_query::{DeviceQuery, DeviceState};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_specta::Event as _;
|
use tauri_specta::Event as _;
|
||||||
@@ -11,10 +11,8 @@ use tracing::{error, info, warn};
|
|||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r,
|
get_app_handle, lock_r,
|
||||||
services::{
|
services::{
|
||||||
app_events::SceneInteractiveChanged,
|
accelerators::is_accelerator_active, app_events::SceneInteractiveChanged,
|
||||||
client_config::{
|
client_config::AcceleratorAction,
|
||||||
SceneInteractivityHotkey, SceneInteractivityKey, SceneInteractivityModifier,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
@@ -38,135 +36,6 @@ fn scene_interactive_state() -> Arc<AtomicBool> {
|
|||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_modifier(keys: &[Keycode], modifier: SceneInteractivityModifier) -> bool {
|
|
||||||
match modifier {
|
|
||||||
SceneInteractivityModifier::Cmd => {
|
|
||||||
keys.contains(&Keycode::Command)
|
|
||||||
|| keys.contains(&Keycode::RCommand)
|
|
||||||
|| keys.contains(&Keycode::LMeta)
|
|
||||||
|| keys.contains(&Keycode::RMeta)
|
|
||||||
}
|
|
||||||
SceneInteractivityModifier::Alt => {
|
|
||||||
keys.contains(&Keycode::LAlt)
|
|
||||||
|| keys.contains(&Keycode::RAlt)
|
|
||||||
|| keys.contains(&Keycode::LOption)
|
|
||||||
|| keys.contains(&Keycode::ROption)
|
|
||||||
}
|
|
||||||
SceneInteractivityModifier::Ctrl => {
|
|
||||||
keys.contains(&Keycode::LControl) || keys.contains(&Keycode::RControl)
|
|
||||||
}
|
|
||||||
SceneInteractivityModifier::Shift => {
|
|
||||||
keys.contains(&Keycode::LShift) || keys.contains(&Keycode::RShift)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_key(keys: &[Keycode], key: SceneInteractivityKey) -> bool {
|
|
||||||
match key {
|
|
||||||
SceneInteractivityKey::A => keys.contains(&Keycode::A),
|
|
||||||
SceneInteractivityKey::B => keys.contains(&Keycode::B),
|
|
||||||
SceneInteractivityKey::C => keys.contains(&Keycode::C),
|
|
||||||
SceneInteractivityKey::D => keys.contains(&Keycode::D),
|
|
||||||
SceneInteractivityKey::E => keys.contains(&Keycode::E),
|
|
||||||
SceneInteractivityKey::F => keys.contains(&Keycode::F),
|
|
||||||
SceneInteractivityKey::G => keys.contains(&Keycode::G),
|
|
||||||
SceneInteractivityKey::H => keys.contains(&Keycode::H),
|
|
||||||
SceneInteractivityKey::I => keys.contains(&Keycode::I),
|
|
||||||
SceneInteractivityKey::J => keys.contains(&Keycode::J),
|
|
||||||
SceneInteractivityKey::K => keys.contains(&Keycode::K),
|
|
||||||
SceneInteractivityKey::L => keys.contains(&Keycode::L),
|
|
||||||
SceneInteractivityKey::M => keys.contains(&Keycode::M),
|
|
||||||
SceneInteractivityKey::N => keys.contains(&Keycode::N),
|
|
||||||
SceneInteractivityKey::O => keys.contains(&Keycode::O),
|
|
||||||
SceneInteractivityKey::P => keys.contains(&Keycode::P),
|
|
||||||
SceneInteractivityKey::Q => keys.contains(&Keycode::Q),
|
|
||||||
SceneInteractivityKey::R => keys.contains(&Keycode::R),
|
|
||||||
SceneInteractivityKey::S => keys.contains(&Keycode::S),
|
|
||||||
SceneInteractivityKey::T => keys.contains(&Keycode::T),
|
|
||||||
SceneInteractivityKey::U => keys.contains(&Keycode::U),
|
|
||||||
SceneInteractivityKey::V => keys.contains(&Keycode::V),
|
|
||||||
SceneInteractivityKey::W => keys.contains(&Keycode::W),
|
|
||||||
SceneInteractivityKey::X => keys.contains(&Keycode::X),
|
|
||||||
SceneInteractivityKey::Y => keys.contains(&Keycode::Y),
|
|
||||||
SceneInteractivityKey::Z => keys.contains(&Keycode::Z),
|
|
||||||
SceneInteractivityKey::Num0 => keys.contains(&Keycode::Key0),
|
|
||||||
SceneInteractivityKey::Num1 => keys.contains(&Keycode::Key1),
|
|
||||||
SceneInteractivityKey::Num2 => keys.contains(&Keycode::Key2),
|
|
||||||
SceneInteractivityKey::Num3 => keys.contains(&Keycode::Key3),
|
|
||||||
SceneInteractivityKey::Num4 => keys.contains(&Keycode::Key4),
|
|
||||||
SceneInteractivityKey::Num5 => keys.contains(&Keycode::Key5),
|
|
||||||
SceneInteractivityKey::Num6 => keys.contains(&Keycode::Key6),
|
|
||||||
SceneInteractivityKey::Num7 => keys.contains(&Keycode::Key7),
|
|
||||||
SceneInteractivityKey::Num8 => keys.contains(&Keycode::Key8),
|
|
||||||
SceneInteractivityKey::Num9 => keys.contains(&Keycode::Key9),
|
|
||||||
SceneInteractivityKey::F1 => keys.contains(&Keycode::F1),
|
|
||||||
SceneInteractivityKey::F2 => keys.contains(&Keycode::F2),
|
|
||||||
SceneInteractivityKey::F3 => keys.contains(&Keycode::F3),
|
|
||||||
SceneInteractivityKey::F4 => keys.contains(&Keycode::F4),
|
|
||||||
SceneInteractivityKey::F5 => keys.contains(&Keycode::F5),
|
|
||||||
SceneInteractivityKey::F6 => keys.contains(&Keycode::F6),
|
|
||||||
SceneInteractivityKey::F7 => keys.contains(&Keycode::F7),
|
|
||||||
SceneInteractivityKey::F8 => keys.contains(&Keycode::F8),
|
|
||||||
SceneInteractivityKey::F9 => keys.contains(&Keycode::F9),
|
|
||||||
SceneInteractivityKey::F10 => keys.contains(&Keycode::F10),
|
|
||||||
SceneInteractivityKey::F11 => keys.contains(&Keycode::F11),
|
|
||||||
SceneInteractivityKey::F12 => keys.contains(&Keycode::F12),
|
|
||||||
SceneInteractivityKey::Enter => {
|
|
||||||
keys.contains(&Keycode::Enter) || keys.contains(&Keycode::NumpadEnter)
|
|
||||||
}
|
|
||||||
SceneInteractivityKey::Space => keys.contains(&Keycode::Space),
|
|
||||||
SceneInteractivityKey::Escape => keys.contains(&Keycode::Escape),
|
|
||||||
SceneInteractivityKey::Tab => keys.contains(&Keycode::Tab),
|
|
||||||
SceneInteractivityKey::Backspace => keys.contains(&Keycode::Backspace),
|
|
||||||
SceneInteractivityKey::Delete => keys.contains(&Keycode::Delete),
|
|
||||||
SceneInteractivityKey::Insert => keys.contains(&Keycode::Insert),
|
|
||||||
SceneInteractivityKey::Home => keys.contains(&Keycode::Home),
|
|
||||||
SceneInteractivityKey::End => keys.contains(&Keycode::End),
|
|
||||||
SceneInteractivityKey::PageUp => keys.contains(&Keycode::PageUp),
|
|
||||||
SceneInteractivityKey::PageDown => keys.contains(&Keycode::PageDown),
|
|
||||||
SceneInteractivityKey::ArrowUp => keys.contains(&Keycode::Up),
|
|
||||||
SceneInteractivityKey::ArrowDown => keys.contains(&Keycode::Down),
|
|
||||||
SceneInteractivityKey::ArrowLeft => keys.contains(&Keycode::Left),
|
|
||||||
SceneInteractivityKey::ArrowRight => keys.contains(&Keycode::Right),
|
|
||||||
SceneInteractivityKey::Minus => keys.contains(&Keycode::Minus),
|
|
||||||
SceneInteractivityKey::Equal => keys.contains(&Keycode::Equal),
|
|
||||||
SceneInteractivityKey::LeftBracket => keys.contains(&Keycode::LeftBracket),
|
|
||||||
SceneInteractivityKey::RightBracket => keys.contains(&Keycode::RightBracket),
|
|
||||||
SceneInteractivityKey::BackSlash => keys.contains(&Keycode::BackSlash),
|
|
||||||
SceneInteractivityKey::Semicolon => keys.contains(&Keycode::Semicolon),
|
|
||||||
SceneInteractivityKey::Apostrophe => keys.contains(&Keycode::Apostrophe),
|
|
||||||
SceneInteractivityKey::Comma => keys.contains(&Keycode::Comma),
|
|
||||||
SceneInteractivityKey::Dot => keys.contains(&Keycode::Dot),
|
|
||||||
SceneInteractivityKey::Slash => keys.contains(&Keycode::Slash),
|
|
||||||
SceneInteractivityKey::Grave => keys.contains(&Keycode::Grave),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pressed_modifiers(keys: &[Keycode]) -> Vec<SceneInteractivityModifier> {
|
|
||||||
let mut modifiers = Vec::new();
|
|
||||||
|
|
||||||
for modifier in [
|
|
||||||
SceneInteractivityModifier::Cmd,
|
|
||||||
SceneInteractivityModifier::Alt,
|
|
||||||
SceneInteractivityModifier::Ctrl,
|
|
||||||
SceneInteractivityModifier::Shift,
|
|
||||||
] {
|
|
||||||
if has_modifier(keys, modifier) {
|
|
||||||
modifiers.push(modifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modifiers
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_hotkey_active(keys: &[Keycode], hotkey: SceneInteractivityHotkey) -> bool {
|
|
||||||
if pressed_modifiers(keys) != hotkey.modifiers {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hotkey.key.map_or(true, |key| has_key(keys, key))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn start_scene_modifier_listener() {
|
pub(crate) fn start_scene_modifier_listener() {
|
||||||
MODIFIER_LISTENER_INIT.get_or_init(|| {
|
MODIFIER_LISTENER_INIT.get_or_init(|| {
|
||||||
let state = scene_interactive_state();
|
let state = scene_interactive_state();
|
||||||
@@ -206,9 +75,11 @@ pub(crate) fn start_scene_modifier_listener() {
|
|||||||
let keys = device_state.get_keys();
|
let keys = device_state.get_keys();
|
||||||
let hotkey = {
|
let hotkey = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
guard.app_config.scene_interactivity_hotkey.clone()
|
guard
|
||||||
|
.app_config
|
||||||
|
.accelerator_for(AcceleratorAction::SceneInteractivity)
|
||||||
};
|
};
|
||||||
let keys_interactive = is_hotkey_active(&keys, hotkey);
|
let keys_interactive = is_accelerator_active(&keys, &hotkey);
|
||||||
|
|
||||||
let menus_open = {
|
let menus_open = {
|
||||||
if let Ok(menus) = get_open_pet_menus().lock() {
|
if let Ok(menus) = get_open_pet_menus().lock() {
|
||||||
|
|||||||
@@ -174,8 +174,11 @@ userStatusChanged: "user-status-changed"
|
|||||||
|
|
||||||
/** user-defined types **/
|
/** user-defined types **/
|
||||||
|
|
||||||
|
export type AcceleratorAction = "scene_interactivity"
|
||||||
|
export type AcceleratorKey = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "num_0" | "num_1" | "num_2" | "num_3" | "num_4" | "num_5" | "num_6" | "num_7" | "num_8" | "num_9" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" | "enter" | "space" | "escape" | "tab" | "backspace" | "delete" | "insert" | "home" | "end" | "page_up" | "page_down" | "arrow_up" | "arrow_down" | "arrow_left" | "arrow_right" | "minus" | "equal" | "left_bracket" | "right_bracket" | "back_slash" | "semicolon" | "apostrophe" | "comma" | "dot" | "slash" | "grave"
|
||||||
|
export type AcceleratorModifier = "cmd" | "alt" | "ctrl" | "shift"
|
||||||
export type ActiveDollSpriteChanged = string | null
|
export type ActiveDollSpriteChanged = string | null
|
||||||
export type AppConfig = { api_base_url: string | null; debug_mode: boolean; scene_interactivity_hotkey?: SceneInteractivityHotkey }
|
export type AppConfig = { api_base_url: string | null; debug_mode: boolean; accelerators?: Partial<{ [key in AcceleratorAction]: KeyboardAccelerator }> }
|
||||||
export type AppDataRefreshed = UserData
|
export type AppDataRefreshed = UserData
|
||||||
export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled"
|
export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled"
|
||||||
export type AuthFlowUpdated = AuthFlowUpdatedPayload
|
export type AuthFlowUpdated = AuthFlowUpdatedPayload
|
||||||
@@ -212,13 +215,11 @@ export type InteractionDeliveryFailed = InteractionDeliveryFailedDto
|
|||||||
export type InteractionDeliveryFailedDto = { recipientUserId: string; reason: string }
|
export type InteractionDeliveryFailedDto = { recipientUserId: string; reason: string }
|
||||||
export type InteractionPayloadDto = { senderUserId: string; senderName: string; content: string; type: string; timestamp: string }
|
export type InteractionPayloadDto = { senderUserId: string; senderName: string; content: string; type: string; timestamp: string }
|
||||||
export type InteractionReceived = InteractionPayloadDto
|
export type InteractionReceived = InteractionPayloadDto
|
||||||
|
export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null }
|
||||||
export type ModuleMetadata = { id: string; name: string; version: string; description: string | null }
|
export type ModuleMetadata = { id: string; name: string; version: string; description: string | null }
|
||||||
export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null }
|
export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null }
|
||||||
export type SceneData = { display: DisplayData; grid_size: number }
|
export type SceneData = { display: DisplayData; grid_size: number }
|
||||||
export type SceneInteractiveChanged = boolean
|
export type SceneInteractiveChanged = boolean
|
||||||
export type SceneInteractivityHotkey = { modifiers?: SceneInteractivityModifier[]; key?: SceneInteractivityKey | null }
|
|
||||||
export type SceneInteractivityKey = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "num_0" | "num_1" | "num_2" | "num_3" | "num_4" | "num_5" | "num_6" | "num_7" | "num_8" | "num_9" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" | "enter" | "space" | "escape" | "tab" | "backspace" | "delete" | "insert" | "home" | "end" | "page_up" | "page_down" | "arrow_up" | "arrow_down" | "arrow_left" | "arrow_right" | "minus" | "equal" | "left_bracket" | "right_bracket" | "back_slash" | "semicolon" | "apostrophe" | "comma" | "dot" | "slash" | "grave"
|
|
||||||
export type SceneInteractivityModifier = "cmd" | "alt" | "ctrl" | "shift"
|
|
||||||
export type SendFriendRequestDto = { receiverId: string }
|
export type SendFriendRequestDto = { receiverId: string }
|
||||||
export type SendInteractionDto = { recipientUserId: string; content: string; type: string }
|
export type SendInteractionDto = { recipientUserId: string; content: string; type: string }
|
||||||
export type SetInteractionOverlay = boolean
|
export type SetInteractionOverlay = boolean
|
||||||
|
|||||||
212
src/lib/utils/accelerators.ts
Normal file
212
src/lib/utils/accelerators.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import type {
|
||||||
|
AcceleratorAction,
|
||||||
|
AcceleratorKey,
|
||||||
|
AcceleratorModifier,
|
||||||
|
AppConfig,
|
||||||
|
KeyboardAccelerator,
|
||||||
|
} from "$lib/bindings";
|
||||||
|
|
||||||
|
export const SCENE_INTERACTIVITY_ACTION: AcceleratorAction =
|
||||||
|
"scene_interactivity";
|
||||||
|
|
||||||
|
const MODIFIER_PRIORITY: Record<AcceleratorModifier, number> = {
|
||||||
|
cmd: 0,
|
||||||
|
alt: 1,
|
||||||
|
ctrl: 2,
|
||||||
|
shift: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MODIFIER_LABELS: Record<AcceleratorModifier, string> = {
|
||||||
|
cmd: "Cmd",
|
||||||
|
alt: "Alt",
|
||||||
|
ctrl: "Ctrl",
|
||||||
|
shift: "Shift",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SPECIAL_KEY_LABELS: Partial<Record<AcceleratorKey, string>> = {
|
||||||
|
enter: "Enter",
|
||||||
|
space: "Space",
|
||||||
|
escape: "Escape",
|
||||||
|
tab: "Tab",
|
||||||
|
backspace: "Backspace",
|
||||||
|
delete: "Delete",
|
||||||
|
insert: "Insert",
|
||||||
|
home: "Home",
|
||||||
|
end: "End",
|
||||||
|
page_up: "Page Up",
|
||||||
|
page_down: "Page Down",
|
||||||
|
arrow_up: "Arrow Up",
|
||||||
|
arrow_down: "Arrow Down",
|
||||||
|
arrow_left: "Arrow Left",
|
||||||
|
arrow_right: "Arrow Right",
|
||||||
|
minus: "-",
|
||||||
|
equal: "=",
|
||||||
|
left_bracket: "[",
|
||||||
|
right_bracket: "]",
|
||||||
|
back_slash: "\\",
|
||||||
|
semicolon: ";",
|
||||||
|
apostrophe: "'",
|
||||||
|
comma: ",",
|
||||||
|
dot: ".",
|
||||||
|
slash: "/",
|
||||||
|
grave: "`",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SPECIAL_CODE_TO_KEY: Record<string, AcceleratorKey> = {
|
||||||
|
Enter: "enter",
|
||||||
|
Space: "space",
|
||||||
|
Escape: "escape",
|
||||||
|
Tab: "tab",
|
||||||
|
Backspace: "backspace",
|
||||||
|
Delete: "delete",
|
||||||
|
Insert: "insert",
|
||||||
|
Home: "home",
|
||||||
|
End: "end",
|
||||||
|
PageUp: "page_up",
|
||||||
|
PageDown: "page_down",
|
||||||
|
ArrowUp: "arrow_up",
|
||||||
|
ArrowDown: "arrow_down",
|
||||||
|
ArrowLeft: "arrow_left",
|
||||||
|
ArrowRight: "arrow_right",
|
||||||
|
Minus: "minus",
|
||||||
|
Equal: "equal",
|
||||||
|
BracketLeft: "left_bracket",
|
||||||
|
BracketRight: "right_bracket",
|
||||||
|
Backslash: "back_slash",
|
||||||
|
Semicolon: "semicolon",
|
||||||
|
Quote: "apostrophe",
|
||||||
|
Comma: "comma",
|
||||||
|
Period: "dot",
|
||||||
|
Slash: "slash",
|
||||||
|
Backquote: "grave",
|
||||||
|
};
|
||||||
|
|
||||||
|
const FUNCTION_KEY_MIN = 1;
|
||||||
|
const FUNCTION_KEY_MAX = 12;
|
||||||
|
|
||||||
|
const toLetterKey = (code: string): AcceleratorKey | null => {
|
||||||
|
if (!code.startsWith("Key") || code.length !== 4) return null;
|
||||||
|
|
||||||
|
const letter = code[3].toLowerCase();
|
||||||
|
if (letter < "a" || letter > "z") return null;
|
||||||
|
|
||||||
|
return letter as AcceleratorKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toDigitKey = (code: string): AcceleratorKey | null => {
|
||||||
|
if (!code.startsWith("Digit") || code.length !== 6) return null;
|
||||||
|
|
||||||
|
const digit = code[5];
|
||||||
|
if (digit < "0" || digit > "9") return null;
|
||||||
|
|
||||||
|
return `num_${digit}` as AcceleratorKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toFunctionKey = (code: string): AcceleratorKey | null => {
|
||||||
|
if (!code.startsWith("F")) return null;
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(code.slice(1), 10);
|
||||||
|
if (Number.isNaN(parsed)) return null;
|
||||||
|
if (parsed < FUNCTION_KEY_MIN || parsed > FUNCTION_KEY_MAX) return null;
|
||||||
|
|
||||||
|
return `f${parsed}` as AcceleratorKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPatternKey = (code: string): AcceleratorKey | null => {
|
||||||
|
return toLetterKey(code) ?? toDigitKey(code) ?? toFunctionKey(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPatternLabel = (key: AcceleratorKey): string | null => {
|
||||||
|
if (key.length === 1) {
|
||||||
|
return key.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.startsWith("num_")) {
|
||||||
|
return key.slice(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.startsWith("f")) {
|
||||||
|
const parsed = Number.parseInt(key.slice(1), 10);
|
||||||
|
if (Number.isNaN(parsed)) return null;
|
||||||
|
if (parsed < FUNCTION_KEY_MIN || parsed > FUNCTION_KEY_MAX) return null;
|
||||||
|
return `F${parsed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const keyFromKeyboardCode = (code: string): AcceleratorKey | null => {
|
||||||
|
return toPatternKey(code) ?? SPECIAL_CODE_TO_KEY[code] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const labelForAcceleratorKey = (key: AcceleratorKey): string => {
|
||||||
|
return toPatternLabel(key) ?? SPECIAL_KEY_LABELS[key] ?? key;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MODIFIER_CODES = new Set([
|
||||||
|
"MetaLeft",
|
||||||
|
"MetaRight",
|
||||||
|
"AltLeft",
|
||||||
|
"AltRight",
|
||||||
|
"ControlLeft",
|
||||||
|
"ControlRight",
|
||||||
|
"ShiftLeft",
|
||||||
|
"ShiftRight",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const normalizeAccelerator = (
|
||||||
|
accelerator: KeyboardAccelerator,
|
||||||
|
): KeyboardAccelerator => {
|
||||||
|
const uniqueModifiers = [...new Set(accelerator.modifiers ?? [])].sort(
|
||||||
|
(a, b) => MODIFIER_PRIORITY[a] - MODIFIER_PRIORITY[b],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiers: uniqueModifiers,
|
||||||
|
key: accelerator.key ?? null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatAcceleratorLabel = (accelerator: KeyboardAccelerator): string => {
|
||||||
|
const modifiers = (accelerator.modifiers ?? []).map(
|
||||||
|
(modifier) => MODIFIER_LABELS[modifier],
|
||||||
|
);
|
||||||
|
const key = accelerator.key ? labelForAcceleratorKey(accelerator.key) : null;
|
||||||
|
return key ? [...modifiers, key].join(" + ") : modifiers.join(" + ");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getModifiersFromEvent = (
|
||||||
|
event: KeyboardEvent,
|
||||||
|
): AcceleratorModifier[] => {
|
||||||
|
const modifiers: AcceleratorModifier[] = [];
|
||||||
|
if (event.metaKey) modifiers.push("cmd");
|
||||||
|
if (event.altKey) modifiers.push("alt");
|
||||||
|
if (event.ctrlKey) modifiers.push("ctrl");
|
||||||
|
if (event.shiftKey) modifiers.push("shift");
|
||||||
|
return modifiers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const acceleratorsEqual = (
|
||||||
|
left: KeyboardAccelerator,
|
||||||
|
right: KeyboardAccelerator,
|
||||||
|
): boolean => {
|
||||||
|
const normalizedLeft = normalizeAccelerator(left);
|
||||||
|
const normalizedRight = normalizeAccelerator(right);
|
||||||
|
|
||||||
|
if (normalizedLeft.key !== normalizedRight.key) return false;
|
||||||
|
|
||||||
|
const leftModifiers = normalizedLeft.modifiers ?? [];
|
||||||
|
const rightModifiers = normalizedRight.modifiers ?? [];
|
||||||
|
|
||||||
|
if (leftModifiers.length !== rightModifiers.length) return false;
|
||||||
|
|
||||||
|
return leftModifiers.every((modifier, index) => modifier === rightModifiers[index]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAcceleratorForAction = (
|
||||||
|
config: AppConfig,
|
||||||
|
action: AcceleratorAction,
|
||||||
|
): KeyboardAccelerator | null => {
|
||||||
|
const accelerator = config.accelerators?.[action];
|
||||||
|
return accelerator ? normalizeAccelerator(accelerator) : null;
|
||||||
|
};
|
||||||
@@ -3,263 +3,41 @@
|
|||||||
import {
|
import {
|
||||||
commands,
|
commands,
|
||||||
type AppConfig,
|
type AppConfig,
|
||||||
type SceneInteractivityHotkey,
|
type KeyboardAccelerator,
|
||||||
type SceneInteractivityKey,
|
|
||||||
type SceneInteractivityModifier,
|
|
||||||
} from "$lib/bindings";
|
} from "$lib/bindings";
|
||||||
|
import {
|
||||||
|
MODIFIER_CODES,
|
||||||
|
SCENE_INTERACTIVITY_ACTION,
|
||||||
|
acceleratorsEqual,
|
||||||
|
formatAcceleratorLabel,
|
||||||
|
getAcceleratorForAction,
|
||||||
|
getModifiersFromEvent,
|
||||||
|
keyFromKeyboardCode,
|
||||||
|
normalizeAccelerator,
|
||||||
|
} from "$lib/utils/accelerators";
|
||||||
import { appData } from "../../../events/app-data";
|
import { appData } from "../../../events/app-data";
|
||||||
import Power from "../../../assets/icons/power.svelte";
|
import Power from "../../../assets/icons/power.svelte";
|
||||||
|
|
||||||
const DEFAULT_HOTKEY: SceneInteractivityHotkey = {
|
|
||||||
modifiers: ["alt"],
|
|
||||||
key: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MODIFIER_LABELS: Record<SceneInteractivityModifier, string> = {
|
|
||||||
cmd: "Cmd",
|
|
||||||
alt: "Alt",
|
|
||||||
ctrl: "Ctrl",
|
|
||||||
shift: "Shift",
|
|
||||||
};
|
|
||||||
|
|
||||||
const KEY_LABELS: Record<SceneInteractivityKey, string> = {
|
|
||||||
a: "A",
|
|
||||||
b: "B",
|
|
||||||
c: "C",
|
|
||||||
d: "D",
|
|
||||||
e: "E",
|
|
||||||
f: "F",
|
|
||||||
g: "G",
|
|
||||||
h: "H",
|
|
||||||
i: "I",
|
|
||||||
j: "J",
|
|
||||||
k: "K",
|
|
||||||
l: "L",
|
|
||||||
m: "M",
|
|
||||||
n: "N",
|
|
||||||
o: "O",
|
|
||||||
p: "P",
|
|
||||||
q: "Q",
|
|
||||||
r: "R",
|
|
||||||
s: "S",
|
|
||||||
t: "T",
|
|
||||||
u: "U",
|
|
||||||
v: "V",
|
|
||||||
w: "W",
|
|
||||||
x: "X",
|
|
||||||
y: "Y",
|
|
||||||
z: "Z",
|
|
||||||
num_0: "0",
|
|
||||||
num_1: "1",
|
|
||||||
num_2: "2",
|
|
||||||
num_3: "3",
|
|
||||||
num_4: "4",
|
|
||||||
num_5: "5",
|
|
||||||
num_6: "6",
|
|
||||||
num_7: "7",
|
|
||||||
num_8: "8",
|
|
||||||
num_9: "9",
|
|
||||||
f1: "F1",
|
|
||||||
f2: "F2",
|
|
||||||
f3: "F3",
|
|
||||||
f4: "F4",
|
|
||||||
f5: "F5",
|
|
||||||
f6: "F6",
|
|
||||||
f7: "F7",
|
|
||||||
f8: "F8",
|
|
||||||
f9: "F9",
|
|
||||||
f10: "F10",
|
|
||||||
f11: "F11",
|
|
||||||
f12: "F12",
|
|
||||||
enter: "Enter",
|
|
||||||
space: "Space",
|
|
||||||
escape: "Escape",
|
|
||||||
tab: "Tab",
|
|
||||||
backspace: "Backspace",
|
|
||||||
delete: "Delete",
|
|
||||||
insert: "Insert",
|
|
||||||
home: "Home",
|
|
||||||
end: "End",
|
|
||||||
page_up: "Page Up",
|
|
||||||
page_down: "Page Down",
|
|
||||||
arrow_up: "Arrow Up",
|
|
||||||
arrow_down: "Arrow Down",
|
|
||||||
arrow_left: "Arrow Left",
|
|
||||||
arrow_right: "Arrow Right",
|
|
||||||
minus: "-",
|
|
||||||
equal: "=",
|
|
||||||
left_bracket: "[",
|
|
||||||
right_bracket: "]",
|
|
||||||
back_slash: "\\",
|
|
||||||
semicolon: ";",
|
|
||||||
apostrophe: "'",
|
|
||||||
comma: ",",
|
|
||||||
dot: ".",
|
|
||||||
slash: "/",
|
|
||||||
grave: "`",
|
|
||||||
};
|
|
||||||
|
|
||||||
const CODE_TO_KEY: Partial<Record<string, SceneInteractivityKey>> = {
|
|
||||||
KeyA: "a",
|
|
||||||
KeyB: "b",
|
|
||||||
KeyC: "c",
|
|
||||||
KeyD: "d",
|
|
||||||
KeyE: "e",
|
|
||||||
KeyF: "f",
|
|
||||||
KeyG: "g",
|
|
||||||
KeyH: "h",
|
|
||||||
KeyI: "i",
|
|
||||||
KeyJ: "j",
|
|
||||||
KeyK: "k",
|
|
||||||
KeyL: "l",
|
|
||||||
KeyM: "m",
|
|
||||||
KeyN: "n",
|
|
||||||
KeyO: "o",
|
|
||||||
KeyP: "p",
|
|
||||||
KeyQ: "q",
|
|
||||||
KeyR: "r",
|
|
||||||
KeyS: "s",
|
|
||||||
KeyT: "t",
|
|
||||||
KeyU: "u",
|
|
||||||
KeyV: "v",
|
|
||||||
KeyW: "w",
|
|
||||||
KeyX: "x",
|
|
||||||
KeyY: "y",
|
|
||||||
KeyZ: "z",
|
|
||||||
Digit0: "num_0",
|
|
||||||
Digit1: "num_1",
|
|
||||||
Digit2: "num_2",
|
|
||||||
Digit3: "num_3",
|
|
||||||
Digit4: "num_4",
|
|
||||||
Digit5: "num_5",
|
|
||||||
Digit6: "num_6",
|
|
||||||
Digit7: "num_7",
|
|
||||||
Digit8: "num_8",
|
|
||||||
Digit9: "num_9",
|
|
||||||
F1: "f1",
|
|
||||||
F2: "f2",
|
|
||||||
F3: "f3",
|
|
||||||
F4: "f4",
|
|
||||||
F5: "f5",
|
|
||||||
F6: "f6",
|
|
||||||
F7: "f7",
|
|
||||||
F8: "f8",
|
|
||||||
F9: "f9",
|
|
||||||
F10: "f10",
|
|
||||||
F11: "f11",
|
|
||||||
F12: "f12",
|
|
||||||
Enter: "enter",
|
|
||||||
Space: "space",
|
|
||||||
Escape: "escape",
|
|
||||||
Tab: "tab",
|
|
||||||
Backspace: "backspace",
|
|
||||||
Delete: "delete",
|
|
||||||
Insert: "insert",
|
|
||||||
Home: "home",
|
|
||||||
End: "end",
|
|
||||||
PageUp: "page_up",
|
|
||||||
PageDown: "page_down",
|
|
||||||
ArrowUp: "arrow_up",
|
|
||||||
ArrowDown: "arrow_down",
|
|
||||||
ArrowLeft: "arrow_left",
|
|
||||||
ArrowRight: "arrow_right",
|
|
||||||
Minus: "minus",
|
|
||||||
Equal: "equal",
|
|
||||||
BracketLeft: "left_bracket",
|
|
||||||
BracketRight: "right_bracket",
|
|
||||||
Backslash: "back_slash",
|
|
||||||
Semicolon: "semicolon",
|
|
||||||
Quote: "apostrophe",
|
|
||||||
Comma: "comma",
|
|
||||||
Period: "dot",
|
|
||||||
Slash: "slash",
|
|
||||||
Backquote: "grave",
|
|
||||||
};
|
|
||||||
|
|
||||||
const MODIFIER_CODES = new Set([
|
|
||||||
"MetaLeft",
|
|
||||||
"MetaRight",
|
|
||||||
"AltLeft",
|
|
||||||
"AltRight",
|
|
||||||
"ControlLeft",
|
|
||||||
"ControlRight",
|
|
||||||
"ShiftLeft",
|
|
||||||
"ShiftRight",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const normalizeHotkey = (
|
|
||||||
hotkey: SceneInteractivityHotkey | null | undefined,
|
|
||||||
): SceneInteractivityHotkey => {
|
|
||||||
if (!hotkey) return { ...DEFAULT_HOTKEY };
|
|
||||||
|
|
||||||
const uniqueModifiers = [...new Set(hotkey.modifiers ?? [])].sort();
|
|
||||||
if (uniqueModifiers.length === 0 && !hotkey.key) {
|
|
||||||
return { ...DEFAULT_HOTKEY };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
modifiers: uniqueModifiers,
|
|
||||||
key: hotkey.key ?? null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatHotkeyLabel = (hotkey: SceneInteractivityHotkey): string => {
|
|
||||||
const modifiers = (hotkey.modifiers ?? []).map(
|
|
||||||
(modifier) => MODIFIER_LABELS[modifier],
|
|
||||||
);
|
|
||||||
const key = hotkey.key ? KEY_LABELS[hotkey.key] : null;
|
|
||||||
const parts = key ? [...modifiers, key] : modifiers;
|
|
||||||
return parts.join(" + ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getModifiersFromEvent = (
|
|
||||||
event: KeyboardEvent,
|
|
||||||
): SceneInteractivityModifier[] => {
|
|
||||||
const modifiers: SceneInteractivityModifier[] = [];
|
|
||||||
if (event.metaKey) modifiers.push("cmd");
|
|
||||||
if (event.altKey) modifiers.push("alt");
|
|
||||||
if (event.ctrlKey) modifiers.push("ctrl");
|
|
||||||
if (event.shiftKey) modifiers.push("shift");
|
|
||||||
return modifiers;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hotkeysEqual = (
|
|
||||||
a: SceneInteractivityHotkey | null | undefined,
|
|
||||||
b: SceneInteractivityHotkey | null | undefined,
|
|
||||||
): boolean => {
|
|
||||||
const left = normalizeHotkey(a);
|
|
||||||
const right = normalizeHotkey(b);
|
|
||||||
|
|
||||||
if (left.key !== right.key) return false;
|
|
||||||
if ((left.modifiers?.length ?? 0) !== (right.modifiers?.length ?? 0)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (left.modifiers ?? []).every(
|
|
||||||
(modifier, index) => modifier === (right.modifiers ?? [])[index],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let signingOut = false;
|
let signingOut = false;
|
||||||
let appConfig: AppConfig | null = null;
|
let appConfig: AppConfig | null = null;
|
||||||
let hotkey = { ...DEFAULT_HOTKEY };
|
let accelerator: KeyboardAccelerator | null = null;
|
||||||
let captureMode = false;
|
let captureMode = false;
|
||||||
let capturePreviewLabel = "";
|
let capturePreviewLabel = "";
|
||||||
let hotkeyInput: HTMLInputElement | null = null;
|
let acceleratorInput: HTMLInputElement | null = null;
|
||||||
let hotkeyLabel = "";
|
let acceleratorLabel = "";
|
||||||
let hotkeyError = "";
|
let acceleratorError = "";
|
||||||
let hotkeySuccess = "";
|
let acceleratorSuccess = "";
|
||||||
let hotkeyDirty = false;
|
let acceleratorDirty = false;
|
||||||
let hotkeySaving = false;
|
let acceleratorSaving = false;
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const config = await commands.getClientConfig();
|
const config = await commands.getClientConfig();
|
||||||
appConfig = config;
|
appConfig = config;
|
||||||
hotkey = normalizeHotkey(config.scene_interactivity_hotkey);
|
accelerator = getAcceleratorForAction(config, SCENE_INTERACTIVITY_ACTION);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load client config", error);
|
console.error("Failed to load client config", error);
|
||||||
hotkeyError = "Failed to load current hotkey settings.";
|
acceleratorError = "Failed to load current accelerator settings.";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -285,41 +63,44 @@
|
|||||||
const beginCapture = () => {
|
const beginCapture = () => {
|
||||||
captureMode = true;
|
captureMode = true;
|
||||||
capturePreviewLabel = "";
|
capturePreviewLabel = "";
|
||||||
hotkeyError = "";
|
acceleratorError = "";
|
||||||
hotkeySuccess = "";
|
acceleratorSuccess = "";
|
||||||
setTimeout(() => hotkeyInput?.focus(), 0);
|
setTimeout(() => acceleratorInput?.focus(), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopCapture = () => {
|
const stopCapture = () => {
|
||||||
captureMode = false;
|
captureMode = false;
|
||||||
capturePreviewLabel = "";
|
capturePreviewLabel = "";
|
||||||
saveHotkey();
|
saveAccelerator();
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveHotkey = async () => {
|
const saveAccelerator = async () => {
|
||||||
if (!appConfig || hotkeySaving) return;
|
if (!appConfig || !accelerator || acceleratorSaving) return;
|
||||||
hotkeySaving = true;
|
acceleratorSaving = true;
|
||||||
hotkeyError = "";
|
acceleratorError = "";
|
||||||
hotkeySuccess = "";
|
acceleratorSuccess = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nextConfig: AppConfig = {
|
const nextConfig: AppConfig = {
|
||||||
...appConfig,
|
...appConfig,
|
||||||
scene_interactivity_hotkey: hotkey,
|
accelerators: {
|
||||||
|
...(appConfig.accelerators ?? {}),
|
||||||
|
[SCENE_INTERACTIVITY_ACTION]: accelerator,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
await commands.saveClientConfig(nextConfig);
|
await commands.saveClientConfig(nextConfig);
|
||||||
appConfig = nextConfig;
|
appConfig = nextConfig;
|
||||||
hotkeySuccess = "Hotkey saved.";
|
acceleratorSuccess = "Accelerator saved.";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save hotkey", error);
|
console.error("Failed to save accelerator", error);
|
||||||
hotkeyError = "Failed to save hotkey.";
|
acceleratorError = "Failed to save accelerator.";
|
||||||
} finally {
|
} finally {
|
||||||
hotkeySaving = false;
|
acceleratorSaving = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onHotkeyCapture = async (event: KeyboardEvent) => {
|
const onAcceleratorCapture = async (event: KeyboardEvent) => {
|
||||||
if (!captureMode || hotkeySaving) return;
|
if (!captureMode || acceleratorSaving) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -330,47 +111,52 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modifiers = getModifiersFromEvent(event);
|
const modifiers = getModifiersFromEvent(event);
|
||||||
const key = CODE_TO_KEY[event.code] ?? null;
|
const key = keyFromKeyboardCode(event.code);
|
||||||
const modifierOnlyPress = MODIFIER_CODES.has(event.code);
|
const modifierOnlyPress = MODIFIER_CODES.has(event.code);
|
||||||
|
|
||||||
if (modifiers.length === 0) {
|
if (modifiers.length === 0) {
|
||||||
capturePreviewLabel = "";
|
capturePreviewLabel = "";
|
||||||
hotkeyError = "Hotkey must include at least one modifier key.";
|
acceleratorError = "Accelerator must include at least one modifier key.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modifierOnlyPress) {
|
if (modifierOnlyPress) {
|
||||||
const nextHotkey = normalizeHotkey({
|
const nextAccelerator = normalizeAccelerator({
|
||||||
modifiers,
|
modifiers,
|
||||||
key: null,
|
key: null,
|
||||||
});
|
});
|
||||||
hotkey = nextHotkey;
|
accelerator = nextAccelerator;
|
||||||
capturePreviewLabel = formatHotkeyLabel(nextHotkey);
|
capturePreviewLabel = formatAcceleratorLabel(nextAccelerator);
|
||||||
hotkeySuccess = "";
|
acceleratorSuccess = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
hotkeyError = "That key is not supported for hotkey combos yet.";
|
acceleratorError = "That key is not supported for accelerator combos yet.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextHotkey = normalizeHotkey({ modifiers, key });
|
const nextAccelerator = normalizeAccelerator({ modifiers, key });
|
||||||
hotkey = nextHotkey;
|
accelerator = nextAccelerator;
|
||||||
capturePreviewLabel = formatHotkeyLabel(nextHotkey);
|
capturePreviewLabel = formatAcceleratorLabel(nextAccelerator);
|
||||||
hotkeySuccess = "";
|
acceleratorSuccess = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
$: hotkeyLabel = formatHotkeyLabel(hotkey);
|
$: acceleratorLabel = accelerator ? formatAcceleratorLabel(accelerator) : "";
|
||||||
$: hotkeyDirty = appConfig
|
$: acceleratorDirty = appConfig
|
||||||
? !hotkeysEqual(hotkey, appConfig.scene_interactivity_hotkey)
|
? accelerator && appConfig.accelerators?.[SCENE_INTERACTIVITY_ACTION]
|
||||||
|
? !acceleratorsEqual(
|
||||||
|
accelerator,
|
||||||
|
appConfig.accelerators?.[SCENE_INTERACTIVITY_ACTION],
|
||||||
|
)
|
||||||
|
: false
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadConfig();
|
loadConfig();
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
void onHotkeyCapture(event);
|
void onAcceleratorCapture(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeydown, true);
|
window.addEventListener("keydown", handleKeydown, true);
|
||||||
@@ -385,31 +171,31 @@
|
|||||||
<div class="flex flex-col gap-4 max-w-md">
|
<div class="flex flex-col gap-4 max-w-md">
|
||||||
<p>{$appData?.user?.name}'s preferences</p>
|
<p>{$appData?.user?.name}'s preferences</p>
|
||||||
<label class="flex flex-col gap-2">
|
<label class="flex flex-col gap-2">
|
||||||
<span class="text-sm">Scene Interactivity Hotkey</span>
|
<span class="text-sm">Scene Interactivity Accelerator</span>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<input
|
<input
|
||||||
class="input input-bordered flex-1"
|
class="input input-bordered flex-1"
|
||||||
readonly
|
readonly
|
||||||
bind:this={hotkeyInput}
|
bind:this={acceleratorInput}
|
||||||
value={captureMode
|
value={captureMode
|
||||||
? capturePreviewLabel || "Press your shortcut..."
|
? capturePreviewLabel || "Press your shortcut..."
|
||||||
: hotkeyLabel}
|
: acceleratorLabel}
|
||||||
/>
|
/>
|
||||||
{#if captureMode}
|
{#if captureMode}
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline"
|
class="btn btn-outline"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={hotkeySaving}
|
disabled={acceleratorSaving}
|
||||||
onclick={stopCapture}
|
onclick={stopCapture}
|
||||||
>
|
>
|
||||||
{hotkeySaving ? "Saving..." : "Stop Record"}
|
{acceleratorSaving ? "Saving..." : "Stop Record"}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline"
|
class="btn btn-outline"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!appConfig || hotkeySaving}
|
disabled={!appConfig || acceleratorSaving}
|
||||||
onclick={beginCapture}
|
onclick={beginCapture}
|
||||||
>
|
>
|
||||||
Record
|
Record
|
||||||
@@ -421,11 +207,11 @@
|
|||||||
Requires at least one modifier (Cmd, Alt, Ctrl, Shift). Press Escape to
|
Requires at least one modifier (Cmd, Alt, Ctrl, Shift). Press Escape to
|
||||||
cancel recording.
|
cancel recording.
|
||||||
</span>
|
</span>
|
||||||
{#if hotkeyError}
|
{#if acceleratorError}
|
||||||
<span class="text-xs text-error">{hotkeyError}</span>
|
<span class="text-xs text-error">{acceleratorError}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hotkeySuccess}
|
{#if acceleratorSuccess}
|
||||||
<span class="text-xs text-success">{hotkeySuccess}</span>
|
<span class="text-xs text-success">{acceleratorSuccess}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
|
|||||||
@@ -3,34 +3,12 @@
|
|||||||
import {
|
import {
|
||||||
commands,
|
commands,
|
||||||
type AppConfig,
|
type AppConfig,
|
||||||
type SceneInteractivityHotkey,
|
|
||||||
} from "$lib/bindings";
|
} from "$lib/bindings";
|
||||||
|
|
||||||
const DEFAULT_HOTKEY: SceneInteractivityHotkey = {
|
|
||||||
modifiers: ["alt"],
|
|
||||||
key: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeHotkey = (
|
|
||||||
hotkey: SceneInteractivityHotkey | null | undefined,
|
|
||||||
): SceneInteractivityHotkey => {
|
|
||||||
if (!hotkey) return { ...DEFAULT_HOTKEY };
|
|
||||||
|
|
||||||
const uniqueModifiers = [...new Set(hotkey.modifiers ?? [])].sort();
|
|
||||||
if (uniqueModifiers.length === 0) {
|
|
||||||
return { ...DEFAULT_HOTKEY };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
modifiers: uniqueModifiers,
|
|
||||||
key: hotkey.key ?? null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let form: AppConfig = {
|
let form: AppConfig = {
|
||||||
api_base_url: "",
|
api_base_url: "",
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
scene_interactivity_hotkey: { ...DEFAULT_HOTKEY },
|
accelerators: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
let saving = false;
|
let saving = false;
|
||||||
@@ -44,9 +22,7 @@
|
|||||||
form = {
|
form = {
|
||||||
api_base_url: config.api_base_url ?? "",
|
api_base_url: config.api_base_url ?? "",
|
||||||
debug_mode: config.debug_mode ?? false,
|
debug_mode: config.debug_mode ?? false,
|
||||||
scene_interactivity_hotkey: normalizeHotkey(
|
accelerators: config.accelerators ?? {},
|
||||||
config.scene_interactivity_hotkey,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorMessage = `Failed to load config: ${err}`;
|
errorMessage = `Failed to load config: ${err}`;
|
||||||
@@ -85,9 +61,7 @@
|
|||||||
await commands.saveClientConfig({
|
await commands.saveClientConfig({
|
||||||
api_base_url: form.api_base_url?.trim() || null,
|
api_base_url: form.api_base_url?.trim() || null,
|
||||||
debug_mode: form.debug_mode,
|
debug_mode: form.debug_mode,
|
||||||
scene_interactivity_hotkey: normalizeHotkey(
|
accelerators: form.accelerators ?? {},
|
||||||
form.scene_interactivity_hotkey,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
successMessage = "Success. Restart to apply changes.";
|
successMessage = "Success. Restart to apply changes.";
|
||||||
|
|||||||
Reference in New Issue
Block a user