From 8e200c41aa3b49c6112adae830bbd6d8a9dcfa1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 6 Jun 2026 12:14:37 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=ED=82=A4=EC=9D=8C=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=EC=9E=A5=EC=B9=98=20=EC=84=A0=ED=83=9D(=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=9E=A5=EC=B9=98/ASIO)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows ASIO 출력 백엔드를 asio-backend feature로 추가 cfg 게이팅으로 macOS/Linux 빌드는 영향 없음 - 설정에 키음 출력 장치 드롭다운 (기본 장치 / 감지된 ASIO 드라이버) - 선택값 store 영속화 및 부팅 시 복원, 실패 시 기본 장치 fallback - 출력 백엔드 커맨드 3개 추가, 로케일 5종 키 반영 --- .gitignore | 2 + src-tauri/Cargo.lock | 112 ++++++- src-tauri/Cargo.toml | 4 + src-tauri/gen/schemas/acl-manifests.json | 2 +- src-tauri/permissions/dmnote-allow-all.json | 3 + src-tauri/src/audio/engine.rs | 305 +++++++++++++++++++- src-tauri/src/audio/mod.rs | 5 +- src-tauri/src/commands/keys/key_sound.rs | 22 +- src-tauri/src/main.rs | 3 + src-tauri/src/models/mod.rs | 14 + src-tauri/src/state/app_state.rs | 55 +++- src/renderer/api/modules/resourceApi.ts | 25 ++ src/renderer/components/main/Settings.tsx | 88 +++++- src/renderer/locales/en.json | 2 + src/renderer/locales/ko.json | 2 + src/renderer/locales/ru.json | 2 + src/renderer/locales/zh-Hant.json | 2 + src/renderer/locales/zh-cn.json | 2 + 18 files changed, 635 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 236e77f0..f1798bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ src-tauri/webview2-fixed-runtime/* .mcp.json mcp.json +tasks/ + # --- C# / Visual Studio (UWP) --- [Bb]in/ [Oo]bj/ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2521d4fb..55115d2d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -104,6 +104,20 @@ dependencies = [ "zbus 4.4.0", ] +[[package]] +name = "asio-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826194e1612938c9be09b78b58323fbb2e326de3d491b4230186cf6e832d8ded" +dependencies = [ + "bindgen", + "cc", + "num-derive", + "num-traits", + "parse_cfg", + "walkdir", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -310,6 +324,26 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.111", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -515,6 +549,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.7.3" @@ -570,6 +613,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + [[package]] name = "cocoa" version = "0.22.0" @@ -754,6 +808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" dependencies = [ "alsa", + "asio-sys", "coreaudio-rs", "dasp_sample", "jni", @@ -1102,6 +1157,7 @@ version = "1.6.0" dependencies = [ "anyhow", "base64 0.22.1", + "cpal", "dirs-next", "fern", "futures-util", @@ -2275,6 +2331,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -2437,7 +2502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -2457,6 +2522,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libredox" version = "0.1.11" @@ -2601,6 +2676,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2739,6 +2820,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "6.1.1" @@ -3225,6 +3316,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "parse_cfg" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905787a434a2c721408e7c9a252e85f3d93ca0f118a5283022636c0e05a7ea49" +dependencies = [ + "nom", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -3499,6 +3599,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 91504f29..e0bb459f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -4,6 +4,9 @@ version = "1.6.0" edition = "2021" default-run = "dm-note" +[features] +asio-backend = ["dep:cpal"] + [build-dependencies] tauri-build = { version = "2.4.1", features = [] } walkdir = "2.5" @@ -40,6 +43,7 @@ tokio-tungstenite = "0.24" futures-util = "0.3" local-ip-address = "0.6.10" [target."cfg(windows)".dependencies] +cpal = { version = "0.17.3", optional = true, default-features = false, features = ["asio"] } windows = { version = "0.61.3", features = [ "Win32_Foundation", "Win32_UI_WindowsAndMessaging", diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index 92b77a39..1e324afb 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"__app-acl__":{"default_permission":null,"permissions":{"dmnote-allow-all":{"identifier":"dmnote-allow-all","description":"Full DM Note command access for renderer","commands":{"allow":["app_auto_update","app_bootstrap","app_open_external","app_quit","app_restart","counter_animation_create","counter_animation_delete","counter_animation_list","counter_animation_update","css_get","css_get_use","css_load","css_reset","css_set_content","css_tab_clear","css_tab_get","css_tab_get_all","css_tab_load","css_tab_set","css_tab_toggle","css_toggle","custom_tabs_create","custom_tabs_delete","custom_tabs_list","custom_tabs_restore","custom_tabs_select","font_load","get_cursor_settings","graph_positions_get","graph_positions_update","image_load","js_get","js_get_use","js_load","js_reload","js_remove_plugin","js_reset","js_set_content","js_set_plugin_enabled","js_toggle","key_sound_get_status","key_sound_load_soundpack","key_sound_set_enabled","key_sound_set_latency_logging","key_sound_set_volume","key_sound_unload_soundpack","keys_get","keys_get_counters","keys_reset_all","keys_reset_counters","keys_reset_counters_mode","keys_reset_mode","keys_reset_single_counter","keys_set_counters","keys_set_mode","keys_update","layer_groups_get","layer_groups_update","note_tab_clear","note_tab_get","note_tab_get_all","note_tab_set","obs_regenerate_token","obs_start","obs_status","obs_stop","overlay_get","overlay_resize","overlay_set_anchor","overlay_set_lock","overlay_set_visible","plugin_bridge_send","plugin_bridge_send_to","plugin_storage_clear","plugin_storage_clear_by_prefix","plugin_storage_get","plugin_storage_has_data","plugin_storage_keys","plugin_storage_remove","plugin_storage_set","positions_get","positions_update","preset_load","preset_load_tab","preset_save","preset_save_tab","raw_input_subscribe","raw_input_unsubscribe","settings_get","settings_update","sound_delete","sound_list","sound_load","sound_load_original","sound_save_processed_wav","sound_set_enabled","sound_update_processed_wav","stat_positions_get","stat_positions_update","window_close","window_minimize","window_open_devtools_all","window_show_main"],"deny":[]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"__app-acl__":{"default_permission":null,"permissions":{"dmnote-allow-all":{"identifier":"dmnote-allow-all","description":"Full DM Note command access for renderer","commands":{"allow":["app_auto_update","app_bootstrap","app_open_external","app_quit","app_restart","counter_animation_create","counter_animation_delete","counter_animation_list","counter_animation_update","css_get","css_get_use","css_load","css_reset","css_set_content","css_tab_clear","css_tab_get","css_tab_get_all","css_tab_load","css_tab_set","css_tab_toggle","css_toggle","custom_tabs_create","custom_tabs_delete","custom_tabs_list","custom_tabs_restore","custom_tabs_select","font_load","get_cursor_settings","graph_positions_get","graph_positions_update","image_load","js_get","js_get_use","js_load","js_reload","js_remove_plugin","js_reset","js_set_content","js_set_plugin_enabled","js_toggle","key_sound_get_output_state","key_sound_get_status","key_sound_list_output_devices","key_sound_load_soundpack","key_sound_set_enabled","key_sound_set_latency_logging","key_sound_set_output_backend","key_sound_set_volume","key_sound_unload_soundpack","keys_get","keys_get_counters","keys_reset_all","keys_reset_counters","keys_reset_counters_mode","keys_reset_mode","keys_reset_single_counter","keys_set_counters","keys_set_mode","keys_update","layer_groups_get","layer_groups_update","note_tab_clear","note_tab_get","note_tab_get_all","note_tab_set","obs_regenerate_token","obs_start","obs_status","obs_stop","overlay_get","overlay_resize","overlay_set_anchor","overlay_set_lock","overlay_set_visible","plugin_bridge_send","plugin_bridge_send_to","plugin_storage_clear","plugin_storage_clear_by_prefix","plugin_storage_get","plugin_storage_has_data","plugin_storage_keys","plugin_storage_remove","plugin_storage_set","positions_get","positions_update","preset_load","preset_load_tab","preset_save","preset_save_tab","raw_input_subscribe","raw_input_unsubscribe","settings_get","settings_update","sound_delete","sound_list","sound_load","sound_load_original","sound_save_processed_wav","sound_set_enabled","sound_update_processed_wav","stat_positions_get","stat_positions_update","window_close","window_minimize","window_open_devtools_all","window_show_main"],"deny":[]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/src-tauri/permissions/dmnote-allow-all.json b/src-tauri/permissions/dmnote-allow-all.json index 38c49411..0f2de797 100644 --- a/src-tauri/permissions/dmnote-allow-all.json +++ b/src-tauri/permissions/dmnote-allow-all.json @@ -46,10 +46,13 @@ "js_set_content", "js_set_plugin_enabled", "js_toggle", + "key_sound_get_output_state", "key_sound_get_status", + "key_sound_list_output_devices", "key_sound_load_soundpack", "key_sound_set_enabled", "key_sound_set_latency_logging", + "key_sound_set_output_backend", "key_sound_set_volume", "key_sound_unload_soundpack", "keys_get", diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs index b2cc07db..39193d99 100644 --- a/src-tauri/src/audio/engine.rs +++ b/src-tauri/src/audio/engine.rs @@ -12,6 +12,7 @@ use std::{ thread, time::{Duration, Instant}, }; +use std::{error::Error, fmt}; use anyhow::{Context, Result}; #[cfg(debug_assertions)] @@ -175,9 +176,62 @@ impl Default for KeySoundStatus { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum KeySoundOutputBackend { + #[default] + DefaultDevice, + Asio { + driver_name: String, + }, +} + +impl KeySoundOutputBackend { + fn normalized(self) -> Self { + match self { + Self::DefaultDevice => Self::DefaultDevice, + Self::Asio { driver_name } => Self::Asio { + driver_name: driver_name.trim().to_string(), + }, + } + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KeySoundOutputState { + pub requested: KeySoundOutputBackend, + pub effective: KeySoundOutputBackend, + pub error: Option, + pub asio_available: bool, +} + +impl Default for KeySoundOutputState { + fn default() -> Self { + Self { + requested: KeySoundOutputBackend::DefaultDevice, + effective: KeySoundOutputBackend::DefaultDevice, + error: None, + asio_available: asio_backend_available(), + } + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KeySoundOutputDevices { + pub default_device: bool, + pub asio: Vec, +} + #[derive(Debug, Clone)] struct KeySoundRuntimeState { status: KeySoundStatus, + output_state: KeySoundOutputState, soundpack: Option>, } @@ -200,6 +254,10 @@ enum AudioCommand { InvalidateFileCache { path: String, }, + SetOutputBackend { + backend: KeySoundOutputBackend, + reply: Sender, + }, } #[derive(Debug, Clone)] @@ -225,6 +283,7 @@ impl KeySoundEngine { let (sender, receiver) = mpsc::channel(); let state = Arc::new(RwLock::new(KeySoundRuntimeState { status: KeySoundStatus::default(), + output_state: KeySoundOutputState::default(), soundpack: None, })); let state_for_thread = state.clone(); @@ -238,6 +297,33 @@ impl KeySoundEngine { self.state.read().status.clone() } + pub fn output_state(&self) -> KeySoundOutputState { + self.state.read().output_state.clone() + } + + pub fn list_output_devices(&self) -> KeySoundOutputDevices { + KeySoundOutputDevices { + default_device: true, + asio: list_asio_drivers(), + } + } + + pub fn set_output_backend(&self, backend: KeySoundOutputBackend) -> KeySoundOutputState { + let (reply_tx, reply_rx) = mpsc::channel(); + if self + .sender + .send(AudioCommand::SetOutputBackend { + backend, + reply: reply_tx, + }) + .is_err() + { + return self.output_state(); + } + + reply_rx.recv().unwrap_or_else(|_| self.output_state()) + } + pub fn set_enabled(&self, enabled: bool) -> KeySoundStatus { { let mut guard = self.state.write(); @@ -358,7 +444,11 @@ fn audio_thread(receiver: Receiver, state: Arc> = HashMap::new(); #[cfg(debug_assertions)] let mut latency_summary = LatencySummary::default(); @@ -387,6 +477,12 @@ fn audio_thread(receiver: Receiver, state: Arc { file_cache.remove(&path); } + AudioCommand::SetOutputBackend { backend, reply } => { + let output_state = switch_output_backend(backend, &mut stream_handler); + requested_backend = output_state.requested.clone(); + state.write().output_state = output_state.clone(); + let _ = reply.send(output_state); + } AudioCommand::PlayLabels { labels, queued_at, @@ -408,7 +504,13 @@ fn audio_thread(receiver: Receiver, state: Arc, state: Arc, } -fn open_audio_sink() -> Option { +#[derive(Debug)] +enum AudioSinkOpenError { + AsioUnavailableBuild, + #[cfg_attr(not(all(windows, feature = "asio-backend")), allow(dead_code))] + AsioDeviceNotFound, + OpenFailed(anyhow::Error), +} + +type AudioSinkResult = std::result::Result; + +impl fmt::Display for AudioSinkOpenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AsioUnavailableBuild => write!(f, "ASIO 미지원 빌드"), + Self::AsioDeviceNotFound => write!(f, "ASIO 장치를 찾을 수 없습니다"), + Self::OpenFailed(err) => write!(f, "{err:#}"), + } + } +} + +impl Error for AudioSinkOpenError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::OpenFailed(err) => Some(err.as_ref()), + _ => None, + } + } +} + +fn asio_backend_available() -> bool { + cfg!(all(windows, feature = "asio-backend")) +} + +fn switch_output_backend( + backend: KeySoundOutputBackend, + stream_handler: &mut Option, +) -> KeySoundOutputState { + let requested = backend.normalized(); + *stream_handler = None; + + match open_audio_sink(&requested) { + Ok(handler) => { + *stream_handler = Some(handler); + KeySoundOutputState { + requested: requested.clone(), + effective: requested, + error: None, + asio_available: asio_backend_available(), + } + } + Err(err) => { + warn!("[KeySound] failed to open output backend: {err}"); + match requested { + KeySoundOutputBackend::DefaultDevice => KeySoundOutputState { + requested: KeySoundOutputBackend::DefaultDevice, + effective: KeySoundOutputBackend::DefaultDevice, + error: Some("기본 출력 장치를 열 수 없습니다".to_string()), + asio_available: asio_backend_available(), + }, + KeySoundOutputBackend::Asio { driver_name } => { + if let Err(default_err) = open_audio_sink(&KeySoundOutputBackend::DefaultDevice) + .map(|handler| { + *stream_handler = Some(handler); + }) + { + warn!("[KeySound] failed to fallback to default output: {default_err}"); + } + + KeySoundOutputState { + requested: KeySoundOutputBackend::Asio { driver_name }, + effective: KeySoundOutputBackend::DefaultDevice, + error: Some(asio_output_error_message(&err).to_string()), + asio_available: asio_backend_available(), + } + } + } + } + } +} + +fn asio_output_error_message(err: &AudioSinkOpenError) -> &'static str { + match err { + AudioSinkOpenError::AsioUnavailableBuild => "ASIO 미지원 빌드", + AudioSinkOpenError::AsioDeviceNotFound => "ASIO 장치를 찾을 수 없습니다", + AudioSinkOpenError::OpenFailed(_) => "ASIO 장치를 열 수 없어 기본 출력으로 재생합니다", + } +} + +fn open_audio_sink(backend: &KeySoundOutputBackend) -> AudioSinkResult { + match backend { + KeySoundOutputBackend::DefaultDevice => open_default_audio_sink(), + KeySoundOutputBackend::Asio { driver_name } => open_asio_audio_sink(driver_name), + } +} + +fn open_default_audio_sink() -> AudioSinkResult { let error = Arc::new(AtomicBool::new(false)); let err_flag = Arc::clone(&error); @@ -558,22 +761,110 @@ fn open_audio_sink() -> Option { }) .open_sink_or_fallback() }) - .ok()?; + .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))?; + + Ok(StreamHandler { sink, error }) +} + +#[cfg(all(windows, feature = "asio-backend"))] +fn open_asio_audio_sink(driver_name: &str) -> AudioSinkResult { + use cpal::traits::{DeviceTrait, HostTrait}; + + let driver_name = driver_name.trim(); + if driver_name.is_empty() { + return Err(AudioSinkOpenError::AsioDeviceNotFound); + } + + let host = cpal::host_from_id(cpal::HostId::Asio) + .map_err(|_| AudioSinkOpenError::AsioDeviceNotFound)?; + let devices = host + .output_devices() + .map_err(|_| AudioSinkOpenError::AsioDeviceNotFound)?; + + for device in devices { + let name = match device.description() { + Ok(description) => description.name().to_string(), + Err(err) => { + warn!("[KeySound] failed to read ASIO device name: {err}"); + continue; + } + }; + if name == driver_name { + return open_device_audio_sink(device); + } + } + + Err(AudioSinkOpenError::AsioDeviceNotFound) +} + +#[cfg(not(all(windows, feature = "asio-backend")))] +fn open_asio_audio_sink(_driver_name: &str) -> AudioSinkResult { + Err(AudioSinkOpenError::AsioUnavailableBuild) +} + +#[cfg(all(windows, feature = "asio-backend"))] +fn open_device_audio_sink(device: cpal::Device) -> AudioSinkResult { + let error = Arc::new(AtomicBool::new(false)); + let err_flag = Arc::clone(&error); + + let sink = DeviceSinkBuilder::from_device(device) + .and_then(|builder| { + builder + .with_error_callback(move |err| { + warn!("[KeySound] ASIO stream error: {err}"); + err_flag.store(true, Ordering::Release); + }) + .open_sink_or_fallback() + }) + .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))?; + + Ok(StreamHandler { sink, error }) +} + +#[cfg(all(windows, feature = "asio-backend"))] +fn list_asio_drivers() -> Vec { + use cpal::traits::{DeviceTrait, HostTrait}; + + let Ok(host) = cpal::host_from_id(cpal::HostId::Asio) else { + return Vec::new(); + }; + let Ok(devices) = host.output_devices() else { + return Vec::new(); + }; + + let mut names: Vec = devices + .filter_map(|device| { + device + .description() + .ok() + .map(|description| description.name().trim().to_string()) + .filter(|name| !name.is_empty()) + }) + .collect(); + names.sort(); + names.dedup(); + names +} - Some(StreamHandler { sink, error }) +#[cfg(not(all(windows, feature = "asio-backend")))] +fn list_asio_drivers() -> Vec { + Vec::new() } fn play_on_stream( stream_handler: &mut Option, source: AudioSource, volume: f32, + requested_backend: &KeySoundOutputBackend, + state: &Arc>, ) -> bool { // 장치 에러 또는 스트림 없음 → 재연결 if stream_handler .as_ref() .is_none_or(|h| h.error.load(Ordering::Acquire)) { - *stream_handler = open_audio_sink(); + let output_state = switch_output_backend(requested_backend.clone(), stream_handler); + state.write().output_state = output_state; } let Some(handler) = stream_handler.as_ref() else { diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index 990a9407..95eaad14 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -2,4 +2,7 @@ pub mod engine; #[cfg(debug_assertions)] pub use engine::KeySoundDispatchTrace; -pub use engine::{KeySoundEngine, KeySoundStatus}; +pub use engine::{ + KeySoundEngine, KeySoundOutputBackend, KeySoundOutputDevices, KeySoundOutputState, + KeySoundStatus, +}; diff --git a/src-tauri/src/commands/keys/key_sound.rs b/src-tauri/src/commands/keys/key_sound.rs index 71f9ef2f..11b04c07 100644 --- a/src-tauri/src/commands/keys/key_sound.rs +++ b/src-tauri/src/commands/keys/key_sound.rs @@ -1,7 +1,7 @@ use tauri::State; use crate::{ - audio::KeySoundStatus, + audio::{KeySoundOutputBackend, KeySoundOutputDevices, KeySoundOutputState, KeySoundStatus}, errors::{CmdResult, CommandError}, state::AppState, }; @@ -51,3 +51,23 @@ pub fn key_sound_set_latency_logging( } Ok(state.key_sound_set_latency_logging(enabled)) } + +#[tauri::command] +pub fn key_sound_list_output_devices( + state: State<'_, AppState>, +) -> CmdResult { + Ok(state.key_sound_list_output_devices()) +} + +#[tauri::command] +pub fn key_sound_set_output_backend( + state: State<'_, AppState>, + backend: KeySoundOutputBackend, +) -> CmdResult { + Ok(state.key_sound_set_output_backend(backend)) +} + +#[tauri::command] +pub fn key_sound_get_output_state(state: State<'_, AppState>) -> CmdResult { + Ok(state.key_sound_get_output_state()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 43caee80..40be7e3e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -215,6 +215,9 @@ fn main() { commands::keys::key_sound::key_sound_load_soundpack, commands::keys::key_sound::key_sound_unload_soundpack, commands::keys::key_sound::key_sound_set_latency_logging, + commands::keys::key_sound::key_sound_list_output_devices, + commands::keys::key_sound::key_sound_set_output_backend, + commands::keys::key_sound::key_sound_get_output_state, commands::keys::sound::sound_load, commands::keys::sound::sound_list, commands::keys::sound::sound_set_enabled, diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 81db2f5b..3ba5d80d 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -95,6 +95,17 @@ impl Default for SoundLibraryEntry { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum KeySoundOutputBackendPersist { + DefaultDevice, + Asio { driver_name: String }, +} + // 직렬화 형식: // - Solid: JSON 문자열 (예: "#FF00FF") // - Gradient: 명시적 type 필드를 포함한 객체 { type: "gradient", top, bottom } @@ -1261,6 +1272,8 @@ pub struct AppStoreData { /// 사운드 라이브러리 메타데이터 (키: 절대 경로, 값: 메타데이터) #[serde(default)] pub sound_library: HashMap, + #[serde(default)] + pub key_sound_output_backend: Option, /// OBS 모드 활성화 여부 #[serde(default)] pub obs_mode_enabled: bool, @@ -1320,6 +1333,7 @@ impl Default for AppStoreData { grid_settings: GridSettings::default(), shortcuts: ShortcutsState::default(), sound_library: HashMap::new(), + key_sound_output_backend: None, obs_mode_enabled: false, obs_port: default_obs_port(), obs_token: None, diff --git a/src-tauri/src/state/app_state.rs b/src-tauri/src/state/app_state.rs index 49c14fe1..05d8aecf 100644 --- a/src-tauri/src/state/app_state.rs +++ b/src-tauri/src/state/app_state.rs @@ -28,12 +28,15 @@ use super::store::AppStore; #[cfg(debug_assertions)] use crate::audio::KeySoundDispatchTrace; use crate::{ - audio::{KeySoundEngine, KeySoundStatus}, + audio::{ + KeySoundEngine, KeySoundOutputBackend, KeySoundOutputDevices, KeySoundOutputState, + KeySoundStatus, + }, keyboard::KeyboardManager, models::{ overlay_resize_anchor_from_str, BootstrapOverlayState, BootstrapPayload, DefaultsPayload, - KeyCounterSettings, KeyCounters, KeyMappings, OverlayBounds, OverlayResizeAnchor, - SettingsDiff, SettingsState, + KeyCounterSettings, KeyCounters, KeyMappings, KeySoundOutputBackendPersist, OverlayBounds, + OverlayResizeAnchor, SettingsDiff, SettingsState, }, services::{css_watcher::CssWatcher, obs_bridge::ObsBridgeService, settings::SettingsService}, }; @@ -82,6 +85,12 @@ impl AppState { let key_counter_enabled = Arc::new(AtomicBool::new(snapshot.key_counter_enabled)); let active_keys = Arc::new(RwLock::new(HashSet::new())); let key_sound = Arc::new(KeySoundEngine::new()); + if let Some(backend) = snapshot.key_sound_output_backend.clone() { + let output_state = key_sound.set_output_backend(output_backend_from_persist(backend)); + if let Some(error) = output_state.error.as_ref() { + warn!("[KeySound] failed to restore output backend: {error}"); + } + } let obs_bridge = Arc::new(ObsBridgeService::new(env!("CARGO_PKG_VERSION"))); Ok(Self { @@ -1593,6 +1602,28 @@ impl AppState { self.key_sound.set_latency_logging(enabled) } + pub fn key_sound_list_output_devices(&self) -> KeySoundOutputDevices { + self.key_sound.list_output_devices() + } + + pub fn key_sound_set_output_backend( + &self, + backend: KeySoundOutputBackend, + ) -> KeySoundOutputState { + let output_state = self.key_sound.set_output_backend(backend); + let requested = output_state.requested.clone(); + if let Err(err) = self.store.update(|state| { + state.key_sound_output_backend = Some(output_backend_to_persist(requested.clone())); + }) { + warn!("[KeySound] failed to persist output backend: {err}"); + } + output_state + } + + pub fn key_sound_get_output_state(&self) -> KeySoundOutputState { + self.key_sound.output_state() + } + pub fn key_sound_latency_logging_available(&self) -> bool { self.key_sound.latency_logging_available() } @@ -1694,6 +1725,24 @@ impl Drop for AppState { } } +fn output_backend_from_persist(value: KeySoundOutputBackendPersist) -> KeySoundOutputBackend { + match value { + KeySoundOutputBackendPersist::DefaultDevice => KeySoundOutputBackend::DefaultDevice, + KeySoundOutputBackendPersist::Asio { driver_name } => { + KeySoundOutputBackend::Asio { driver_name } + } + } +} + +fn output_backend_to_persist(value: KeySoundOutputBackend) -> KeySoundOutputBackendPersist { + match value { + KeySoundOutputBackend::DefaultDevice => KeySoundOutputBackendPersist::DefaultDevice, + KeySoundOutputBackend::Asio { driver_name } => { + KeySoundOutputBackendPersist::Asio { driver_name } + } + } +} + fn attach_main_window_close_handler( window: WebviewWindow, overlay_force_close: Arc, diff --git a/src/renderer/api/modules/resourceApi.ts b/src/renderer/api/modules/resourceApi.ts index ba45692a..d51aa7e8 100644 --- a/src/renderer/api/modules/resourceApi.ts +++ b/src/renderer/api/modules/resourceApi.ts @@ -74,6 +74,31 @@ export const soundApi = { invoke('key_sound_set_latency_logging', { enabled }).then(() => undefined), }; +// 키음 출력 백엔드 (기본 장치 / ASIO) +export type KeySoundOutputBackend = + | { kind: 'defaultDevice' } + | { kind: 'asio'; driverName: string }; + +export interface KeySoundOutputDevices { + defaultDevice: true; + asio: string[]; +} + +export interface KeySoundOutputState { + requested: KeySoundOutputBackend; + effective: KeySoundOutputBackend; + error: string | null; + asioAvailable: boolean; +} + +export const keySoundOutputApi = { + listDevices: () => + invoke('key_sound_list_output_devices'), + getState: () => invoke('key_sound_get_output_state'), + setBackend: (backend: KeySoundOutputBackend) => + invoke('key_sound_set_output_backend', { backend }), +}; + export const counterAnimationApi = { list: () => invoke( diff --git a/src/renderer/components/main/Settings.tsx b/src/renderer/components/main/Settings.tsx index 0e4e00cc..03fdd198 100644 --- a/src/renderer/components/main/Settings.tsx +++ b/src/renderer/components/main/Settings.tsx @@ -28,6 +28,11 @@ import type { import type { JsPlugin } from '@src/types/plugin/js'; import type { KeyCounters } from '@src/types/key/keys'; import { obsApi } from '@api/modules/obsApi'; +import { keySoundOutputApi } from '@api/modules/resourceApi'; +import type { + KeySoundOutputBackend, + KeySoundOutputState, +} from '@api/modules/resourceApi'; import type { ObsStatus } from '@src/types/obs'; import { DEFAULT_OBS_PORT } from '@src/types/obs'; @@ -138,6 +143,43 @@ const Settings = ({ }); const obsTogglingRef = useRef(false); + // 키음 출력 백엔드 (기본 장치 / ASIO) + const [keySoundOutput, setKeySoundOutput] = + useState(null); + const [asioDrivers, setAsioDrivers] = useState([]); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const [devices, state] = await Promise.all([ + keySoundOutputApi.listDevices(), + keySoundOutputApi.getState(), + ]); + if (cancelled) return; + setAsioDrivers(devices.asio); + setKeySoundOutput(state); + } catch (error) { + console.error('Failed to load key sound output state', error); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const handleKeySoundOutputChange = async (val: string) => { + const backend: KeySoundOutputBackend = val.startsWith('asio:') + ? { kind: 'asio', driverName: val.slice('asio:'.length) } + : { kind: 'defaultDevice' }; + try { + const next = await keySoundOutputApi.setBackend(backend); + setKeySoundOutput(next); + } catch (error) { + console.error('Failed to set key sound output backend', error); + } + }; + // Lenis smooth scroll 적용 (전역 설정 사용) const { scrollContainerRef } = useLenis(); @@ -825,6 +867,46 @@ const Settings = ({ /> + {/* 키음 출력 설정 */} +
+
setHoveredKey('keySoundOutput')} + onMouseLeave={() => setHoveredKey(null)} + > +

