custom interactivity hotkey
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
mod store;
|
||||
mod window;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use specta::Type;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -12,6 +12,213 @@ pub use window::open_config_window;
|
||||
pub struct AppConfig {
|
||||
pub api_base_url: Option<String>,
|
||||
pub debug_mode: bool,
|
||||
#[serde(default)]
|
||||
pub scene_interactivity_hotkey: SceneInteractivityHotkey,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, PartialEq, Eq, Type)]
|
||||
pub struct SceneInteractivityHotkey {
|
||||
#[serde(default)]
|
||||
pub modifiers: Vec<SceneInteractivityModifier>,
|
||||
#[serde(default)]
|
||||
pub key: Option<SceneInteractivityKey>,
|
||||
}
|
||||
|
||||
impl SceneInteractivityHotkey {
|
||||
pub fn normalized(mut self) -> Self {
|
||||
self.modifiers.sort_unstable();
|
||||
self.modifiers.dedup();
|
||||
|
||||
if self.modifiers.is_empty() {
|
||||
return Self::default();
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn from_legacy(value: LegacySceneInteractivityHotkey) -> Self {
|
||||
let modifiers = match value {
|
||||
LegacySceneInteractivityHotkey::CmdAlt => {
|
||||
vec![
|
||||
SceneInteractivityModifier::Cmd,
|
||||
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],
|
||||
};
|
||||
|
||||
Self {
|
||||
modifiers,
|
||||
key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SceneInteractivityHotkey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct SceneInteractivityHotkeyV2 {
|
||||
#[serde(default)]
|
||||
modifiers: Vec<SceneInteractivityModifier>,
|
||||
#[serde(default)]
|
||||
key: Option<SceneInteractivityKey>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum SceneInteractivityHotkeySerde {
|
||||
V2(SceneInteractivityHotkeyV2),
|
||||
Legacy(LegacySceneInteractivityHotkey),
|
||||
}
|
||||
|
||||
let value = SceneInteractivityHotkeySerde::deserialize(deserializer)?;
|
||||
let normalized = match value {
|
||||
SceneInteractivityHotkeySerde::V2(v2) => Self {
|
||||
modifiers: v2.modifiers,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
||||
@@ -6,7 +6,7 @@ use url::Url;
|
||||
|
||||
use crate::get_app_handle;
|
||||
|
||||
use super::{AppConfig, ClientConfigError};
|
||||
use super::{AppConfig, ClientConfigError, SceneInteractivityHotkey};
|
||||
|
||||
const CONFIG_FILENAME: &str = "client_config.json";
|
||||
const DEFAULT_API_BASE_URL: &str = "https://api.friendolls.adamcv.com";
|
||||
@@ -48,6 +48,8 @@ fn sanitize(mut config: AppConfig) -> AppConfig {
|
||||
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
|
||||
.map(|v| strip_trailing_slash(&v));
|
||||
|
||||
config.scene_interactivity_hotkey = config.scene_interactivity_hotkey.normalized();
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
@@ -55,6 +57,7 @@ pub fn default_app_config() -> AppConfig {
|
||||
AppConfig {
|
||||
api_base_url: Some(DEFAULT_API_BASE_URL.to_string()),
|
||||
debug_mode: false,
|
||||
scene_interactivity_hotkey: SceneInteractivityHotkey::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,16 @@ use tauri::Manager;
|
||||
use tauri_specta::Event as _;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{get_app_handle, services::app_events::SceneInteractiveChanged};
|
||||
use crate::{
|
||||
get_app_handle, lock_r,
|
||||
services::{
|
||||
app_events::SceneInteractiveChanged,
|
||||
client_config::{
|
||||
SceneInteractivityHotkey, SceneInteractivityKey, SceneInteractivityModifier,
|
||||
},
|
||||
},
|
||||
state::FDOLL,
|
||||
};
|
||||
|
||||
use super::windows::SCENE_WINDOW_LABEL;
|
||||
|
||||
@@ -29,6 +38,135 @@ fn scene_interactive_state() -> Arc<AtomicBool> {
|
||||
.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() {
|
||||
MODIFIER_LISTENER_INIT.get_or_init(|| {
|
||||
let state = scene_interactive_state();
|
||||
@@ -51,7 +189,7 @@ pub(crate) fn start_scene_modifier_listener() {
|
||||
MessageDialogBuilder::new(
|
||||
app_handle.dialog().clone(),
|
||||
"Missing Permissions",
|
||||
"Friendolls needs Accessibility permissions to detect the Alt key for interactivity. Please grant permissions in System Settings -> Privacy & Security -> Accessibility and restart the app.",
|
||||
"Friendolls needs Accessibility permissions to detect your scene interactivity hotkey. Please grant permissions in System Settings -> Privacy & Security -> Accessibility and restart the app.",
|
||||
)
|
||||
.kind(MessageDialogKind::Warning)
|
||||
.show(|_| {});
|
||||
@@ -66,9 +204,11 @@ pub(crate) fn start_scene_modifier_listener() {
|
||||
|
||||
loop {
|
||||
let keys = device_state.get_keys();
|
||||
let keys_interactive =
|
||||
(keys.contains(&Keycode::LAlt) || keys.contains(&Keycode::RAlt))
|
||||
|| keys.contains(&Keycode::Command);
|
||||
let hotkey = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard.app_config.scene_interactivity_hotkey.clone()
|
||||
};
|
||||
let keys_interactive = is_hotkey_active(&keys, hotkey);
|
||||
|
||||
let menus_open = {
|
||||
if let Ok(menus) = get_open_pet_menus().lock() {
|
||||
|
||||
@@ -175,7 +175,7 @@ userStatusChanged: "user-status-changed"
|
||||
/** user-defined types **/
|
||||
|
||||
export type ActiveDollSpriteChanged = string | null
|
||||
export type AppConfig = { api_base_url: string | null; debug_mode: boolean }
|
||||
export type AppConfig = { api_base_url: string | null; debug_mode: boolean; scene_interactivity_hotkey?: SceneInteractivityHotkey }
|
||||
export type AppDataRefreshed = UserData
|
||||
export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled"
|
||||
export type AuthFlowUpdated = AuthFlowUpdatedPayload
|
||||
@@ -216,6 +216,9 @@ export type ModuleMetadata = { id: string; name: string; version: string; descri
|
||||
export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null }
|
||||
export type SceneData = { display: DisplayData; grid_size: number }
|
||||
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 SendInteractionDto = { recipientUserId: string; content: string; type: string }
|
||||
export type SetInteractionOverlay = boolean
|
||||
|
||||
@@ -1,9 +1,267 @@
|
||||
<script lang="ts">
|
||||
import { commands } from "$lib/bindings";
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
commands,
|
||||
type AppConfig,
|
||||
type SceneInteractivityHotkey,
|
||||
type SceneInteractivityKey,
|
||||
type SceneInteractivityModifier,
|
||||
} from "$lib/bindings";
|
||||
import { appData } from "../../../events/app-data";
|
||||
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 appConfig: AppConfig | null = null;
|
||||
let hotkey = { ...DEFAULT_HOTKEY };
|
||||
let captureMode = false;
|
||||
let capturePreviewLabel = "";
|
||||
let hotkeyInput: HTMLInputElement | null = null;
|
||||
let hotkeyLabel = "";
|
||||
let hotkeyError = "";
|
||||
let hotkeySuccess = "";
|
||||
let hotkeyDirty = false;
|
||||
let hotkeySaving = false;
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const config = await commands.getClientConfig();
|
||||
appConfig = config;
|
||||
hotkey = normalizeHotkey(config.scene_interactivity_hotkey);
|
||||
} catch (error) {
|
||||
console.error("Failed to load client config", error);
|
||||
hotkeyError = "Failed to load current hotkey settings.";
|
||||
}
|
||||
};
|
||||
|
||||
async function handleSignOut() {
|
||||
if (signingOut) return;
|
||||
@@ -23,11 +281,153 @@
|
||||
console.error("Failed to open client config", error);
|
||||
}
|
||||
};
|
||||
|
||||
const beginCapture = () => {
|
||||
captureMode = true;
|
||||
capturePreviewLabel = "";
|
||||
hotkeyError = "";
|
||||
hotkeySuccess = "";
|
||||
setTimeout(() => hotkeyInput?.focus(), 0);
|
||||
};
|
||||
|
||||
const stopCapture = () => {
|
||||
captureMode = false;
|
||||
capturePreviewLabel = "";
|
||||
saveHotkey();
|
||||
};
|
||||
|
||||
const saveHotkey = async () => {
|
||||
if (!appConfig || hotkeySaving) return;
|
||||
hotkeySaving = true;
|
||||
hotkeyError = "";
|
||||
hotkeySuccess = "";
|
||||
|
||||
try {
|
||||
const nextConfig: AppConfig = {
|
||||
...appConfig,
|
||||
scene_interactivity_hotkey: hotkey,
|
||||
};
|
||||
await commands.saveClientConfig(nextConfig);
|
||||
appConfig = nextConfig;
|
||||
hotkeySuccess = "Hotkey saved.";
|
||||
} catch (error) {
|
||||
console.error("Failed to save hotkey", error);
|
||||
hotkeyError = "Failed to save hotkey.";
|
||||
} finally {
|
||||
hotkeySaving = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onHotkeyCapture = async (event: KeyboardEvent) => {
|
||||
if (!captureMode || hotkeySaving) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === "Escape") {
|
||||
stopCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
const modifiers = getModifiersFromEvent(event);
|
||||
const key = CODE_TO_KEY[event.code] ?? null;
|
||||
const modifierOnlyPress = MODIFIER_CODES.has(event.code);
|
||||
|
||||
if (modifiers.length === 0) {
|
||||
capturePreviewLabel = "";
|
||||
hotkeyError = "Hotkey must include at least one modifier key.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (modifierOnlyPress) {
|
||||
const nextHotkey = normalizeHotkey({
|
||||
modifiers,
|
||||
key: null,
|
||||
});
|
||||
hotkey = nextHotkey;
|
||||
capturePreviewLabel = formatHotkeyLabel(nextHotkey);
|
||||
hotkeySuccess = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
hotkeyError = "That key is not supported for hotkey combos yet.";
|
||||
return;
|
||||
}
|
||||
|
||||
const nextHotkey = normalizeHotkey({ modifiers, key });
|
||||
hotkey = nextHotkey;
|
||||
capturePreviewLabel = formatHotkeyLabel(nextHotkey);
|
||||
hotkeySuccess = "";
|
||||
};
|
||||
|
||||
$: hotkeyLabel = formatHotkeyLabel(hotkey);
|
||||
$: hotkeyDirty = appConfig
|
||||
? !hotkeysEqual(hotkey, appConfig.scene_interactivity_hotkey)
|
||||
: false;
|
||||
|
||||
onMount(() => {
|
||||
loadConfig();
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
void onHotkeyCapture(event);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeydown, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeydown, true);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="size-full flex flex-col justify-between">
|
||||
<div class="flex flex-col gap-4 max-w-md">
|
||||
<p>{$appData?.user?.name}'s preferences</p>
|
||||
<label class="flex flex-col gap-2">
|
||||
<span class="text-sm">Scene Interactivity Hotkey</span>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<input
|
||||
class="input input-bordered flex-1"
|
||||
readonly
|
||||
bind:this={hotkeyInput}
|
||||
value={captureMode
|
||||
? capturePreviewLabel || "Press your shortcut..."
|
||||
: hotkeyLabel}
|
||||
/>
|
||||
{#if captureMode}
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
type="button"
|
||||
disabled={hotkeySaving}
|
||||
onclick={stopCapture}
|
||||
>
|
||||
{hotkeySaving ? "Saving..." : "Stop Record"}
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-2">
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
type="button"
|
||||
disabled={!appConfig || hotkeySaving}
|
||||
onclick={beginCapture}
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-xs opacity-70">
|
||||
Requires at least one modifier (Cmd, Alt, Ctrl, Shift). Press Escape to
|
||||
cancel recording.
|
||||
</span>
|
||||
{#if hotkeyError}
|
||||
<span class="text-xs text-error">{hotkeyError}</span>
|
||||
{/if}
|
||||
{#if hotkeySuccess}
|
||||
<span class="text-xs text-success">{hotkeySuccess}</span>
|
||||
{/if}
|
||||
</label>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button
|
||||
class="btn"
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { commands, type AppConfig } from "$lib/bindings";
|
||||
import {
|
||||
commands,
|
||||
type AppConfig,
|
||||
type SceneInteractivityHotkey,
|
||||
} 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 = {
|
||||
api_base_url: "",
|
||||
debug_mode: false,
|
||||
scene_interactivity_hotkey: { ...DEFAULT_HOTKEY },
|
||||
};
|
||||
|
||||
let saving = false;
|
||||
@@ -18,6 +44,9 @@
|
||||
form = {
|
||||
api_base_url: config.api_base_url ?? "",
|
||||
debug_mode: config.debug_mode ?? false,
|
||||
scene_interactivity_hotkey: normalizeHotkey(
|
||||
config.scene_interactivity_hotkey,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
errorMessage = `Failed to load config: ${err}`;
|
||||
@@ -56,6 +85,9 @@
|
||||
await commands.saveClientConfig({
|
||||
api_base_url: form.api_base_url?.trim() || null,
|
||||
debug_mode: form.debug_mode,
|
||||
scene_interactivity_hotkey: normalizeHotkey(
|
||||
form.scene_interactivity_hotkey,
|
||||
),
|
||||
});
|
||||
|
||||
successMessage = "Success. Restart to apply changes.";
|
||||
@@ -103,6 +135,7 @@
|
||||
bind:checked={form.debug_mode}
|
||||
/>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
{#if errorMessage}
|
||||
|
||||
Reference in New Issue
Block a user