accelerators sservice

This commit is contained in:
2026-03-22 02:10:10 +08:00
parent a72430e65f
commit aa9d1f54a1
9 changed files with 642 additions and 641 deletions

View 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))
}

View File

@@ -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())
} }
} }

View File

@@ -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()
} }
} }

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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

View 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;
};

View File

@@ -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">

View File

@@ -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.";