+ {t('settings.keySoundOutput') || '키음 출력 장치'} +

+ ({ + value: `asio:${name}`, + label: `ASIO: ${name}`, + })), + ]} + value={ + keySoundOutput?.requested.kind === 'asio' + ? `asio:${keySoundOutput.requested.driverName}` + : 'defaultDevice' + } + onChange={handleKeySoundOutputChange} + placeholder={ + t('settings.keySoundOutputDefault') || '기본 출력 장치' + } + align="right" + /> +
+ {keySoundOutput?.error && ( +

+ {keySoundOutput.error} +

+ )} +
{/* 커스텀 CSS & JS 설정 */}
- {t(hoveredKey === 'obsMode' ? 'settings.obsGuide' : `settings.${hoveredKey}Desc`)} + {t( + hoveredKey === 'obsMode' + ? 'settings.obsGuide' + : `settings.${hoveredKey}Desc`, + )}
diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index 3e2caf42..4c1b7495 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -21,6 +21,8 @@ "noPlugins": "No plugins loaded yet", "removePlugin": "Remove plugin", "resizeAnchor": "Resize Anchor", + "keySoundOutput": "Key Sound Output", + "keySoundOutputDefault": "Default Output Device", "topLeft": "Top-Left", "bottomLeft": "Bottom-Left", "topRight": "Top-Right", diff --git a/src/renderer/locales/ko.json b/src/renderer/locales/ko.json index ac7efef3..f46939b1 100644 --- a/src/renderer/locales/ko.json +++ b/src/renderer/locales/ko.json @@ -21,6 +21,8 @@ "noPlugins": "불러온 플러그인이 없습니다", "removePlugin": "플러그인 삭제", "resizeAnchor": "리사이즈 기준점", + "keySoundOutput": "키음 출력 장치", + "keySoundOutputDefault": "기본 출력 장치", "topLeft": "좌상단", "bottomLeft": "좌하단", "topRight": "우상단", diff --git a/src/renderer/locales/ru.json b/src/renderer/locales/ru.json index 7739a473..bbf7c09e 100644 --- a/src/renderer/locales/ru.json +++ b/src/renderer/locales/ru.json @@ -21,6 +21,8 @@ "noPlugins": "Нет плагинов", "removePlugin": "Удалить плагин", "resizeAnchor": "Точка привязки", + "keySoundOutput": "Вывод звука клавиш", + "keySoundOutputDefault": "Устройство по умолчанию", "topLeft": "Верх-лево", "bottomLeft": "Низ-лево", "topRight": "Верх-право", diff --git a/src/renderer/locales/zh-Hant.json b/src/renderer/locales/zh-Hant.json index 407899ed..47fdfa32 100644 --- a/src/renderer/locales/zh-Hant.json +++ b/src/renderer/locales/zh-Hant.json @@ -21,6 +21,8 @@ "noPlugins": "尚未載入任何插件", "removePlugin": "移除插件", "resizeAnchor": "調整錨點大小", + "keySoundOutput": "按鍵音輸出裝置", + "keySoundOutputDefault": "預設輸出裝置", "topLeft": "左上", "bottomLeft": "左下", "topRight": "右上", diff --git a/src/renderer/locales/zh-cn.json b/src/renderer/locales/zh-cn.json index 9a52fc63..d4e2de03 100644 --- a/src/renderer/locales/zh-cn.json +++ b/src/renderer/locales/zh-cn.json @@ -21,6 +21,8 @@ "noPlugins": "尚未加载任何插件", "removePlugin": "移除插件", "resizeAnchor": "调整锚点大小", + "keySoundOutput": "按键音输出设备", + "keySoundOutputDefault": "默认输出设备", "topLeft": "左上", "bottomLeft": "左下", "topRight": "右上", From 0ffdcca066bd44c09f52436e5260858b6acbecdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 6 Jun 2026 13:33:54 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=ED=82=A4=EC=9D=8C=20ASIO=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=B2=84=ED=8D=BC=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다른 ASIO 앱(게임)과 동시 사용 시 버퍼 크기 불일치로 드라이버가 전역 재초기화되어 기존 앱이 무음되는 문제 해결. 사용자가 버퍼를 게임과 동일하게 맞출 수 있도록 설정을 추가한다. - KeySoundOutputBackend::Asio에 buffer_size 추가, 고정 버퍼로 스트림 오픈 - 샘플레이트는 드라이버 현재값 유지(ASIOSetSampleRate 회피) - 고정 버퍼 실패 시 자동 버퍼로 폴백 + 진단 로그 추가 - 설정에 ASIO 버퍼 크기 드롭다운(자동/64/128/256/512/1024) 추가 - 출력 백엔드 영속화에 buffer_size 반영, 로케일 5종 키 추가 --- src-tauri/src/audio/engine.rs | 99 ++++++++++++++++++----- src-tauri/src/models/mod.rs | 7 +- src-tauri/src/state/app_state.rs | 20 +++-- src/renderer/api/modules/resourceApi.ts | 2 +- src/renderer/components/main/Settings.tsx | 56 +++++++++++++ src/renderer/locales/en.json | 3 + src/renderer/locales/ko.json | 3 + src/renderer/locales/ru.json | 3 + src/renderer/locales/zh-Hant.json | 3 + src/renderer/locales/zh-cn.json | 3 + 10 files changed, 173 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs index 39193d99..a691d751 100644 --- a/src-tauri/src/audio/engine.rs +++ b/src-tauri/src/audio/engine.rs @@ -17,6 +17,8 @@ use std::{error::Error, fmt}; use anyhow::{Context, Result}; #[cfg(debug_assertions)] use log::debug; +#[cfg(all(windows, feature = "asio-backend"))] +use log::info; use log::warn; use parking_lot::RwLock; use rodio::{DeviceSinkBuilder, MixerDeviceSink, Source}; @@ -187,6 +189,10 @@ pub enum KeySoundOutputBackend { DefaultDevice, Asio { driver_name: String, + /// ASIO 버퍼 크기(프레임). None이면 자동(드라이버 preferred). + /// 다른 ASIO 앱(게임)과 공존하려면 그 앱과 동일한 버퍼로 맞춰야 함. + #[serde(default)] + buffer_size: Option, }, } @@ -194,8 +200,12 @@ impl KeySoundOutputBackend { fn normalized(self) -> Self { match self { Self::DefaultDevice => Self::DefaultDevice, - Self::Asio { driver_name } => Self::Asio { + Self::Asio { + driver_name, + buffer_size, + } => Self::Asio { driver_name: driver_name.trim().to_string(), + buffer_size: buffer_size.filter(|size| *size > 0), }, } } @@ -712,7 +722,10 @@ fn switch_output_backend( error: Some("기본 출력 장치를 열 수 없습니다".to_string()), asio_available: asio_backend_available(), }, - KeySoundOutputBackend::Asio { driver_name } => { + KeySoundOutputBackend::Asio { + driver_name, + buffer_size, + } => { if let Err(default_err) = open_audio_sink(&KeySoundOutputBackend::DefaultDevice) .map(|handler| { *stream_handler = Some(handler); @@ -722,7 +735,10 @@ fn switch_output_backend( } KeySoundOutputState { - requested: KeySoundOutputBackend::Asio { driver_name }, + requested: KeySoundOutputBackend::Asio { + driver_name, + buffer_size, + }, effective: KeySoundOutputBackend::DefaultDevice, error: Some(asio_output_error_message(&err).to_string()), asio_available: asio_backend_available(), @@ -744,7 +760,10 @@ fn asio_output_error_message(err: &AudioSinkOpenError) -> &'static str { fn open_audio_sink(backend: &KeySoundOutputBackend) -> AudioSinkResult { match backend { KeySoundOutputBackend::DefaultDevice => open_default_audio_sink(), - KeySoundOutputBackend::Asio { driver_name } => open_asio_audio_sink(driver_name), + KeySoundOutputBackend::Asio { + driver_name, + buffer_size, + } => open_asio_audio_sink(driver_name, *buffer_size), } } @@ -767,7 +786,10 @@ fn open_default_audio_sink() -> AudioSinkResult { } #[cfg(all(windows, feature = "asio-backend"))] -fn open_asio_audio_sink(driver_name: &str) -> AudioSinkResult { +fn open_asio_audio_sink( + driver_name: &str, + buffer_size: Option, +) -> AudioSinkResult { use cpal::traits::{DeviceTrait, HostTrait}; let driver_name = driver_name.trim(); @@ -790,7 +812,7 @@ fn open_asio_audio_sink(driver_name: &str) -> AudioSinkResult { } }; if name == driver_name { - return open_device_audio_sink(device); + return open_device_audio_sink(device, buffer_size); } } @@ -798,25 +820,66 @@ fn open_asio_audio_sink(driver_name: &str) -> AudioSinkResult { } #[cfg(not(all(windows, feature = "asio-backend")))] -fn open_asio_audio_sink(_driver_name: &str) -> AudioSinkResult { +fn open_asio_audio_sink( + _driver_name: &str, + _buffer_size: Option, +) -> AudioSinkResult { Err(AudioSinkOpenError::AsioUnavailableBuild) } #[cfg(all(windows, feature = "asio-backend"))] -fn open_device_audio_sink(device: cpal::Device) -> AudioSinkResult { +fn open_device_audio_sink( + device: cpal::Device, + buffer_size: Option, +) -> AudioSinkResult { + let buffer_size = buffer_size.filter(|frames| *frames > 0); + + // 고정 버퍼 지정 시 그 값 그대로 오픈 → 다른 ASIO 앱(게임)과 버퍼를 맞춰 공존. + // 드라이버가 해당 버퍼를 거부하면 자동 버퍼로 폴백. + if let Some(frames) = buffer_size { + match try_open_asio_sink(device.clone(), Some(frames)) { + Ok(handler) => return Ok(handler), + Err(err) => { + warn!("[KeySound] ASIO 고정 버퍼({frames}) 오픈 실패, 자동 버퍼로 재시도: {err:?}") + } + } + } + + try_open_asio_sink(device, None) +} + +#[cfg(all(windows, feature = "asio-backend"))] +fn try_open_asio_sink( + device: cpal::Device, + buffer_size: Option, +) -> AudioSinkResult { let error = Arc::new(AtomicBool::new(false)); let err_flag = Arc::clone(&error); - let sink = DeviceSinkBuilder::from_device(device) - .and_then(|builder| { - builder - .with_error_callback(move |err| { - warn!("[KeySound] ASIO stream error: {err}"); - err_flag.store(true, Ordering::Release); - }) - .open_sink_or_fallback() - }) - .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))?; + let builder = DeviceSinkBuilder::from_device(device) + .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))? + .with_error_callback(move |err| { + warn!("[KeySound] ASIO stream error: {err}"); + err_flag.store(true, Ordering::Release); + }); + + // 샘플레이트는 드라이버 현재값(default_output_config)을 그대로 사용 → ASIOSetSampleRate 회피. + // 버퍼만 고정 지정 시 적용. + let sink = match buffer_size { + Some(frames) => builder + .with_buffer_size(cpal::BufferSize::Fixed(frames)) + .open_stream(), + None => builder.open_sink_or_fallback(), + } + .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))?; + + let config = sink.config(); + info!( + "[KeySound] ASIO 스트림 오픈: 요청 버퍼={:?}, 적용 sample_rate={}Hz, buffer={:?}", + buffer_size, + config.sample_rate().get(), + config.buffer_size() + ); Ok(StreamHandler { sink, error }) } diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 3ba5d80d..11ec174e 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -103,7 +103,12 @@ impl Default for SoundLibraryEntry { )] pub enum KeySoundOutputBackendPersist { DefaultDevice, - Asio { driver_name: String }, + Asio { + driver_name: String, + /// ASIO 버퍼 크기(프레임). None이면 자동. 기존 store 하위호환을 위해 default. + #[serde(default, skip_serializing_if = "Option::is_none")] + buffer_size: Option, + }, } // 직렬화 형식: diff --git a/src-tauri/src/state/app_state.rs b/src-tauri/src/state/app_state.rs index 05d8aecf..5cd307b4 100644 --- a/src-tauri/src/state/app_state.rs +++ b/src-tauri/src/state/app_state.rs @@ -1728,18 +1728,26 @@ impl Drop for AppState { fn output_backend_from_persist(value: KeySoundOutputBackendPersist) -> KeySoundOutputBackend { match value { KeySoundOutputBackendPersist::DefaultDevice => KeySoundOutputBackend::DefaultDevice, - KeySoundOutputBackendPersist::Asio { driver_name } => { - KeySoundOutputBackend::Asio { driver_name } - } + KeySoundOutputBackendPersist::Asio { + driver_name, + buffer_size, + } => KeySoundOutputBackend::Asio { + driver_name, + buffer_size, + }, } } fn output_backend_to_persist(value: KeySoundOutputBackend) -> KeySoundOutputBackendPersist { match value { KeySoundOutputBackend::DefaultDevice => KeySoundOutputBackendPersist::DefaultDevice, - KeySoundOutputBackend::Asio { driver_name } => { - KeySoundOutputBackendPersist::Asio { driver_name } - } + KeySoundOutputBackend::Asio { + driver_name, + buffer_size, + } => KeySoundOutputBackendPersist::Asio { + driver_name, + buffer_size, + }, } } diff --git a/src/renderer/api/modules/resourceApi.ts b/src/renderer/api/modules/resourceApi.ts index d51aa7e8..9264820e 100644 --- a/src/renderer/api/modules/resourceApi.ts +++ b/src/renderer/api/modules/resourceApi.ts @@ -77,7 +77,7 @@ export const soundApi = { // 키음 출력 백엔드 (기본 장치 / ASIO) export type KeySoundOutputBackend = | { kind: 'defaultDevice' } - | { kind: 'asio'; driverName: string }; + | { kind: 'asio'; driverName: string; bufferSize?: number | null }; export interface KeySoundOutputDevices { defaultDevice: true; diff --git a/src/renderer/components/main/Settings.tsx b/src/renderer/components/main/Settings.tsx index 03fdd198..762af383 100644 --- a/src/renderer/components/main/Settings.tsx +++ b/src/renderer/components/main/Settings.tsx @@ -56,6 +56,9 @@ const PREVIEW_SOURCES: Record = { 'https://raw.githubusercontent.com/lee-sihun/DmNote/master/docs/assets/webm/obs.webm', }; +// ASIO 버퍼 크기 선택지(프레임). 게임 설정값과 맞춰야 ASIO 공존 가능. +const ASIO_BUFFER_SIZES = [64, 128, 256, 512, 1024] as const; + interface SettingsProps { showAlert: (msg: string, confirmText?: string) => void; showConfirm: ( @@ -180,6 +183,23 @@ const Settings = ({ } }; + // ASIO 버퍼 크기 변경 (게임과 동일 버퍼로 맞춰야 ASIO 공존 가능) + const handleAsioBufferChange = async (val: string) => { + const requested = keySoundOutput?.requested; + if (requested?.kind !== 'asio') return; + const bufferSize = val === 'auto' ? null : Number(val); + try { + const next = await keySoundOutputApi.setBackend({ + kind: 'asio', + driverName: requested.driverName, + bufferSize, + }); + setKeySoundOutput(next); + } catch (error) { + console.error('Failed to set ASIO buffer size', error); + } + }; + // Lenis smooth scroll 적용 (전역 설정 사용) const { scrollContainerRef } = useLenis(); @@ -901,6 +921,42 @@ const Settings = ({ align="right" />
+ {keySoundOutput?.requested.kind === 'asio' && ( +
+

+ {t('settings.keySoundOutputBuffer') || 'ASIO 버퍼 크기'} +

+ ({ + value: String(size), + label: String(size), + })), + ]} + value={ + keySoundOutput.requested.kind === 'asio' && + keySoundOutput.requested.bufferSize + ? String(keySoundOutput.requested.bufferSize) + : 'auto' + } + onChange={handleAsioBufferChange} + placeholder={ + t('settings.keySoundOutputBufferAuto') || '자동' + } + align="right" + /> +
+ )} + {keySoundOutput?.requested.kind === 'asio' && ( +

+ {t('settings.keySoundOutputBufferHint') || + '다른 ASIO 앱(게임)과 동시 사용 시, 그 앱과 같은 버퍼 크기로 맞추세요.'} +

+ )} {keySoundOutput?.error && (

{keySoundOutput.error} diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index 4c1b7495..3de88828 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -23,6 +23,9 @@ "resizeAnchor": "Resize Anchor", "keySoundOutput": "Key Sound Output", "keySoundOutputDefault": "Default Output Device", + "keySoundOutputBuffer": "ASIO Buffer Size", + "keySoundOutputBufferAuto": "Auto", + "keySoundOutputBufferHint": "When using another ASIO app (game) at the same time, set the same buffer size as that app.", "topLeft": "Top-Left", "bottomLeft": "Bottom-Left", "topRight": "Top-Right", diff --git a/src/renderer/locales/ko.json b/src/renderer/locales/ko.json index f46939b1..883d7d75 100644 --- a/src/renderer/locales/ko.json +++ b/src/renderer/locales/ko.json @@ -23,6 +23,9 @@ "resizeAnchor": "리사이즈 기준점", "keySoundOutput": "키음 출력 장치", "keySoundOutputDefault": "기본 출력 장치", + "keySoundOutputBuffer": "ASIO 버퍼 크기", + "keySoundOutputBufferAuto": "자동", + "keySoundOutputBufferHint": "다른 ASIO 앱(게임)과 동시 사용 시, 그 앱과 같은 버퍼 크기로 맞추세요.", "topLeft": "좌상단", "bottomLeft": "좌하단", "topRight": "우상단", diff --git a/src/renderer/locales/ru.json b/src/renderer/locales/ru.json index bbf7c09e..f80dd928 100644 --- a/src/renderer/locales/ru.json +++ b/src/renderer/locales/ru.json @@ -23,6 +23,9 @@ "resizeAnchor": "Точка привязки", "keySoundOutput": "Вывод звука клавиш", "keySoundOutputDefault": "Устройство по умолчанию", + "keySoundOutputBuffer": "Размер буфера ASIO", + "keySoundOutputBufferAuto": "Авто", + "keySoundOutputBufferHint": "При одновременном использовании другого приложения ASIO (игры) установите такой же размер буфера, как в нём.", "topLeft": "Верх-лево", "bottomLeft": "Низ-лево", "topRight": "Верх-право", diff --git a/src/renderer/locales/zh-Hant.json b/src/renderer/locales/zh-Hant.json index 47fdfa32..93981be5 100644 --- a/src/renderer/locales/zh-Hant.json +++ b/src/renderer/locales/zh-Hant.json @@ -23,6 +23,9 @@ "resizeAnchor": "調整錨點大小", "keySoundOutput": "按鍵音輸出裝置", "keySoundOutputDefault": "預設輸出裝置", + "keySoundOutputBuffer": "ASIO 緩衝區大小", + "keySoundOutputBufferAuto": "自動", + "keySoundOutputBufferHint": "與其他 ASIO 應用程式(遊戲)同時使用時,請設定與該應用程式相同的緩衝區大小。", "topLeft": "左上", "bottomLeft": "左下", "topRight": "右上", diff --git a/src/renderer/locales/zh-cn.json b/src/renderer/locales/zh-cn.json index d4e2de03..d8ce99f0 100644 --- a/src/renderer/locales/zh-cn.json +++ b/src/renderer/locales/zh-cn.json @@ -23,6 +23,9 @@ "resizeAnchor": "调整锚点大小", "keySoundOutput": "按键音输出设备", "keySoundOutputDefault": "默认输出设备", + "keySoundOutputBuffer": "ASIO 缓冲区大小", + "keySoundOutputBufferAuto": "自动", + "keySoundOutputBufferHint": "与其他 ASIO 应用(游戏)同时使用时,请设置与该应用相同的缓冲区大小。", "topLeft": "左上", "bottomLeft": "左下", "topRight": "右上", From 4334208bba70c187a4548107ed12d6b7942f8056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 6 Jun 2026 14:30:49 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20ASIO=20=ED=82=A4=EC=9D=8C=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EA=B9=9C=EB=B9=A1=EC=9E=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EB=B2=84=ED=8D=BC=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=2064=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 부팅 시 엔진을 저장된 출력 백엔드로 바로 초기화(KeySoundEngine::with_output_backend) → '기본 장치 → ASIO' 전환 깜빡임 제거 및 복원 상태 즉시 반영 - ASIO 버퍼 미지정 시 기본 64프레임으로 오픈(자동=드라이버 preferred는 게임과 어긋나 충돌하므로 제거) - 설정 드롭다운에서 '자동' 옵션 제거, 기본값 64로 표시 - 미사용 로케일 키 keySoundOutputBufferAuto 제거(5종) --- src-tauri/src/audio/engine.rs | 23 +++++++++++-- src-tauri/src/state/app_state.rs | 14 ++++---- src/renderer/components/main/Settings.tsx | 41 +++++++++++------------ src/renderer/locales/en.json | 1 - src/renderer/locales/ko.json | 1 - src/renderer/locales/ru.json | 1 - src/renderer/locales/zh-Hant.json | 1 - src/renderer/locales/zh-cn.json | 1 - 8 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs index a691d751..41bfffcd 100644 --- a/src-tauri/src/audio/engine.rs +++ b/src-tauri/src/audio/engine.rs @@ -290,10 +290,22 @@ impl Default for KeySoundEngine { impl KeySoundEngine { pub fn new() -> Self { + Self::with_output_backend(KeySoundOutputBackend::DefaultDevice) + } + + /// 초기 출력 백엔드를 지정해 생성. 오디오 스레드가 처음부터 이 백엔드로 스트림을 열어 + /// "기본 장치 → ASIO" 전환에서 발생하던 깜빡임을 제거한다. + pub fn with_output_backend(backend: KeySoundOutputBackend) -> Self { let (sender, receiver) = mpsc::channel(); + let output_state = KeySoundOutputState { + requested: backend.clone(), + effective: backend, + error: None, + asio_available: asio_backend_available(), + }; let state = Arc::new(RwLock::new(KeySoundRuntimeState { status: KeySoundStatus::default(), - output_state: KeySoundOutputState::default(), + output_state, soundpack: None, })); let state_for_thread = state.clone(); @@ -827,12 +839,19 @@ fn open_asio_audio_sink( Err(AudioSinkOpenError::AsioUnavailableBuild) } +/// ASIO 기본 버퍼 크기(프레임). 미지정 시 이 값으로 오픈 → 게임 기본값(최저)과 동일하게 맞춤. +#[cfg(all(windows, feature = "asio-backend"))] +const DEFAULT_ASIO_BUFFER_FRAMES: u32 = 64; + #[cfg(all(windows, feature = "asio-backend"))] fn open_device_audio_sink( device: cpal::Device, buffer_size: Option, ) -> AudioSinkResult { - let buffer_size = buffer_size.filter(|frames| *frames > 0); + // 미지정(None)이면 기본 64로 오픈 (자동=드라이버 preferred는 게임과 어긋나 충돌하므로 사용 안 함). + let buffer_size = buffer_size + .filter(|frames| *frames > 0) + .or(Some(DEFAULT_ASIO_BUFFER_FRAMES)); // 고정 버퍼 지정 시 그 값 그대로 오픈 → 다른 ASIO 앱(게임)과 버퍼를 맞춰 공존. // 드라이버가 해당 버퍼를 거부하면 자동 버퍼로 폴백. diff --git a/src-tauri/src/state/app_state.rs b/src-tauri/src/state/app_state.rs index 5cd307b4..c0b3b12b 100644 --- a/src-tauri/src/state/app_state.rs +++ b/src-tauri/src/state/app_state.rs @@ -84,13 +84,13 @@ impl AppState { Self::sync_counters_with_keys_impl(&key_counters, &snapshot.keys); let key_counter_enabled = Arc::new(AtomicBool::new(snapshot.key_counter_enabled)); let active_keys = Arc::new(RwLock::new(HashSet::new())); - let key_sound = Arc::new(KeySoundEngine::new()); - if let Some(backend) = snapshot.key_sound_output_backend.clone() { - let output_state = key_sound.set_output_backend(output_backend_from_persist(backend)); - if let Some(error) = output_state.error.as_ref() { - warn!("[KeySound] failed to restore output backend: {error}"); - } - } + // 저장된 출력 백엔드로 엔진을 처음부터 초기화 → "기본 장치 → ASIO" 전환 깜빡임 제거. + let initial_backend = snapshot + .key_sound_output_backend + .clone() + .map(output_backend_from_persist) + .unwrap_or_default(); + let key_sound = Arc::new(KeySoundEngine::with_output_backend(initial_backend)); let obs_bridge = Arc::new(ObsBridgeService::new(env!("CARGO_PKG_VERSION"))); Ok(Self { diff --git a/src/renderer/components/main/Settings.tsx b/src/renderer/components/main/Settings.tsx index 762af383..e96adbde 100644 --- a/src/renderer/components/main/Settings.tsx +++ b/src/renderer/components/main/Settings.tsx @@ -58,6 +58,8 @@ const PREVIEW_SOURCES: Record = { // ASIO 버퍼 크기 선택지(프레임). 게임 설정값과 맞춰야 ASIO 공존 가능. const ASIO_BUFFER_SIZES = [64, 128, 256, 512, 1024] as const; +// 기본 버퍼 크기 (게임 기본값과 동일한 최저값) +const DEFAULT_ASIO_BUFFER = 64; interface SettingsProps { showAlert: (msg: string, confirmText?: string) => void; @@ -173,7 +175,12 @@ const Settings = ({ const handleKeySoundOutputChange = async (val: string) => { const backend: KeySoundOutputBackend = val.startsWith('asio:') - ? { kind: 'asio', driverName: val.slice('asio:'.length) } + ? { + kind: 'asio', + driverName: val.slice('asio:'.length), + // ASIO 선택 시 기본 버퍼 64 (게임과 동일하게 맞춰야 공존 가능) + bufferSize: DEFAULT_ASIO_BUFFER, + } : { kind: 'defaultDevice' }; try { const next = await keySoundOutputApi.setBackend(backend); @@ -187,12 +194,11 @@ const Settings = ({ const handleAsioBufferChange = async (val: string) => { const requested = keySoundOutput?.requested; if (requested?.kind !== 'asio') return; - const bufferSize = val === 'auto' ? null : Number(val); try { const next = await keySoundOutputApi.setBackend({ kind: 'asio', driverName: requested.driverName, - bufferSize, + bufferSize: Number(val), }); setKeySoundOutput(next); } catch (error) { @@ -927,26 +933,17 @@ const Settings = ({ {t('settings.keySoundOutputBuffer') || 'ASIO 버퍼 크기'}

({ - value: String(size), - label: String(size), - })), - ]} - value={ - keySoundOutput.requested.kind === 'asio' && - keySoundOutput.requested.bufferSize - ? String(keySoundOutput.requested.bufferSize) - : 'auto' - } + options={ASIO_BUFFER_SIZES.map((size) => ({ + value: String(size), + label: String(size), + }))} + value={String( + (keySoundOutput.requested.kind === 'asio' && + keySoundOutput.requested.bufferSize) || + DEFAULT_ASIO_BUFFER, + )} onChange={handleAsioBufferChange} - placeholder={ - t('settings.keySoundOutputBufferAuto') || '자동' - } + placeholder={String(DEFAULT_ASIO_BUFFER)} align="right" /> diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index 3de88828..a0454a14 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -24,7 +24,6 @@ "keySoundOutput": "Key Sound Output", "keySoundOutputDefault": "Default Output Device", "keySoundOutputBuffer": "ASIO Buffer Size", - "keySoundOutputBufferAuto": "Auto", "keySoundOutputBufferHint": "When using another ASIO app (game) at the same time, set the same buffer size as that app.", "topLeft": "Top-Left", "bottomLeft": "Bottom-Left", diff --git a/src/renderer/locales/ko.json b/src/renderer/locales/ko.json index 883d7d75..94529088 100644 --- a/src/renderer/locales/ko.json +++ b/src/renderer/locales/ko.json @@ -24,7 +24,6 @@ "keySoundOutput": "키음 출력 장치", "keySoundOutputDefault": "기본 출력 장치", "keySoundOutputBuffer": "ASIO 버퍼 크기", - "keySoundOutputBufferAuto": "자동", "keySoundOutputBufferHint": "다른 ASIO 앱(게임)과 동시 사용 시, 그 앱과 같은 버퍼 크기로 맞추세요.", "topLeft": "좌상단", "bottomLeft": "좌하단", diff --git a/src/renderer/locales/ru.json b/src/renderer/locales/ru.json index f80dd928..28657edb 100644 --- a/src/renderer/locales/ru.json +++ b/src/renderer/locales/ru.json @@ -24,7 +24,6 @@ "keySoundOutput": "Вывод звука клавиш", "keySoundOutputDefault": "Устройство по умолчанию", "keySoundOutputBuffer": "Размер буфера ASIO", - "keySoundOutputBufferAuto": "Авто", "keySoundOutputBufferHint": "При одновременном использовании другого приложения ASIO (игры) установите такой же размер буфера, как в нём.", "topLeft": "Верх-лево", "bottomLeft": "Низ-лево", diff --git a/src/renderer/locales/zh-Hant.json b/src/renderer/locales/zh-Hant.json index 93981be5..2e3469a2 100644 --- a/src/renderer/locales/zh-Hant.json +++ b/src/renderer/locales/zh-Hant.json @@ -24,7 +24,6 @@ "keySoundOutput": "按鍵音輸出裝置", "keySoundOutputDefault": "預設輸出裝置", "keySoundOutputBuffer": "ASIO 緩衝區大小", - "keySoundOutputBufferAuto": "自動", "keySoundOutputBufferHint": "與其他 ASIO 應用程式(遊戲)同時使用時,請設定與該應用程式相同的緩衝區大小。", "topLeft": "左上", "bottomLeft": "左下", diff --git a/src/renderer/locales/zh-cn.json b/src/renderer/locales/zh-cn.json index d8ce99f0..8429b7f1 100644 --- a/src/renderer/locales/zh-cn.json +++ b/src/renderer/locales/zh-cn.json @@ -24,7 +24,6 @@ "keySoundOutput": "按键音输出设备", "keySoundOutputDefault": "默认输出设备", "keySoundOutputBuffer": "ASIO 缓冲区大小", - "keySoundOutputBufferAuto": "自动", "keySoundOutputBufferHint": "与其他 ASIO 应用(游戏)同时使用时,请设置与该应用相同的缓冲区大小。", "topLeft": "左上", "bottomLeft": "左下", From 1e27e945e26844ed7e02e2e606723ee2c1195af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 6 Jun 2026 14:43:31 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=ED=82=A4=20=EC=82=AC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=20=EC=84=A4=EC=A0=95=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 설정 패널 재진입 시 '기본 장치 → ASIO' 드롭다운 깜빡임 제거(출력 상태 모듈 캐시) - 출력 장치 라벨을 '키 사운드'로 변경 - 출력/버퍼 드롭다운 너비 고정(160px) + 긴 항목 말줄임(...) 처리 - ASIO 버퍼 크기: ASIO 선택이 아니면 비활성화(항상 표시) - 버퍼 안내 힌트 문구 제거 --- src/renderer/components/main/Settings.tsx | 125 ++++++++++-------- .../components/main/common/Dropdown.tsx | 7 +- src/renderer/locales/en.json | 3 +- src/renderer/locales/ko.json | 5 +- src/renderer/locales/ru.json | 5 +- src/renderer/locales/zh-Hant.json | 5 +- src/renderer/locales/zh-cn.json | 5 +- 7 files changed, 87 insertions(+), 68 deletions(-) diff --git a/src/renderer/components/main/Settings.tsx b/src/renderer/components/main/Settings.tsx index e96adbde..f2e6894e 100644 --- a/src/renderer/components/main/Settings.tsx +++ b/src/renderer/components/main/Settings.tsx @@ -61,6 +61,11 @@ const ASIO_BUFFER_SIZES = [64, 128, 256, 512, 1024] as const; // 기본 버퍼 크기 (게임 기본값과 동일한 최저값) const DEFAULT_ASIO_BUFFER = 64; +// 설정 패널은 열 때마다 재마운트되므로, 마지막 출력 상태를 모듈에 캐시해 +// 재진입 시 '기본 장치 → ASIO' 드롭다운 깜빡임을 방지한다. +let cachedKeySoundOutput: KeySoundOutputState | null = null; +let cachedAsioDrivers: string[] = []; + interface SettingsProps { showAlert: (msg: string, confirmText?: string) => void; showConfirm: ( @@ -148,10 +153,15 @@ const Settings = ({ }); const obsTogglingRef = useRef(false); - // 키음 출력 백엔드 (기본 장치 / ASIO) - const [keySoundOutput, setKeySoundOutput] = - useState(null); - const [asioDrivers, setAsioDrivers] = useState([]); + // 키음 출력 백엔드 (기본 장치 / ASIO) — 캐시로 초기화해 재진입 깜빡임 방지 + const [keySoundOutput, setKeySoundOutputRaw] = + useState(cachedKeySoundOutput); + const [asioDrivers, setAsioDrivers] = useState(cachedAsioDrivers); + + const setKeySoundOutput = (state: KeySoundOutputState) => { + cachedKeySoundOutput = state; + setKeySoundOutputRaw(state); + }; useEffect(() => { let cancelled = false; @@ -162,6 +172,7 @@ const Settings = ({ keySoundOutputApi.getState(), ]); if (cancelled) return; + cachedAsioDrivers = devices.asio; setAsioDrivers(devices.asio); setKeySoundOutput(state); } catch (error) { @@ -890,6 +901,7 @@ const Settings = ({ } }} placeholder={t('settings.selectAnchor')} + align="right" /> @@ -900,60 +912,67 @@ const Settings = ({ onMouseEnter={() => setHoveredKey('keySoundOutput')} onMouseLeave={() => setHoveredKey(null)} > -

- {t('settings.keySoundOutput') || '키음 출력 장치'} +

+ {t('settings.keySoundOutput') || '키 사운드 출력'}

- ({ - value: `asio:${name}`, - label: `ASIO: ${name}`, - })), - ]} - value={ - keySoundOutput?.requested.kind === 'asio' - ? `asio:${keySoundOutput.requested.driverName}` - : 'defaultDevice' - } - onChange={handleKeySoundOutputChange} - placeholder={ - t('settings.keySoundOutputDefault') || '기본 출력 장치' - } - align="right" - /> - - {keySoundOutput?.requested.kind === 'asio' && ( -
-

- {t('settings.keySoundOutputBuffer') || 'ASIO 버퍼 크기'} -

+
({ - value: String(size), - label: String(size), - }))} - value={String( - (keySoundOutput.requested.kind === 'asio' && - keySoundOutput.requested.bufferSize) || - DEFAULT_ASIO_BUFFER, - )} - onChange={handleAsioBufferChange} - placeholder={String(DEFAULT_ASIO_BUFFER)} + options={[ + { + value: 'defaultDevice', + label: + t('settings.keySoundOutputDefault') || + '기본 재생 장치', + }, + ...asioDrivers.map((name) => ({ + value: `asio:${name}`, + // 드라이버 이름이 길면 …로 축약 (기본 항목 라벨은 안 잘리게 max-w 여유, ASIO만 축약) + label: `ASIO: ${ + name.length > 16 ? `${name.slice(0, 16)}…` : name + }`, + })), + ]} + value={ + keySoundOutput?.requested.kind === 'asio' + ? `asio:${keySoundOutput.requested.driverName}` + : 'defaultDevice' + } + onChange={handleKeySoundOutputChange} + placeholder={ + t('settings.keySoundOutputDefault') || '기본 재생 장치' + } align="right" + widthClass="max-w-[160px]" />
- )} - {keySoundOutput?.requested.kind === 'asio' && ( -

- {t('settings.keySoundOutputBufferHint') || - '다른 ASIO 앱(게임)과 동시 사용 시, 그 앱과 같은 버퍼 크기로 맞추세요.'} +

+
+

+ {t('settings.keySoundOutputBuffer') || 'ASIO 버퍼 크기'}

- )} + ({ + value: String(size), + label: String(size), + }))} + value={String( + (keySoundOutput?.requested.kind === 'asio' && + keySoundOutput.requested.bufferSize) || + DEFAULT_ASIO_BUFFER, + )} + onChange={handleAsioBufferChange} + placeholder={String(DEFAULT_ASIO_BUFFER)} + align="right" + widthClass="w-[70px]" + disabled={keySoundOutput?.requested.kind !== 'asio'} + /> +
{keySoundOutput?.error && (

{keySoundOutput.error} @@ -1141,6 +1160,7 @@ const Settings = ({ value={language} onChange={handleLanguageChange} placeholder={t('settings.selectLanguage')} + align="right" />

@@ -1164,6 +1184,7 @@ const Settings = ({ onChange={handleAngleModeChangeSelect} placeholder={t('settings.renderMode')} disabled={isMacOS} + align="right" />
{!isMacOS && ( diff --git a/src/renderer/components/main/common/Dropdown.tsx b/src/renderer/components/main/common/Dropdown.tsx index c9c6688f..5e1af84e 100644 --- a/src/renderer/components/main/common/Dropdown.tsx +++ b/src/renderer/components/main/common/Dropdown.tsx @@ -17,6 +17,8 @@ interface DropdownProps { iconTrigger?: React.ReactNode; /** 메뉴 수평 정렬 (기본: left) */ align?: 'left' | 'center' | 'right'; + /** 트리거/메뉴 너비 고정용 Tailwind 클래스 (예: 'w-[160px]'). 길면 말줄임(...) 처리됨 */ + widthClass?: string; } const Dropdown: React.FC = ({ @@ -28,6 +30,7 @@ const Dropdown: React.FC = ({ fullWidth = false, iconTrigger, align = 'left', + widthClass = '', }) => { const [open, setOpen] = useState(false); const [openUpward, setOpenUpward] = useState(false); @@ -93,7 +96,7 @@ const Dropdown: React.FC = ({ type="button" className={`flex box-border items-center justify-between h-[23px] py-[0px] px-[8px] bg-[#2A2A31] border-[1px] border-[#3A3944] rounded-[7px] text-[#DBDEE8] text-style-2 outline-none ${ fullWidth ? 'w-full' : '' - }`} + } ${widthClass}`} onClick={() => setOpen((prev) => !prev)} disabled={disabled} > @@ -133,7 +136,7 @@ const Dropdown: React.FC = ({ : align === 'center' ? 'left-1/2 -translate-x-1/2' : 'left-0' - } ${openUpward ? 'bottom-[25px]' : 'top-[25px]'}`} + } ${widthClass} ${openUpward ? 'bottom-[25px]' : 'top-[25px]'}`} > {options.length === 0 ? (
diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index a0454a14..88e57779 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -22,9 +22,8 @@ "removePlugin": "Remove plugin", "resizeAnchor": "Resize Anchor", "keySoundOutput": "Key Sound Output", - "keySoundOutputDefault": "Default Output Device", + "keySoundOutputDefault": "Default Device", "keySoundOutputBuffer": "ASIO Buffer Size", - "keySoundOutputBufferHint": "When using another ASIO app (game) at the same time, set the same buffer size as that app.", "topLeft": "Top-Left", "bottomLeft": "Bottom-Left", "topRight": "Top-Right", diff --git a/src/renderer/locales/ko.json b/src/renderer/locales/ko.json index 94529088..ee4a19c9 100644 --- a/src/renderer/locales/ko.json +++ b/src/renderer/locales/ko.json @@ -21,10 +21,9 @@ "noPlugins": "불러온 플러그인이 없습니다", "removePlugin": "플러그인 삭제", "resizeAnchor": "리사이즈 기준점", - "keySoundOutput": "키음 출력 장치", - "keySoundOutputDefault": "기본 출력 장치", + "keySoundOutput": "키 사운드 출력", + "keySoundOutputDefault": "기본 재생 장치", "keySoundOutputBuffer": "ASIO 버퍼 크기", - "keySoundOutputBufferHint": "다른 ASIO 앱(게임)과 동시 사용 시, 그 앱과 같은 버퍼 크기로 맞추세요.", "topLeft": "좌상단", "bottomLeft": "좌하단", "topRight": "우상단", diff --git a/src/renderer/locales/ru.json b/src/renderer/locales/ru.json index 28657edb..6ed8e994 100644 --- a/src/renderer/locales/ru.json +++ b/src/renderer/locales/ru.json @@ -21,10 +21,9 @@ "noPlugins": "Нет плагинов", "removePlugin": "Удалить плагин", "resizeAnchor": "Точка привязки", - "keySoundOutput": "Вывод звука клавиш", - "keySoundOutputDefault": "Устройство по умолчанию", + "keySoundOutput": "Звук клавиш", + "keySoundOutputDefault": "По умолчанию", "keySoundOutputBuffer": "Размер буфера ASIO", - "keySoundOutputBufferHint": "При одновременном использовании другого приложения ASIO (игры) установите такой же размер буфера, как в нём.", "topLeft": "Верх-лево", "bottomLeft": "Низ-лево", "topRight": "Верх-право", diff --git a/src/renderer/locales/zh-Hant.json b/src/renderer/locales/zh-Hant.json index 2e3469a2..b3815167 100644 --- a/src/renderer/locales/zh-Hant.json +++ b/src/renderer/locales/zh-Hant.json @@ -21,10 +21,9 @@ "noPlugins": "尚未載入任何插件", "removePlugin": "移除插件", "resizeAnchor": "調整錨點大小", - "keySoundOutput": "按鍵音輸出裝置", - "keySoundOutputDefault": "預設輸出裝置", + "keySoundOutput": "按鍵音輸出", + "keySoundOutputDefault": "預設裝置", "keySoundOutputBuffer": "ASIO 緩衝區大小", - "keySoundOutputBufferHint": "與其他 ASIO 應用程式(遊戲)同時使用時,請設定與該應用程式相同的緩衝區大小。", "topLeft": "左上", "bottomLeft": "左下", "topRight": "右上", diff --git a/src/renderer/locales/zh-cn.json b/src/renderer/locales/zh-cn.json index 8429b7f1..c660a09b 100644 --- a/src/renderer/locales/zh-cn.json +++ b/src/renderer/locales/zh-cn.json @@ -21,10 +21,9 @@ "noPlugins": "尚未加载任何插件", "removePlugin": "移除插件", "resizeAnchor": "调整锚点大小", - "keySoundOutput": "按键音输出设备", - "keySoundOutputDefault": "默认输出设备", + "keySoundOutput": "按键音输出", + "keySoundOutputDefault": "默认设备", "keySoundOutputBuffer": "ASIO 缓冲区大小", - "keySoundOutputBufferHint": "与其他 ASIO 应用(游戏)同时使用时,请设置与该应用相同的缓冲区大小。", "topLeft": "左上", "bottomLeft": "左下", "topRight": "右上", From 74db7b5b0f9b9b11fd9fa44919a52b81da9a584e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 6 Jun 2026 17:23:52 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20ASIO=20=ED=82=A4=EC=9D=8C=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EC=95=88=EC=A0=95=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=91=9C=EC=8B=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ASIO는 고정 버퍼로만 열고 자동 버퍼 폴백 제거 (게임과 버퍼 어긋남 방지) - 고정 버퍼 실패 시 ASIO 대신 기본 장치로 폴백 + 사유 노출 - ASIO 장치 이름 trim 매칭, 목록에 없는 선택값도 드롭다운에 표시 - 출력 에러를 드롭다운 라벨에 다국어로 표시 (인라인 경고 제거) - 윈도우 ASIO 빌드 스크립트(tauri:dev:asio/build:asio) 및 안내 추가 --- docs/readme_en.md | 2 + package.json | 2 + src-tauri/src/audio/engine.rs | 130 ++- src-tauri/src/models/mod.rs | 2 +- src/renderer/api/modules/resourceApi.ts | 9 +- src/renderer/components/main/Settings.tsx | 67 +- src/renderer/locales/en.json | 6 + src/renderer/locales/ko.json | 6 + src/renderer/locales/ru.json | 6 + src/renderer/locales/zh-Hant.json | 1208 +++++++++++---------- src/renderer/locales/zh-cn.json | 6 + 11 files changed, 771 insertions(+), 673 deletions(-) diff --git a/docs/readme_en.md b/docs/readme_en.md index e477db73..3bdcf7d7 100644 --- a/docs/readme_en.md +++ b/docs/readme_en.md @@ -109,6 +109,8 @@ npm install npm run tauri:dev ``` +To build ASIO output on Windows, set up LLVM (`LIBCLANG_PATH`) and the ASIO SDK (`CPAL_ASIO_DIR`), then use `npm run tauri:dev:asio` or `npm run tauri:build:asio`. + ## � Notes - **This program is free to use for streaming or gameplay video production.** diff --git a/package.json b/package.json index b4ae8e9f..52dc3295 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "type-check": "tsc --noEmit", "type-check:watch": "tsc --noEmit --watch", "tauri:dev": "tauri dev", + "tauri:dev:asio": "tauri dev -f asio-backend", "tauri:dev:edge-beta": "node scripts/run-with-edge-beta.js", "webview2:download:143": "node scripts/download-webview2-fixed-runtime.js --major 143 --arch x64", "tauri:dev:webview2-fixed:143": "npm run webview2:download:143 && tauri dev", "tauri:build": "tauri build", + "tauri:build:asio": "tauri build -f asio-backend", "tauri:build:webview2-fixed:143": "npm run webview2:download:143 && tauri build --config src-tauri/tauri.webview2-fixed.conf.json", "tauri:build:portable:webview2-fixed:143": "node scripts/build-portable-win.js --major 143 --arch x64", "tauri:build:single-exe:webview2-fixed:143": "node scripts/build-single-exe-win.js --major 143 --arch x64", diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs index 41bfffcd..7cab1471 100644 --- a/src-tauri/src/audio/engine.rs +++ b/src-tauri/src/audio/engine.rs @@ -21,7 +21,7 @@ use log::debug; use log::info; use log::warn; use parking_lot::RwLock; -use rodio::{DeviceSinkBuilder, MixerDeviceSink, Source}; +use rodio::{cpal, DeviceSinkBuilder, MixerDeviceSink, Source}; use serde::{Deserialize, Serialize}; use symphonia::{ core::{ @@ -189,8 +189,8 @@ pub enum KeySoundOutputBackend { DefaultDevice, Asio { driver_name: String, - /// ASIO 버퍼 크기(프레임). None이면 자동(드라이버 preferred). - /// 다른 ASIO 앱(게임)과 공존하려면 그 앱과 동일한 버퍼로 맞춰야 함. + /// ASIO 버퍼 크기(프레임). None이면 기본 64 고정 + /// 다른 ASIO 앱(게임)과 공존하려면 그 앱과 동일한 버퍼로 맞춰야 함 #[serde(default)] buffer_size: Option, }, @@ -217,6 +217,7 @@ pub struct KeySoundOutputState { pub requested: KeySoundOutputBackend, pub effective: KeySoundOutputBackend, pub error: Option, + pub error_code: Option, pub asio_available: bool, } @@ -226,6 +227,7 @@ impl Default for KeySoundOutputState { requested: KeySoundOutputBackend::DefaultDevice, effective: KeySoundOutputBackend::DefaultDevice, error: None, + error_code: None, asio_available: asio_backend_available(), } } @@ -298,9 +300,10 @@ impl KeySoundEngine { pub fn with_output_backend(backend: KeySoundOutputBackend) -> Self { let (sender, receiver) = mpsc::channel(); let output_state = KeySoundOutputState { - requested: backend.clone(), - effective: backend, + requested: backend, + effective: KeySoundOutputBackend::DefaultDevice, error: None, + error_code: None, asio_available: asio_backend_available(), }; let state = Arc::new(RwLock::new(KeySoundRuntimeState { @@ -675,6 +678,11 @@ struct StreamHandler { error: Arc, } +const ERROR_CODE_ASIO_UNAVAILABLE_BUILD: &str = "asioUnavailableBuild"; +const ERROR_CODE_ASIO_DEVICE_NOT_FOUND: &str = "asioDeviceNotFound"; +const ERROR_CODE_ASIO_OPEN_FAILED: &str = "asioOpenFailed"; +const ERROR_CODE_DEFAULT_OPEN_FAILED: &str = "defaultOpenFailed"; + #[derive(Debug)] enum AudioSinkOpenError { AsioUnavailableBuild, @@ -688,8 +696,9 @@ type AudioSinkResult = std::result::Result; impl fmt::Display for AudioSinkOpenError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::AsioUnavailableBuild => write!(f, "ASIO 미지원 빌드"), - Self::AsioDeviceNotFound => write!(f, "ASIO 장치를 찾을 수 없습니다"), + Self::AsioUnavailableBuild | Self::AsioDeviceNotFound => { + write!(f, "{}", audio_sink_error_message(self)) + } Self::OpenFailed(err) => write!(f, "{err:#}"), } } @@ -708,6 +717,21 @@ fn asio_backend_available() -> bool { cfg!(all(windows, feature = "asio-backend")) } +fn stream_error_callback( + label: &'static str, +) -> ( + Arc, + impl FnMut(cpal::StreamError) + Send + Clone + 'static, +) { + let error = Arc::new(AtomicBool::new(false)); + let err_flag = Arc::clone(&error); + let callback = move |err| { + warn!("[KeySound] {label} error: {err}"); + err_flag.store(true, Ordering::Release); + }; + (error, callback) +} + fn switch_output_backend( backend: KeySoundOutputBackend, stream_handler: &mut Option, @@ -722,6 +746,7 @@ fn switch_output_backend( requested: requested.clone(), effective: requested, error: None, + error_code: None, asio_available: asio_backend_available(), } } @@ -731,19 +756,24 @@ fn switch_output_backend( KeySoundOutputBackend::DefaultDevice => KeySoundOutputState { requested: KeySoundOutputBackend::DefaultDevice, effective: KeySoundOutputBackend::DefaultDevice, - error: Some("기본 출력 장치를 열 수 없습니다".to_string()), + error: Some(default_output_error_message(&err).to_string()), + error_code: Some(ERROR_CODE_DEFAULT_OPEN_FAILED.to_string()), asio_available: asio_backend_available(), }, KeySoundOutputBackend::Asio { driver_name, buffer_size, } => { + let mut error = asio_output_error_message(&err).to_string(); + let mut error_code = asio_output_error_code(&err).to_string(); if let Err(default_err) = open_audio_sink(&KeySoundOutputBackend::DefaultDevice) .map(|handler| { *stream_handler = Some(handler); }) { warn!("[KeySound] failed to fallback to default output: {default_err}"); + error = format!("{error}; 기본 장치 폴백도 실패: {default_err}"); + error_code = ERROR_CODE_DEFAULT_OPEN_FAILED.to_string(); } KeySoundOutputState { @@ -752,7 +782,8 @@ fn switch_output_backend( buffer_size, }, effective: KeySoundOutputBackend::DefaultDevice, - error: Some(asio_output_error_message(&err).to_string()), + error: Some(error), + error_code: Some(error_code), asio_available: asio_backend_available(), } } @@ -761,11 +792,30 @@ fn switch_output_backend( } } -fn asio_output_error_message(err: &AudioSinkOpenError) -> &'static str { +fn audio_sink_error_message(err: &AudioSinkOpenError) -> &'static str { match err { AudioSinkOpenError::AsioUnavailableBuild => "ASIO 미지원 빌드", AudioSinkOpenError::AsioDeviceNotFound => "ASIO 장치를 찾을 수 없습니다", + AudioSinkOpenError::OpenFailed(_) => "오디오 출력 장치를 열 수 없습니다", + } +} + +fn default_output_error_message(_err: &AudioSinkOpenError) -> &'static str { + "기본 출력 장치를 열 수 없습니다" +} + +fn asio_output_error_message(err: &AudioSinkOpenError) -> &'static str { + match err { AudioSinkOpenError::OpenFailed(_) => "ASIO 장치를 열 수 없어 기본 출력으로 재생합니다", + _ => audio_sink_error_message(err), + } +} + +fn asio_output_error_code(err: &AudioSinkOpenError) -> &'static str { + match err { + AudioSinkOpenError::AsioUnavailableBuild => ERROR_CODE_ASIO_UNAVAILABLE_BUILD, + AudioSinkOpenError::AsioDeviceNotFound => ERROR_CODE_ASIO_DEVICE_NOT_FOUND, + AudioSinkOpenError::OpenFailed(_) => ERROR_CODE_ASIO_OPEN_FAILED, } } @@ -780,16 +830,12 @@ fn open_audio_sink(backend: &KeySoundOutputBackend) -> AudioSinkResult AudioSinkResult { - let error = Arc::new(AtomicBool::new(false)); - let err_flag = Arc::clone(&error); + let (error, callback) = stream_error_callback("stream"); let sink = DeviceSinkBuilder::from_default_device() .and_then(|builder| { builder - .with_error_callback(move |err| { - warn!("[KeySound] stream error: {err}"); - err_flag.store(true, Ordering::Release); - }) + .with_error_callback(callback) .open_sink_or_fallback() }) .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))?; @@ -817,7 +863,7 @@ fn open_asio_audio_sink( for device in devices { let name = match device.description() { - Ok(description) => description.name().to_string(), + Ok(description) => description.name().trim().to_string(), Err(err) => { warn!("[KeySound] failed to read ASIO device name: {err}"); continue; @@ -839,7 +885,7 @@ fn open_asio_audio_sink( Err(AudioSinkOpenError::AsioUnavailableBuild) } -/// ASIO 기본 버퍼 크기(프레임). 미지정 시 이 값으로 오픈 → 게임 기본값(최저)과 동일하게 맞춤. +/// ASIO 기본 버퍼 크기(프레임). 미지정 시 이 값으로 고정 오픈 #[cfg(all(windows, feature = "asio-backend"))] const DEFAULT_ASIO_BUFFER_FRAMES: u32 = 64; @@ -848,53 +894,33 @@ fn open_device_audio_sink( device: cpal::Device, buffer_size: Option, ) -> AudioSinkResult { - // 미지정(None)이면 기본 64로 오픈 (자동=드라이버 preferred는 게임과 어긋나 충돌하므로 사용 안 함). - let buffer_size = buffer_size + // 미지정(None)이면 기본 64로 오픈 + let frames = buffer_size .filter(|frames| *frames > 0) - .or(Some(DEFAULT_ASIO_BUFFER_FRAMES)); - - // 고정 버퍼 지정 시 그 값 그대로 오픈 → 다른 ASIO 앱(게임)과 버퍼를 맞춰 공존. - // 드라이버가 해당 버퍼를 거부하면 자동 버퍼로 폴백. - if let Some(frames) = buffer_size { - match try_open_asio_sink(device.clone(), Some(frames)) { - Ok(handler) => return Ok(handler), - Err(err) => { - warn!("[KeySound] ASIO 고정 버퍼({frames}) 오픈 실패, 자동 버퍼로 재시도: {err:?}") - } - } - } + .unwrap_or(DEFAULT_ASIO_BUFFER_FRAMES); - try_open_asio_sink(device, None) + // 고정 버퍼 지정 시 그 값 그대로 오픈 + try_open_asio_sink(device, frames) } #[cfg(all(windows, feature = "asio-backend"))] -fn try_open_asio_sink( - device: cpal::Device, - buffer_size: Option, -) -> AudioSinkResult { - let error = Arc::new(AtomicBool::new(false)); - let err_flag = Arc::clone(&error); +fn try_open_asio_sink(device: cpal::Device, buffer_size: u32) -> AudioSinkResult { + let (error, callback) = stream_error_callback("ASIO stream"); let builder = DeviceSinkBuilder::from_device(device) .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))? - .with_error_callback(move |err| { - warn!("[KeySound] ASIO stream error: {err}"); - err_flag.store(true, Ordering::Release); - }); + .with_error_callback(callback); // 샘플레이트는 드라이버 현재값(default_output_config)을 그대로 사용 → ASIOSetSampleRate 회피. - // 버퍼만 고정 지정 시 적용. - let sink = match buffer_size { - Some(frames) => builder - .with_buffer_size(cpal::BufferSize::Fixed(frames)) - .open_stream(), - None => builder.open_sink_or_fallback(), - } - .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))?; + // 버퍼는 명시 고정만 사용 + let sink = builder + .with_buffer_size(cpal::BufferSize::Fixed(buffer_size)) + .open_stream() + .map_err(|err| AudioSinkOpenError::OpenFailed(anyhow::Error::new(err)))?; let config = sink.config(); info!( - "[KeySound] ASIO 스트림 오픈: 요청 버퍼={:?}, 적용 sample_rate={}Hz, buffer={:?}", + "[KeySound] ASIO 스트림 오픈: 요청 버퍼={}, 적용 sample_rate={}Hz, buffer={:?}", buffer_size, config.sample_rate().get(), config.buffer_size() diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 11ec174e..c02a6305 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -105,7 +105,7 @@ pub enum KeySoundOutputBackendPersist { DefaultDevice, Asio { driver_name: String, - /// ASIO 버퍼 크기(프레임). None이면 자동. 기존 store 하위호환을 위해 default. + /// ASIO 버퍼 크기(프레임). None이면 엔진 기본값 사용 #[serde(default, skip_serializing_if = "Option::is_none")] buffer_size: Option, }, diff --git a/src/renderer/api/modules/resourceApi.ts b/src/renderer/api/modules/resourceApi.ts index 9264820e..c60d860a 100644 --- a/src/renderer/api/modules/resourceApi.ts +++ b/src/renderer/api/modules/resourceApi.ts @@ -79,8 +79,14 @@ export type KeySoundOutputBackend = | { kind: 'defaultDevice' } | { kind: 'asio'; driverName: string; bufferSize?: number | null }; +export type KeySoundOutputErrorCode = + | 'asioUnavailableBuild' + | 'asioDeviceNotFound' + | 'asioOpenFailed' + | 'defaultOpenFailed'; + export interface KeySoundOutputDevices { - defaultDevice: true; + defaultDevice: boolean; asio: string[]; } @@ -88,6 +94,7 @@ export interface KeySoundOutputState { requested: KeySoundOutputBackend; effective: KeySoundOutputBackend; error: string | null; + errorCode: KeySoundOutputErrorCode | null; asioAvailable: boolean; } diff --git a/src/renderer/components/main/Settings.tsx b/src/renderer/components/main/Settings.tsx index f2e6894e..3b691ae8 100644 --- a/src/renderer/components/main/Settings.tsx +++ b/src/renderer/components/main/Settings.tsx @@ -61,6 +61,13 @@ const ASIO_BUFFER_SIZES = [64, 128, 256, 512, 1024] as const; // 기본 버퍼 크기 (게임 기본값과 동일한 최저값) const DEFAULT_ASIO_BUFFER = 64; +const KEY_SOUND_OUTPUT_ERROR_KEYS: Record = { + asioUnavailableBuild: 'settings.keySoundOutputError.asioUnavailableBuild', + asioDeviceNotFound: 'settings.keySoundOutputError.asioDeviceNotFound', + asioOpenFailed: 'settings.keySoundOutputError.asioOpenFailed', + defaultOpenFailed: 'settings.keySoundOutputError.defaultOpenFailed', +}; + // 설정 패널은 열 때마다 재마운트되므로, 마지막 출력 상태를 모듈에 캐시해 // 재진입 시 '기본 장치 → ASIO' 드롭다운 깜빡임을 방지한다. let cachedKeySoundOutput: KeySoundOutputState | null = null; @@ -773,6 +780,30 @@ const Settings = ({ i18n.changeLanguage(val as SupportedLocale); }; + const requestedAsioDriver = + keySoundOutput?.requested.kind === 'asio' + ? keySoundOutput.requested.driverName + : null; + const visibleAsioDrivers = + requestedAsioDriver && !asioDrivers.includes(requestedAsioDriver) + ? [...asioDrivers, requestedAsioDriver] + : asioDrivers; + const requestedAsioBuffer = + keySoundOutput?.requested.kind === 'asio' + ? keySoundOutput.requested.bufferSize || DEFAULT_ASIO_BUFFER + : DEFAULT_ASIO_BUFFER; + const visibleAsioBuffers = ASIO_BUFFER_SIZES.some( + (size) => size === requestedAsioBuffer, + ) + ? ASIO_BUFFER_SIZES + : [...ASIO_BUFFER_SIZES, requestedAsioBuffer].sort((a, b) => a - b); + const keySoundOutputErrorKey = + keySoundOutput?.errorCode && + KEY_SOUND_OUTPUT_ERROR_KEYS[keySoundOutput.errorCode]; + const keySoundOutputError = keySoundOutputErrorKey + ? t(keySoundOutputErrorKey) + : keySoundOutput?.error; + return (
({ - value: `asio:${name}`, - // 드라이버 이름이 길면 …로 축약 (기본 항목 라벨은 안 잘리게 max-w 여유, ASIO만 축약) - label: `ASIO: ${ - name.length > 16 ? `${name.slice(0, 16)}…` : name - }`, - })), + ...visibleAsioDrivers.map((name) => { + // 선택한 ASIO가 열기 실패하면 라벨에 ⚠ + 사유 표시 (인라인 경고 대신) + const failed = + name === requestedAsioDriver && !!keySoundOutputError; + return { + value: `asio:${name}`, + // 드라이버 이름이 길면 …로 축약 (기본 항목 라벨은 안 잘리게 max-w 여유, ASIO만 축약) + label: failed + ? `⚠ ${keySoundOutputError}` + : `ASIO: ${ + name.length > 16 + ? `${name.slice(0, 16)}…` + : name + }`, + }; + }), ]} value={ keySoundOutput?.requested.kind === 'asio' @@ -957,15 +997,11 @@ const Settings = ({ {t('settings.keySoundOutputBuffer') || 'ASIO 버퍼 크기'}

({ + options={visibleAsioBuffers.map((size) => ({ value: String(size), label: String(size), }))} - value={String( - (keySoundOutput?.requested.kind === 'asio' && - keySoundOutput.requested.bufferSize) || - DEFAULT_ASIO_BUFFER, - )} + value={String(requestedAsioBuffer)} onChange={handleAsioBufferChange} placeholder={String(DEFAULT_ASIO_BUFFER)} align="right" @@ -973,11 +1009,6 @@ const Settings = ({ disabled={keySoundOutput?.requested.kind !== 'asio'} />
- {keySoundOutput?.error && ( -

- {keySoundOutput.error} -

- )}
{/* 커스텀 CSS & JS 설정 */}
diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index 88e57779..b4063e20 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -24,6 +24,12 @@ "keySoundOutput": "Key Sound Output", "keySoundOutputDefault": "Default Device", "keySoundOutputBuffer": "ASIO Buffer Size", + "keySoundOutputError": { + "asioUnavailableBuild": "This build does not include ASIO support.", + "asioDeviceNotFound": "ASIO device not found.", + "asioOpenFailed": "Could not open the ASIO device, so playback uses the default output.", + "defaultOpenFailed": "No output device could be opened." + }, "topLeft": "Top-Left", "bottomLeft": "Bottom-Left", "topRight": "Top-Right", diff --git a/src/renderer/locales/ko.json b/src/renderer/locales/ko.json index ee4a19c9..def35c69 100644 --- a/src/renderer/locales/ko.json +++ b/src/renderer/locales/ko.json @@ -24,6 +24,12 @@ "keySoundOutput": "키 사운드 출력", "keySoundOutputDefault": "기본 재생 장치", "keySoundOutputBuffer": "ASIO 버퍼 크기", + "keySoundOutputError": { + "asioUnavailableBuild": "ASIO를 지원하지 않는 빌드입니다.", + "asioDeviceNotFound": "ASIO 장치를 찾을 수 없습니다.", + "asioOpenFailed": "ASIO 장치를 열 수 없어 기본 출력으로 재생합니다.", + "defaultOpenFailed": "사용 가능한 출력 장치를 열 수 없습니다." + }, "topLeft": "좌상단", "bottomLeft": "좌하단", "topRight": "우상단", diff --git a/src/renderer/locales/ru.json b/src/renderer/locales/ru.json index 6ed8e994..fcd93610 100644 --- a/src/renderer/locales/ru.json +++ b/src/renderer/locales/ru.json @@ -24,6 +24,12 @@ "keySoundOutput": "Звук клавиш", "keySoundOutputDefault": "По умолчанию", "keySoundOutputBuffer": "Размер буфера ASIO", + "keySoundOutputError": { + "asioUnavailableBuild": "Эта сборка не поддерживает ASIO.", + "asioDeviceNotFound": "Устройство ASIO не найдено.", + "asioOpenFailed": "Не удалось открыть устройство ASIO, используется вывод по умолчанию.", + "defaultOpenFailed": "Не удалось открыть доступное устройство вывода." + }, "topLeft": "Верх-лево", "bottomLeft": "Низ-лево", "topRight": "Верх-право", diff --git a/src/renderer/locales/zh-Hant.json b/src/renderer/locales/zh-Hant.json index b3815167..45a44fdc 100644 --- a/src/renderer/locales/zh-Hant.json +++ b/src/renderer/locales/zh-Hant.json @@ -1,601 +1,607 @@ -{ - "settings": { - "overlayLock": "鎖定懸浮窗", - "alwaysOnTop": "始終置頂", - "noteEffect": "顯示鍵雨", - "laboratory": "啟用實驗性功能", - "trayEnabled": "啟用系統匣模式", - "keyCounter": "顯示按鍵計數器", - "customCSS": "啟用自定義 CSS", - "noCssFile": "(未選擇 CSS 檔案)", - "loadCss": "導入 CSS 檔案", - "customJS": "啟用 JS 插件", - "noJsFile": "(未選擇 JS 檔案)", - "loadJs": "添加插件", - "pluginManageLabel": "插件檔案管理器", - "reloadPlugins": "重新載入", - "managePlugins": "插件管理", - "reloading": "重新載入中...", - "adding": "添加中...", - "managePluginsTitle": "JS 插件管理器", - "noPlugins": "尚未載入任何插件", - "removePlugin": "移除插件", - "resizeAnchor": "調整錨點大小", - "keySoundOutput": "按鍵音輸出", - "keySoundOutputDefault": "預設裝置", - "keySoundOutputBuffer": "ASIO 緩衝區大小", - "topLeft": "左上", - "bottomLeft": "左下", - "topRight": "右上", - "bottomRight": "右下", - "center": "居中", - "fixedPosition": "固定位置", - "graphicsOption": "圖形渲染 (API)", - "renderMode": "選擇渲染模式", - "resetData": "重置資料", - "restartConfirm": "應用設定需要重啟. 是否立即重啟?", - "resetAllConfirm": "確定要重置所有設定嗎?", - "initialize": "初始化", - "cssLoaded": "CSS 檔案已載入.", - "cssLoadFailed": "CSS 檔案載入失敗", - "jsLoaded": "JS 插件已載入.", - "jsLoadFailed": "JS 插件載入失敗", - "jsReloadNoPlugins": "沒有需要重新載入的插件.", - "jsReloadSuccess": "已重新載入 {{count}} 個插件.", - "jsReloadPartial": "已重新載入 {{count}} 個插件, 但部分載入失敗:", - "jsReloadFailed": "重新載入 JS 插件失敗:", - "jsReloadNoChanges": "沒有插件被重新載入.", - "jsAddSuccess": "已添加 {{count}} 個插件.", - "jsAddPartial": "添加了 {{count}} 個插件, 但部分添加失敗:", - "jsAddFailed": "添加 JS 插件失敗:", - "jsPluginToggleFailed": "切換插件狀態失敗.", - "jsPluginRemoveFailed": "移除插件失敗.", - "pluginDataDeleteTitle": "刪除插件資料", - "pluginDataDeleteMessage": "'{{name}}' 插件儲存了資料. 是否要一併刪除?", - "pluginDataDeleteWarning": "刪除資料將永久移除插件設定、歷史記錄等. 此操作無法撤銷.", - "deleteWithData": "包含資料刪除", - "deletePluginOnly": "僅刪除插件", - "language": "語言", - "selectLanguage": "選擇語言", - "shortcuts": "快捷鍵", - "configure": "配置", - "selectAnchor": "選擇錨點", - "overlayLockDesc": "鎖定懸浮窗, 使其無法移動.", - "alwaysOnTopDesc": "使懸浮窗始終顯示在其他視窗之上.", - "noteEffectDesc": "按下按鍵時顯示鍵雨.", - "laboratoryDesc": "嘗試實驗性功能.", - "trayEnabledDesc": "關閉主視窗時不退出程式,而是隱藏到系統匣.", - "autoUpdate": "自動更新", - "autoUpdateDesc": "在新版本提示彈窗中執行自動更新.", - "developerMode": "啟用開發者模式", - "developerModeDesc": "即使在生產版本中也允許打開開發者工具 (DevTools).", - "customCSSDesc": "載入自定義 CSS 檔案以調整懸浮窗樣式.", - "customJSDesc": "運行 JS 插件以實現進階行為.", - "resizeAnchorDesc": "選擇懸浮窗調整大小的錨點.", - "keyCounterDesc": "追蹤並顯示每個按鍵的按下次數.", - "counterResetButton": "重置", - "counterReset": "按鍵計數器已重置.", - "counterResetFailed": "重置按鍵計數器失敗.", - "obsMode": "啟用 OBS 模式", - "obsStart": "啟動", - "obsStop": "停止", - "obsRunning": "執行中", - "obsStopped": "已停止", - "obsClients": "已連線 {{count}} 個用戶端", - "obsCopyUrl": "複製 URL", - "obsCopied": "URL 已複製到剪貼簿。", - "obsStartFailed": "OBS 伺服器啟動失敗。", - "obsStopFailed": "OBS 伺服器停止失敗。", - "obsGuide": "透過 OBS 瀏覽器來源顯示覆蓋層。", - "obsTokenRegenMessage": "是否重新產生工作階段權杖?", - "obsTokenRegenConfirm": "重新產生" - }, - "shortcutSetting": { - "title": "快捷鍵設定", - "sectionOverlay": "懸浮窗", - "sectionCanvas": "畫布", - "toggleOverlay": "切換懸浮窗", - "toggleOverlayHint": "全域快捷鍵, 用於 顯示/隱藏 懸浮窗.", - "toggleOverlayLock": "切換懸浮窗鎖定", - "toggleOverlayLockHint": "全域快捷鍵, 用於 鎖定/解鎖 懸浮窗互動 (在背景工作).", - "toggleAlwaysOnTop": "切換始終置頂", - "toggleAlwaysOnTopHint": "全域快捷鍵,用於切換懸浮窗是否保持在頂部 (在背景工作).", - "switchKeyMode": "切換按鍵模式", - "switchKeyModeHint": "在 4/5/6/8 鍵模式之間切換.", - "toggleSidePanel": "切換側邊面板", - "toggleSidePanelHint": "打開/關閉 畫布右側的側邊面板.", - "zoomIn": "放大", - "zoomInHint": "增大畫布縮放程度.", - "zoomOut": "縮小", - "zoomOutHint": "減小畫布縮放程度.", - "resetZoom": "重置縮放", - "resetZoomHint": "將畫布縮放重置為 100%.", - "listening": "按下按鍵...", - "hint": "右鍵單擊解除綁定. 監聽時: 退格鍵清除, Esc 取消.", - "unassigned": "未分配", - "duplicate": "重複的快捷鍵: \"{{a}}\" 和 \"{{b}}\"", - "reset": "重置", - "cancel": "取消", - "save": "儲存", - "saveFailed": "儲存快捷鍵失敗." - }, - "keySetting": { - "title": "按鍵設定", - "tabKey": "按鍵", - "tabNote": "音符", - "tabCounter": "計數器", - "keyMapping": "按鍵綁定", - "key": "按鍵", - "pressAnyKey": "按下任意鍵", - "clickToSet": "單擊設定按鍵", - "keySize": "按鍵大小", - "size": "大小", - "noteColor": "音符顏色", - "noteWidth": "音符寬度", - "noteOpacity": "音符不透明度", - "noteBorderRadius": "音符圓角半徑", - "noteGlowSize": "發光大小", - "noteGlowOpacity": "發光不透明度", - "noteGlow": "發光效果", - "inactiveState": "閒置", - "activeState": "按下", - "customImage": "自定義圖像", - "configure": "配置", - "className": "類名", - "classPlaceholder": "例如: my-custom-key", - "activeImage": "激活圖像", - "idleImage": "空閒圖像", - "imagePlaceholder": "輸入圖像 URL", - "browse": "瀏覽", - "transparent": "透明", - "idle": "閒置", - "active": "激活", - "save": "應用", - "cancel": "取消", - "noteGlowColor": "發光顏色", - "noteAutoYCorrection": "自動 Y 軸校正", - "noteEffectEnabled": "啟用鍵雨", - "noteAlignment": "鍵雨對齊", - "noteAlignLeft": "靠左", - "noteAlignCenter": "置中", - "noteAlignRight": "靠右", - "noteOffset": "偏移", - "noteOffsetX": "偏移 X", - "noteOffsetY": "偏移 Y", - "noteBorderWidth": "邊框寬度", - "noteBorderColor": "邊框顏色", - "borderSideAll": "全部", - "borderSideVertical": "垂直", - "borderSideHorizontal": "水平" - }, - "imagePicker": { - "idle": "閒置", - "active": "按下", - "transparent": "透明", - "reset": "重置圖像" - }, - "noteSetting": { - "borderRadius": "圓角半徑", - "frameLimit": "幀率限制", - "speed": "速度", - "trackHeight": "軌道高度", - "fade": "淡出", - "reverseEffect": "反向鍵雨", - "save": "應用", - "cancel": "取消" - }, - "common": { - "confirm": "確認", - "cancel": "取消", - "ok": "確定", - "save": "儲存" - }, - "tooltip": { - "github": "GitHub", - "issue": "報告問題", - "move": "移動", - "delete": "刪除", - "add": "添加", - "palette": "畫布", - "resetCurrentTab": "重置", - "exportPreset": "導出預設", - "importExport": "導入/導出", - "overlayClose": "關閉懸浮窗", - "overlayOpen": "打開懸浮窗", - "overlayObsDisabled": "OBS 模式使用中", - "back": "返回", - "settings": "設定", - "etcSettings": "其他設定", - "noteSettings": "音符設定", - "trackSettings": "軌道設定", - "laboratory": "實驗性功能", - "gridSettings": "網格設定" - }, - "toolbar": { - "resetTab": "重置標籤頁", - "resetCounters": "重置計數器", - "addKey": "按鍵", - "addStat": "統計", - "addGraph": "圖表" - }, - "preset": { - "import": "導入", - "export": "導出", - "saveSuccess": "預設已儲存.", - "saveFail": "儲存預設失敗.", - "loadSuccess": "預設已載入.", - "loadFail": "載入預設失敗.", - "importAll": "全部導入", - "importTab": "導入到當前標籤", - "exportAll": "全部導出", - "exportTab": "導出當前標籤", - "saveTabSuccess": "當前標籤預設已儲存.", - "saveTabFail": "儲存當前標籤預設失敗.", - "loadTabSuccess": "預設已載入到當前標籤.", - "loadTabFail": "載入當前標籤預設失敗.", - "loadTabInvalidPreset": "此檔案無法作為當前標籤預設匯入.", - "loadTabAmbiguousSource": "此預設包含多個標籤且無法匹配目前標籤,請使用全部匯入." - }, - "mode": { - "button4": "4 鍵", - "button5": "5 鍵", - "button6": "6 鍵", - "button8": "8 鍵" - }, - "confirm": { - "resetCurrentTab": "重置此標籤頁的設定?", - "resetCountersCurrentTab": "重置此標籤頁的計數器?", - "resetKeyCounter": "重置 [{{name}}] 鍵的計數器?", - "reset": "重置", - "removeKey": "移除 [{{name}}] 鍵?", - - "removeStat": "移除 [{{name}}] 統計?", - "removeGraph": "移除 [{{name}}] 圖表?", - "remove": "移除" - }, - "tabs": { - "empty": "無額外標籤頁", - "createTitle": "建立標籤頁", - "create": "建立", - "delete": "刪除", - "deleteConfirm": "刪除 '{{name}}' 標籤頁?", - "name": { - "placeholder": "例如: 我的自定義", - "required": "請輸入名稱", - "max": "請輸入最多 10 個字符", - "reserved": "不能使用預設標籤頁名稱", - "duplicate": "名稱已存在" - }, - "errors": { - "max": "最多可添加 30 個標籤頁", - "invalid": "無效的名稱", - "createFail": "建立失敗" - } - }, - "laboratory": { - "delayToggle": "強制短音符長度", - "threshold": "短音符閾值", - "minLength": "最短音符長度", - "keyDelay": "按鍵顯示延遲", - "keyDelayAuto": "推薦值: {{value}}ms", - "autoCalc": "自動計算", - "save": "應用", - "cancel": "取消" - }, - "colorPicker": { - "solid": "純色", - "gradient": "漸變", - "idle": "閒置", - "active": "激活" - }, - "contextMenu": { - "duplicateKey": "複製按鍵", - "deleteKey": "刪除按鍵", - "duplicateSelected": "複製所選項目", - "deleteSelected": "刪除所選項目", - "duplicateStat": "複製統計", - "deleteStat": "刪除統計", - "duplicateGraph": "複製圖表", - "deleteGraph": "刪除圖表", - "noteSetting": "音符設定", - "counterSetting": "計數器設定", - "counterReset": "重置計數器", - "bringToFront": "置於頂層", - "sendToBack": "置於底層", - "bringForward": "上移一層", - "sendBackward": "下移一層", - "tabCssSetting": "標籤頁 CSS 設定", - "tabNoteSetting": "標籤頁軌道設定", - "addKey": "新增按鍵", - "addStat": "新增統計", - "addGraph": "新增圖表", - "rename": "重新命名", - "quitApp": "退出程式", - "snapToEdge": "貼靠螢幕邊緣", - "selectTab": "選擇標籤頁", - "groupSelected": "群組", - "renameGroup": "重新命名", - "ungroup": "取消分組", - "removeFromGroup": "從群組中移除" - }, - "layerGroup": { - "defaultName": "群組", - "newGroup": "新群組" - }, - "noteColor": { - "color": "顏色", - "solid": "純色", - "gradient": "漸變", - "gradientBottom": "漸變底部顏色", - "opacity": "不透明度", - "glow": "發光效果", - "glowSize": "大小", - "glowOpacity": "不透明度", - "glowColor": "顏色" - }, - "counterSetting": { - "counterEnabled": "啟用計數器", - "animationEnabled": "計數器動態", - "animation": "動態設定", - "filterAll": "全部", - "filterBuiltin": "內置", - "filterUser": "使用者", - "presetCustom": "自訂", - "searchAnimationPlaceholder": "搜尋", - "noAnimations": "沒有動態", - "createAnimationTitle": "新增動態", - "editAnimationTitle": "編輯動態", - "animationName": "動態名稱", - "animationNamePlaceholder": "動態名稱", - "newAnimationDefaultName": "新動態", - "pressToPreview": "按下預覽", - "motionPerformanceNotice": "動態效果會額外佔用系統資源", - "editAnimation": "編輯", - "deleteAnimation": "刪除", - "deleteAnimationConfirm": "刪除此動態預設?", - "deleteAnimationFailed": "刪除動態預設失敗。", - "loadAnimationFailed": "載入動態清單失敗。", - "saveAnimationFailed": "儲存動態預設失敗。", - "saving": "儲存中...", - "scale": "縮放", - "duration": "持續時間", - "placement": "位置", - "placementArea": "放置區域", - "placementInside": "內部", - "placementOutside": "外部", - "alignDirection": "對齊方向", - "alignMode": "對齊方式", - "alignModeCenter": "置中", - "alignModeBetween": "兩端", - "align": "對齊", - "alignTop": "頂部", - "alignBottom": "底部", - "alignLeft": "左側", - "alignRight": "右側", - "top": "頂部", - "bottom": "底部", - "left": "左側", - "right": "右側", - "start": "起始", - "center": "居中", - "end": "末端", - "gap": "間距", - "colors": "顏色設定", - "fill": "填充", - "stroke": "描邊", - "fillIdle": "填充 (閒置)", - "fillActive": "填充 (激活)", - "strokeIdle": "描邊 (閒置)", - "strokeActive": "描邊 (激活)", - "idle": "閒置", - "active": "按下", - "font": "字體", - "fontSize": "字體大小", - "fontStyle": "字體樣式", - "apply": "應用", - "cancel": "取消" - }, - "tabCss": { - "enableCss": "啟用標籤頁 CSS", - "cssFile": "CSS 檔案", - "noFile": "(未選擇 CSS 檔案)", - "loadFile": "導入", - "remove": "移除", - "loaded": "標籤頁 CSS 檔案已載入.", - "loadFailed": "CSS 檔案載入失敗", - "cleared": "標籤頁 CSS 已移除." - }, - "unifiedSetting": { - "tabKey": "按鍵", - "tabNote": "音符", - "tabCounter": "計數器" - }, - "update": { - "title": "有新版本更新", - "currentVersion": "目前版本", - "latestVersion": "最新版本", - "releaseNotes": "發布說明", - "skipVersion": "跳過此版本", - "goToRelease": "前往發布頁面", - "autoUpdate": "自動更新", - "autoUpdating": "更新中...", - "autoUpdateFailed": "自動更新失敗.", - "later": "稍後", - "checkUpdate": "檢查更新", - "checking": "檢查中...", - "latestAlready": "已是最新版本." - }, - "gridSettings": { - "gridSnapSize": "網格吸附大小", - "overlayPadding": "疊加層邊距", - "alignmentGuides": "對齊參考線", - "spacingGuides": "間距參考線", - "sizeMatchGuides": "尺寸匹配參考線", - "minimapEnabled": "顯示縮略圖", - "save": "儲存", - "cancel": "取消" - }, - "propertiesPanel": { - "noSelection": "未選擇", - "selectHint": "Ctrl+單擊按鍵\n以選擇它", - "multiSelection": "多選", - "multiSelectionHint": "多選時, 單個屬性編輯受到限制.", - "pluginElement": "插件元素", - "pluginHint": "插件元素可以在插件設定中編輯.", - "pluginSettings": "插件設定", - "pluginNoSettings": "無可用設定.", - "pluginMultiSelection": "一次只能編輯一個插件元素.", - "pluginModalHint": "此插件使用設定模態框. 單擊元素以配置它.", - "key": "按鍵", - "transform": "變換", - "position": "位置", - "size": "大小", - "appearance": "外觀", - "backgroundColor": "背景", - "borderColor": "邊框顏色", - "borderWidth": "邊框寬度", - "borderRadius": "圓角半徑", - "typography": "排版", - "fontSize": "字體大小", - "fontColor": "字體顏色", - "image": "圖像", - "imageFit": "顯示", - "imageFitCover": "覆蓋", - "imageFitContain": "包含", - "imageFitFill": "填充", - "imageFitNone": "無", - "note": "音符", - "noteColor": "音符顏色", - "noteOpacity": "音符不透明度", - "advanced": "進階", - "zIndex": "圖層順序", - "useInlineStyles": "內聯樣式優先級", - "useInlineStylesHint": "啟用時, 屬性面板樣式將優先於自定義 CSS.", - "keyMapping": "按鍵映射", - "statType": "統計類型", - "statKpsType": "KPS 類型", - "graphType": "統計類型", - "graphKpsType": "KPS 類型", - "graphShape": "圖表形狀", - "graphShapeLine": "折線", - "graphShapeBar": "柱狀", - "graphShowAverageLine": "顯示平均線", - "graphSpeed": "圖表速度", - "graphColor": "圖表顏色", - "graphAnimation": "圖表動態", - "pressAnyKey": "按下任意鍵", - "clickToSet": "單擊設定", - "customImage": "自定義圖像", - "configure": "配置", - "className": "類名", - "displayText": "文本", - "fontStyle": "字體樣式", - "openPanel": "打開屬性面板", - "closePanel": "關閉屬性面板", - "tabStyle": "樣式", - "tabNote": "音符", - "tabCounter": "計數器", - "tabLayer": "圖層", - "tabGrid": "網格", - "batchEditMode": "批量編輯模式", - "alignment": "對齊", - "alignLeft": "左對齊", - "alignCenterH": "水平居中對齊", - "alignRight": "右對齊", - "alignTop": "頂部對齊", - "alignCenterV": "垂直居中對齊", - "alignBottom": "底部對齊", - "distribution": "分佈", - "distributeH": "水平分佈", - "distributeV": "垂直分佈", - "spacing": "間距", - "width": "寬度", - "height": "高度", - "canvas": "畫布", - "layers": "圖層", - "noLayers": "無圖層", - "switchToLayer": "切換到圖層面板", - "switchToProperty": "切換到屬性面板", - "delete": "刪除", - "hideLayer": "隱藏", - "showLayer": "顯示", - "font": "字體", - "keySound": "聲音設定", - "keySoundEnabled": "啟用按鍵音效", - "selectedSound": "已選音檔", - "soundVolume": "音量", - "import": "匯入", - "loading": "載入中..." - }, - "fontPicker": { - "searchPlaceholder": "搜尋", - "filterAll": "全部", - "filterBuiltin": "內建字體", - "filterLocal": "本地字體", - "filterWeb": "網路字體", - "noFonts": "無字體" - }, - "soundPicker": { - "searchPlaceholder": "搜尋", - "filterAll": "全部", - "filterLocal": "本地音效", - "noSounds": "無音效" - }, - "soundManager": { - "title": "音效", - "noSounds": "無音效", - "addSound": "添加", - "loadFailed": "音效清單載入失敗", - "stateChangeFailed": "音效狀態變更失敗", - "deleteFailed": "音效刪除失敗", - "editSound": "編輯" - }, - "soundTrimModal": { - "defaultTitle": "新增音效", - "editTitle": "編輯音效", - "statusDecoding": "解析中...", - "statusWaiting": "等待檔案", - "statusReady": "可使用", - "nameLabel": "音效名稱", - "namePlaceholder": "音效名稱", - "decodeError": "無法解析音訊檔案。", - "decodingMessage": "正在解析音訊...", - "emptyMessage": "選擇檔案後將顯示波形", - "loadFile": "載入檔案", - "dragHint": "拖曳以編輯範圍", - "saving": "儲存中...", - "submit": "新增", - "submitEdit": "儲存", - "cancel": "取消", - "statusLoading": "載入中...", - "loadOriginalError": "無法載入原始音訊。", - "saveErrorDefault": "無法儲存處理後的音效。", - "saveErrorFailed": "儲存處理後的音效失敗。" - }, - "fontManager": { - "localTab": "本地字體", - "webTab": "網路字體", - "noLocalFonts": "無本地字體", - "noWebFonts": "無網路字體", - "removeFont": "刪除字體", - "adding": "添加中...", - "addFont": "添加" - }, - "webFontInput": { - "cssLabel": "@font-face CSS", - "submit": "儲存", - "update": "修改", - "defaultFileName": "web-font", - "availabilityIdle": "等待輸入", - "availabilityReady": "可用", - "availabilityNotReady": "不可用", - "availabilityInvalidCss": "語法錯誤", - "availabilityMissingFontFace": "無 @font-face", - "availabilityMissingFontFamily": "無 font-family", - "availabilityMissingSrc": "無 src", - "availabilityDuplicateFontFamily": "已註冊的 font-family", - "availabilityMultipleFamilies": "偵測到多個字體", - "duplicateFontFamilyAlert": "\"{{name}}\" 字體已註冊。", - "fixedHint": "您可以添加 @font-face CSS。" - } -} +{ + "settings": { + "overlayLock": "鎖定懸浮窗", + "alwaysOnTop": "始終置頂", + "noteEffect": "顯示鍵雨", + "laboratory": "啟用實驗性功能", + "trayEnabled": "啟用系統匣模式", + "keyCounter": "顯示按鍵計數器", + "customCSS": "啟用自定義 CSS", + "noCssFile": "(未選擇 CSS 檔案)", + "loadCss": "導入 CSS 檔案", + "customJS": "啟用 JS 插件", + "noJsFile": "(未選擇 JS 檔案)", + "loadJs": "添加插件", + "pluginManageLabel": "插件檔案管理器", + "reloadPlugins": "重新載入", + "managePlugins": "插件管理", + "reloading": "重新載入中...", + "adding": "添加中...", + "managePluginsTitle": "JS 插件管理器", + "noPlugins": "尚未載入任何插件", + "removePlugin": "移除插件", + "resizeAnchor": "調整錨點大小", + "keySoundOutput": "按鍵音輸出", + "keySoundOutputDefault": "預設裝置", + "keySoundOutputBuffer": "ASIO 緩衝區大小", + "keySoundOutputError": { + "asioUnavailableBuild": "此版本未包含 ASIO 支援。", + "asioDeviceNotFound": "找不到 ASIO 裝置。", + "asioOpenFailed": "無法開啟 ASIO 裝置,將使用預設輸出播放。", + "defaultOpenFailed": "無法開啟可用的輸出裝置。" + }, + "topLeft": "左上", + "bottomLeft": "左下", + "topRight": "右上", + "bottomRight": "右下", + "center": "居中", + "fixedPosition": "固定位置", + "graphicsOption": "圖形渲染 (API)", + "renderMode": "選擇渲染模式", + "resetData": "重置資料", + "restartConfirm": "應用設定需要重啟. 是否立即重啟?", + "resetAllConfirm": "確定要重置所有設定嗎?", + "initialize": "初始化", + "cssLoaded": "CSS 檔案已載入.", + "cssLoadFailed": "CSS 檔案載入失敗", + "jsLoaded": "JS 插件已載入.", + "jsLoadFailed": "JS 插件載入失敗", + "jsReloadNoPlugins": "沒有需要重新載入的插件.", + "jsReloadSuccess": "已重新載入 {{count}} 個插件.", + "jsReloadPartial": "已重新載入 {{count}} 個插件, 但部分載入失敗:", + "jsReloadFailed": "重新載入 JS 插件失敗:", + "jsReloadNoChanges": "沒有插件被重新載入.", + "jsAddSuccess": "已添加 {{count}} 個插件.", + "jsAddPartial": "添加了 {{count}} 個插件, 但部分添加失敗:", + "jsAddFailed": "添加 JS 插件失敗:", + "jsPluginToggleFailed": "切換插件狀態失敗.", + "jsPluginRemoveFailed": "移除插件失敗.", + "pluginDataDeleteTitle": "刪除插件資料", + "pluginDataDeleteMessage": "'{{name}}' 插件儲存了資料. 是否要一併刪除?", + "pluginDataDeleteWarning": "刪除資料將永久移除插件設定、歷史記錄等. 此操作無法撤銷.", + "deleteWithData": "包含資料刪除", + "deletePluginOnly": "僅刪除插件", + "language": "語言", + "selectLanguage": "選擇語言", + "shortcuts": "快捷鍵", + "configure": "配置", + "selectAnchor": "選擇錨點", + "overlayLockDesc": "鎖定懸浮窗, 使其無法移動.", + "alwaysOnTopDesc": "使懸浮窗始終顯示在其他視窗之上.", + "noteEffectDesc": "按下按鍵時顯示鍵雨.", + "laboratoryDesc": "嘗試實驗性功能.", + "trayEnabledDesc": "關閉主視窗時不退出程式,而是隱藏到系統匣.", + "autoUpdate": "自動更新", + "autoUpdateDesc": "在新版本提示彈窗中執行自動更新.", + "developerMode": "啟用開發者模式", + "developerModeDesc": "即使在生產版本中也允許打開開發者工具 (DevTools).", + "customCSSDesc": "載入自定義 CSS 檔案以調整懸浮窗樣式.", + "customJSDesc": "運行 JS 插件以實現進階行為.", + "resizeAnchorDesc": "選擇懸浮窗調整大小的錨點.", + "keyCounterDesc": "追蹤並顯示每個按鍵的按下次數.", + "counterResetButton": "重置", + "counterReset": "按鍵計數器已重置.", + "counterResetFailed": "重置按鍵計數器失敗.", + "obsMode": "啟用 OBS 模式", + "obsStart": "啟動", + "obsStop": "停止", + "obsRunning": "執行中", + "obsStopped": "已停止", + "obsClients": "已連線 {{count}} 個用戶端", + "obsCopyUrl": "複製 URL", + "obsCopied": "URL 已複製到剪貼簿。", + "obsStartFailed": "OBS 伺服器啟動失敗。", + "obsStopFailed": "OBS 伺服器停止失敗。", + "obsGuide": "透過 OBS 瀏覽器來源顯示覆蓋層。", + "obsTokenRegenMessage": "是否重新產生工作階段權杖?", + "obsTokenRegenConfirm": "重新產生" + }, + "shortcutSetting": { + "title": "快捷鍵設定", + "sectionOverlay": "懸浮窗", + "sectionCanvas": "畫布", + "toggleOverlay": "切換懸浮窗", + "toggleOverlayHint": "全域快捷鍵, 用於 顯示/隱藏 懸浮窗.", + "toggleOverlayLock": "切換懸浮窗鎖定", + "toggleOverlayLockHint": "全域快捷鍵, 用於 鎖定/解鎖 懸浮窗互動 (在背景工作).", + "toggleAlwaysOnTop": "切換始終置頂", + "toggleAlwaysOnTopHint": "全域快捷鍵,用於切換懸浮窗是否保持在頂部 (在背景工作).", + "switchKeyMode": "切換按鍵模式", + "switchKeyModeHint": "在 4/5/6/8 鍵模式之間切換.", + "toggleSidePanel": "切換側邊面板", + "toggleSidePanelHint": "打開/關閉 畫布右側的側邊面板.", + "zoomIn": "放大", + "zoomInHint": "增大畫布縮放程度.", + "zoomOut": "縮小", + "zoomOutHint": "減小畫布縮放程度.", + "resetZoom": "重置縮放", + "resetZoomHint": "將畫布縮放重置為 100%.", + "listening": "按下按鍵...", + "hint": "右鍵單擊解除綁定. 監聽時: 退格鍵清除, Esc 取消.", + "unassigned": "未分配", + "duplicate": "重複的快捷鍵: \"{{a}}\" 和 \"{{b}}\"", + "reset": "重置", + "cancel": "取消", + "save": "儲存", + "saveFailed": "儲存快捷鍵失敗." + }, + "keySetting": { + "title": "按鍵設定", + "tabKey": "按鍵", + "tabNote": "音符", + "tabCounter": "計數器", + "keyMapping": "按鍵綁定", + "key": "按鍵", + "pressAnyKey": "按下任意鍵", + "clickToSet": "單擊設定按鍵", + "keySize": "按鍵大小", + "size": "大小", + "noteColor": "音符顏色", + "noteWidth": "音符寬度", + "noteOpacity": "音符不透明度", + "noteBorderRadius": "音符圓角半徑", + "noteGlowSize": "發光大小", + "noteGlowOpacity": "發光不透明度", + "noteGlow": "發光效果", + "inactiveState": "閒置", + "activeState": "按下", + "customImage": "自定義圖像", + "configure": "配置", + "className": "類名", + "classPlaceholder": "例如: my-custom-key", + "activeImage": "激活圖像", + "idleImage": "空閒圖像", + "imagePlaceholder": "輸入圖像 URL", + "browse": "瀏覽", + "transparent": "透明", + "idle": "閒置", + "active": "激活", + "save": "應用", + "cancel": "取消", + "noteGlowColor": "發光顏色", + "noteAutoYCorrection": "自動 Y 軸校正", + "noteEffectEnabled": "啟用鍵雨", + "noteAlignment": "鍵雨對齊", + "noteAlignLeft": "靠左", + "noteAlignCenter": "置中", + "noteAlignRight": "靠右", + "noteOffset": "偏移", + "noteOffsetX": "偏移 X", + "noteOffsetY": "偏移 Y", + "noteBorderWidth": "邊框寬度", + "noteBorderColor": "邊框顏色", + "borderSideAll": "全部", + "borderSideVertical": "垂直", + "borderSideHorizontal": "水平" + }, + "imagePicker": { + "idle": "閒置", + "active": "按下", + "transparent": "透明", + "reset": "重置圖像" + }, + "noteSetting": { + "borderRadius": "圓角半徑", + "frameLimit": "幀率限制", + "speed": "速度", + "trackHeight": "軌道高度", + "fade": "淡出", + "reverseEffect": "反向鍵雨", + "save": "應用", + "cancel": "取消" + }, + "common": { + "confirm": "確認", + "cancel": "取消", + "ok": "確定", + "save": "儲存" + }, + "tooltip": { + "github": "GitHub", + "issue": "報告問題", + "move": "移動", + "delete": "刪除", + "add": "添加", + "palette": "畫布", + "resetCurrentTab": "重置", + "exportPreset": "導出預設", + "importExport": "導入/導出", + "overlayClose": "關閉懸浮窗", + "overlayOpen": "打開懸浮窗", + "overlayObsDisabled": "OBS 模式使用中", + "back": "返回", + "settings": "設定", + "etcSettings": "其他設定", + "noteSettings": "音符設定", + "trackSettings": "軌道設定", + "laboratory": "實驗性功能", + "gridSettings": "網格設定" + }, + "toolbar": { + "resetTab": "重置標籤頁", + "resetCounters": "重置計數器", + "addKey": "按鍵", + "addStat": "統計", + "addGraph": "圖表" + }, + "preset": { + "import": "導入", + "export": "導出", + "saveSuccess": "預設已儲存.", + "saveFail": "儲存預設失敗.", + "loadSuccess": "預設已載入.", + "loadFail": "載入預設失敗.", + "importAll": "全部導入", + "importTab": "導入到當前標籤", + "exportAll": "全部導出", + "exportTab": "導出當前標籤", + "saveTabSuccess": "當前標籤預設已儲存.", + "saveTabFail": "儲存當前標籤預設失敗.", + "loadTabSuccess": "預設已載入到當前標籤.", + "loadTabFail": "載入當前標籤預設失敗.", + "loadTabInvalidPreset": "此檔案無法作為當前標籤預設匯入.", + "loadTabAmbiguousSource": "此預設包含多個標籤且無法匹配目前標籤,請使用全部匯入." + }, + "mode": { + "button4": "4 鍵", + "button5": "5 鍵", + "button6": "6 鍵", + "button8": "8 鍵" + }, + "confirm": { + "resetCurrentTab": "重置此標籤頁的設定?", + "resetCountersCurrentTab": "重置此標籤頁的計數器?", + "resetKeyCounter": "重置 [{{name}}] 鍵的計數器?", + "reset": "重置", + "removeKey": "移除 [{{name}}] 鍵?", + + "removeStat": "移除 [{{name}}] 統計?", + "removeGraph": "移除 [{{name}}] 圖表?", + "remove": "移除" + }, + "tabs": { + "empty": "無額外標籤頁", + "createTitle": "建立標籤頁", + "create": "建立", + "delete": "刪除", + "deleteConfirm": "刪除 '{{name}}' 標籤頁?", + "name": { + "placeholder": "例如: 我的自定義", + "required": "請輸入名稱", + "max": "請輸入最多 10 個字符", + "reserved": "不能使用預設標籤頁名稱", + "duplicate": "名稱已存在" + }, + "errors": { + "max": "最多可添加 30 個標籤頁", + "invalid": "無效的名稱", + "createFail": "建立失敗" + } + }, + "laboratory": { + "delayToggle": "強制短音符長度", + "threshold": "短音符閾值", + "minLength": "最短音符長度", + "keyDelay": "按鍵顯示延遲", + "keyDelayAuto": "推薦值: {{value}}ms", + "autoCalc": "自動計算", + "save": "應用", + "cancel": "取消" + }, + "colorPicker": { + "solid": "純色", + "gradient": "漸變", + "idle": "閒置", + "active": "激活" + }, + "contextMenu": { + "duplicateKey": "複製按鍵", + "deleteKey": "刪除按鍵", + "duplicateSelected": "複製所選項目", + "deleteSelected": "刪除所選項目", + "duplicateStat": "複製統計", + "deleteStat": "刪除統計", + "duplicateGraph": "複製圖表", + "deleteGraph": "刪除圖表", + "noteSetting": "音符設定", + "counterSetting": "計數器設定", + "counterReset": "重置計數器", + "bringToFront": "置於頂層", + "sendToBack": "置於底層", + "bringForward": "上移一層", + "sendBackward": "下移一層", + "tabCssSetting": "標籤頁 CSS 設定", + "tabNoteSetting": "標籤頁軌道設定", + "addKey": "新增按鍵", + "addStat": "新增統計", + "addGraph": "新增圖表", + "rename": "重新命名", + "quitApp": "退出程式", + "snapToEdge": "貼靠螢幕邊緣", + "selectTab": "選擇標籤頁", + "groupSelected": "群組", + "renameGroup": "重新命名", + "ungroup": "取消分組", + "removeFromGroup": "從群組中移除" + }, + "layerGroup": { + "defaultName": "群組", + "newGroup": "新群組" + }, + "noteColor": { + "color": "顏色", + "solid": "純色", + "gradient": "漸變", + "gradientBottom": "漸變底部顏色", + "opacity": "不透明度", + "glow": "發光效果", + "glowSize": "大小", + "glowOpacity": "不透明度", + "glowColor": "顏色" + }, + "counterSetting": { + "counterEnabled": "啟用計數器", + "animationEnabled": "計數器動態", + "animation": "動態設定", + "filterAll": "全部", + "filterBuiltin": "內置", + "filterUser": "使用者", + "presetCustom": "自訂", + "searchAnimationPlaceholder": "搜尋", + "noAnimations": "沒有動態", + "createAnimationTitle": "新增動態", + "editAnimationTitle": "編輯動態", + "animationName": "動態名稱", + "animationNamePlaceholder": "動態名稱", + "newAnimationDefaultName": "新動態", + "pressToPreview": "按下預覽", + "motionPerformanceNotice": "動態效果會額外佔用系統資源", + "editAnimation": "編輯", + "deleteAnimation": "刪除", + "deleteAnimationConfirm": "刪除此動態預設?", + "deleteAnimationFailed": "刪除動態預設失敗。", + "loadAnimationFailed": "載入動態清單失敗。", + "saveAnimationFailed": "儲存動態預設失敗。", + "saving": "儲存中...", + "scale": "縮放", + "duration": "持續時間", + "placement": "位置", + "placementArea": "放置區域", + "placementInside": "內部", + "placementOutside": "外部", + "alignDirection": "對齊方向", + "alignMode": "對齊方式", + "alignModeCenter": "置中", + "alignModeBetween": "兩端", + "align": "對齊", + "alignTop": "頂部", + "alignBottom": "底部", + "alignLeft": "左側", + "alignRight": "右側", + "top": "頂部", + "bottom": "底部", + "left": "左側", + "right": "右側", + "start": "起始", + "center": "居中", + "end": "末端", + "gap": "間距", + "colors": "顏色設定", + "fill": "填充", + "stroke": "描邊", + "fillIdle": "填充 (閒置)", + "fillActive": "填充 (激活)", + "strokeIdle": "描邊 (閒置)", + "strokeActive": "描邊 (激活)", + "idle": "閒置", + "active": "按下", + "font": "字體", + "fontSize": "字體大小", + "fontStyle": "字體樣式", + "apply": "應用", + "cancel": "取消" + }, + "tabCss": { + "enableCss": "啟用標籤頁 CSS", + "cssFile": "CSS 檔案", + "noFile": "(未選擇 CSS 檔案)", + "loadFile": "導入", + "remove": "移除", + "loaded": "標籤頁 CSS 檔案已載入.", + "loadFailed": "CSS 檔案載入失敗", + "cleared": "標籤頁 CSS 已移除." + }, + "unifiedSetting": { + "tabKey": "按鍵", + "tabNote": "音符", + "tabCounter": "計數器" + }, + "update": { + "title": "有新版本更新", + "currentVersion": "目前版本", + "latestVersion": "最新版本", + "releaseNotes": "發布說明", + "skipVersion": "跳過此版本", + "goToRelease": "前往發布頁面", + "autoUpdate": "自動更新", + "autoUpdating": "更新中...", + "autoUpdateFailed": "自動更新失敗.", + "later": "稍後", + "checkUpdate": "檢查更新", + "checking": "檢查中...", + "latestAlready": "已是最新版本." + }, + "gridSettings": { + "gridSnapSize": "網格吸附大小", + "overlayPadding": "疊加層邊距", + "alignmentGuides": "對齊參考線", + "spacingGuides": "間距參考線", + "sizeMatchGuides": "尺寸匹配參考線", + "minimapEnabled": "顯示縮略圖", + "save": "儲存", + "cancel": "取消" + }, + "propertiesPanel": { + "noSelection": "未選擇", + "selectHint": "Ctrl+單擊按鍵\n以選擇它", + "multiSelection": "多選", + "multiSelectionHint": "多選時, 單個屬性編輯受到限制.", + "pluginElement": "插件元素", + "pluginHint": "插件元素可以在插件設定中編輯.", + "pluginSettings": "插件設定", + "pluginNoSettings": "無可用設定.", + "pluginMultiSelection": "一次只能編輯一個插件元素.", + "pluginModalHint": "此插件使用設定模態框. 單擊元素以配置它.", + "key": "按鍵", + "transform": "變換", + "position": "位置", + "size": "大小", + "appearance": "外觀", + "backgroundColor": "背景", + "borderColor": "邊框顏色", + "borderWidth": "邊框寬度", + "borderRadius": "圓角半徑", + "typography": "排版", + "fontSize": "字體大小", + "fontColor": "字體顏色", + "image": "圖像", + "imageFit": "顯示", + "imageFitCover": "覆蓋", + "imageFitContain": "包含", + "imageFitFill": "填充", + "imageFitNone": "無", + "note": "音符", + "noteColor": "音符顏色", + "noteOpacity": "音符不透明度", + "advanced": "進階", + "zIndex": "圖層順序", + "useInlineStyles": "內聯樣式優先級", + "useInlineStylesHint": "啟用時, 屬性面板樣式將優先於自定義 CSS.", + "keyMapping": "按鍵映射", + "statType": "統計類型", + "statKpsType": "KPS 類型", + "graphType": "統計類型", + "graphKpsType": "KPS 類型", + "graphShape": "圖表形狀", + "graphShapeLine": "折線", + "graphShapeBar": "柱狀", + "graphShowAverageLine": "顯示平均線", + "graphSpeed": "圖表速度", + "graphColor": "圖表顏色", + "graphAnimation": "圖表動態", + "pressAnyKey": "按下任意鍵", + "clickToSet": "單擊設定", + "customImage": "自定義圖像", + "configure": "配置", + "className": "類名", + "displayText": "文本", + "fontStyle": "字體樣式", + "openPanel": "打開屬性面板", + "closePanel": "關閉屬性面板", + "tabStyle": "樣式", + "tabNote": "音符", + "tabCounter": "計數器", + "tabLayer": "圖層", + "tabGrid": "網格", + "batchEditMode": "批量編輯模式", + "alignment": "對齊", + "alignLeft": "左對齊", + "alignCenterH": "水平居中對齊", + "alignRight": "右對齊", + "alignTop": "頂部對齊", + "alignCenterV": "垂直居中對齊", + "alignBottom": "底部對齊", + "distribution": "分佈", + "distributeH": "水平分佈", + "distributeV": "垂直分佈", + "spacing": "間距", + "width": "寬度", + "height": "高度", + "canvas": "畫布", + "layers": "圖層", + "noLayers": "無圖層", + "switchToLayer": "切換到圖層面板", + "switchToProperty": "切換到屬性面板", + "delete": "刪除", + "hideLayer": "隱藏", + "showLayer": "顯示", + "font": "字體", + "keySound": "聲音設定", + "keySoundEnabled": "啟用按鍵音效", + "selectedSound": "已選音檔", + "soundVolume": "音量", + "import": "匯入", + "loading": "載入中..." + }, + "fontPicker": { + "searchPlaceholder": "搜尋", + "filterAll": "全部", + "filterBuiltin": "內建字體", + "filterLocal": "本地字體", + "filterWeb": "網路字體", + "noFonts": "無字體" + }, + "soundPicker": { + "searchPlaceholder": "搜尋", + "filterAll": "全部", + "filterLocal": "本地音效", + "noSounds": "無音效" + }, + "soundManager": { + "title": "音效", + "noSounds": "無音效", + "addSound": "添加", + "loadFailed": "音效清單載入失敗", + "stateChangeFailed": "音效狀態變更失敗", + "deleteFailed": "音效刪除失敗", + "editSound": "編輯" + }, + "soundTrimModal": { + "defaultTitle": "新增音效", + "editTitle": "編輯音效", + "statusDecoding": "解析中...", + "statusWaiting": "等待檔案", + "statusReady": "可使用", + "nameLabel": "音效名稱", + "namePlaceholder": "音效名稱", + "decodeError": "無法解析音訊檔案。", + "decodingMessage": "正在解析音訊...", + "emptyMessage": "選擇檔案後將顯示波形", + "loadFile": "載入檔案", + "dragHint": "拖曳以編輯範圍", + "saving": "儲存中...", + "submit": "新增", + "submitEdit": "儲存", + "cancel": "取消", + "statusLoading": "載入中...", + "loadOriginalError": "無法載入原始音訊。", + "saveErrorDefault": "無法儲存處理後的音效。", + "saveErrorFailed": "儲存處理後的音效失敗。" + }, + "fontManager": { + "localTab": "本地字體", + "webTab": "網路字體", + "noLocalFonts": "無本地字體", + "noWebFonts": "無網路字體", + "removeFont": "刪除字體", + "adding": "添加中...", + "addFont": "添加" + }, + "webFontInput": { + "cssLabel": "@font-face CSS", + "submit": "儲存", + "update": "修改", + "defaultFileName": "web-font", + "availabilityIdle": "等待輸入", + "availabilityReady": "可用", + "availabilityNotReady": "不可用", + "availabilityInvalidCss": "語法錯誤", + "availabilityMissingFontFace": "無 @font-face", + "availabilityMissingFontFamily": "無 font-family", + "availabilityMissingSrc": "無 src", + "availabilityDuplicateFontFamily": "已註冊的 font-family", + "availabilityMultipleFamilies": "偵測到多個字體", + "duplicateFontFamilyAlert": "\"{{name}}\" 字體已註冊。", + "fixedHint": "您可以添加 @font-face CSS。" + } +} diff --git a/src/renderer/locales/zh-cn.json b/src/renderer/locales/zh-cn.json index c660a09b..11460517 100644 --- a/src/renderer/locales/zh-cn.json +++ b/src/renderer/locales/zh-cn.json @@ -24,6 +24,12 @@ "keySoundOutput": "按键音输出", "keySoundOutputDefault": "默认设备", "keySoundOutputBuffer": "ASIO 缓冲区大小", + "keySoundOutputError": { + "asioUnavailableBuild": "此版本未包含 ASIO 支持。", + "asioDeviceNotFound": "找不到 ASIO 设备。", + "asioOpenFailed": "无法打开 ASIO 设备,将使用默认输出播放。", + "defaultOpenFailed": "无法打开可用的输出设备。" + }, "topLeft": "左上", "bottomLeft": "左下", "topRight": "右上", From a0238bba58e1490088ff84a90f9c19d2646a962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 6 Jun 2026 17:53:38 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=ED=82=A4=EC=9D=8C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C=20=EB=B3=BC=EB=A5=A8=20=EC=B5=9C=EB=8C=80=20?= =?UTF-8?q?200%=EA=B9=8C=EC=A7=80=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 키별 사운드 볼륨을 100% → 200%까지 올릴 수 있게 함(작은 사운드 증폭용). - 볼륨 슬라이더/입력 상한 200 (단일·다중 선택) - 백엔드 증폭 클램프 1.0 → 2.0 (app_state, engine ×2) - 트림 편집 미리듣기도 200%까지 --- src-tauri/src/audio/engine.rs | 4 ++-- src-tauri/src/state/app_state.rs | 2 +- .../main/Grid/PropertiesPanel/batch/BatchStyleTabContent.tsx | 4 ++-- .../main/Grid/PropertiesPanel/single/StyleTabContent.tsx | 4 ++-- .../components/main/Modal/content/managers/SoundTrimModal.tsx | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs index 7cab1471..100683b0 100644 --- a/src-tauri/src/audio/engine.rs +++ b/src-tauri/src/audio/engine.rs @@ -450,7 +450,7 @@ impl KeySoundEngine { let _ = self.sender.send(AudioCommand::PlayFile { path: trimmed.to_string(), - per_key_volume: per_key_volume.clamp(0.0, 1.0), + per_key_volume: per_key_volume.clamp(0.0, 2.0), queued_at: Instant::now(), trace, }); @@ -610,7 +610,7 @@ fn audio_thread(receiver: Receiver, state: Arc = ({ onChange={(value) => soundChangeComplete( 'soundVolume', - Math.max(0, Math.min(100, value)), + Math.max(0, Math.min(200, value)), ) } suffix="%" min={0} - max={100} + max={200} isMixed={ soundMixedValue((pos) => pos.soundVolume, 100).isMixed } diff --git a/src/renderer/components/main/Grid/PropertiesPanel/single/StyleTabContent.tsx b/src/renderer/components/main/Grid/PropertiesPanel/single/StyleTabContent.tsx index 4df3d31c..422c840a 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/single/StyleTabContent.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/single/StyleTabContent.tsx @@ -712,12 +712,12 @@ const StyleTabContent: React.FC = ({ onChange={(value) => handleStyleChangeComplete( 'soundVolume', - Math.max(0, Math.min(100, value)), + Math.max(0, Math.min(200, value)), ) } suffix="%" min={0} - max={100} + max={200} /> diff --git a/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx b/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx index 35f75e8f..553fc167 100644 --- a/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx +++ b/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx @@ -438,7 +438,7 @@ const SoundTrimModal = ({ const ctx = createAudioContext(); const gainNode = ctx.createGain(); - gainNode.gain.value = clamp(previewVolume / 100, 0, 1); + gainNode.gain.value = clamp(previewVolume / 100, 0, 2); gainNode.connect(ctx.destination); const source = ctx.createBufferSource(); source.buffer = audioBuffer; From c0f101a130c2509680c144d2ad3b45b902baeea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 6 Jun 2026 18:44:48 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=ED=82=A4=EC=9D=8C=20=EB=B3=BC?= =?UTF-8?q?=EB=A5=A8=20=EC=83=81=ED=95=9C=20=EC=8A=A4=ED=82=A4=EB=A7=88=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/models/mod.rs | 2 +- src/types/key/keys.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index c02a6305..231a68d9 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -195,7 +195,7 @@ pub struct KeyPosition { /// 키 입력 시 재생할 로컬 사운드 파일 경로 #[serde(default)] pub sound_path: Option, - /// 키별 사운드 볼륨 (0~100, 기본값 100) + /// 키별 사운드 볼륨 (0~200, 기본값 100) #[serde(default)] pub sound_volume: Option, #[serde(default)] diff --git a/src/types/key/keys.ts b/src/types/key/keys.ts index ab7c801e..e2d2298a 100644 --- a/src/types/key/keys.ts +++ b/src/types/key/keys.ts @@ -237,7 +237,7 @@ export const keyPositionSchema = z.object({ inactiveImage: z.string().optional().or(z.literal('')), soundEnabled: z.boolean().optional(), soundPath: z.string().optional().or(z.literal('')), - soundVolume: z.number().min(0).max(100).optional(), + soundVolume: z.number().min(0).max(200).optional(), activeTransparent: z.boolean().optional(), idleTransparent: z.boolean().optional(), count: z.number().int().nonnegative(), From f6922f06a9b47585609a12d78f3ce1de6e219890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 6 Jun 2026 18:45:02 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=ED=82=A4=EC=9D=8C=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=EC=86=8C=ED=94=84=ED=8A=B8=20=EB=A6=AC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/audio/engine.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs index 100683b0..250505a3 100644 --- a/src-tauri/src/audio/engine.rs +++ b/src-tauri/src/audio/engine.rs @@ -979,7 +979,7 @@ fn play_on_stream( return false; }; - handler.sink.mixer().add(source.amplify(volume)); + handler.sink.mixer().add(source.with_gain(volume)); true } @@ -1213,18 +1213,38 @@ struct AudioSource { samples: Arc<[f32]>, channels: u16, sample_rate: u32, + gain: f32, pos: usize, } +/// 천장(1.0) 근처에서 부드럽게 수렴시키는 소프트 리미터 +/// knee 미만은 그대로 통과(일반 볼륨 무영향), 초과분만 1.0으로 압축 +fn soft_limit_sample(x: f32) -> f32 { + const KNEE: f32 = 0.95; + let mag = x.abs(); + if mag <= KNEE { + return x; + } + let over = (mag - KNEE) / (1.0 - KNEE); + let limited = KNEE + (1.0 - KNEE) * over.tanh(); + limited.copysign(x) +} + impl AudioSource { fn new(samples: Arc<[f32]>, channels: u16, sample_rate: u32) -> Self { Self { samples, channels, sample_rate, + gain: 1.0, pos: 0, } } + + fn with_gain(mut self, gain: f32) -> Self { + self.gain = gain; + self + } } impl Iterator for AudioSource { @@ -1233,7 +1253,7 @@ impl Iterator for AudioSource { fn next(&mut self) -> Option { let value = self.samples.get(self.pos)?; self.pos += 1; - Some(*value) + Some(soft_limit_sample(*value * self.gain)) } }