fix(tamagotchi): missing shortcuts editor (#190)

This commit is contained in:
LemonNeko
2025-06-01 18:40:27 +08:00
committed by GitHub
parent 7f93a4a71e
commit 11fe66be28
20 changed files with 899 additions and 409 deletions
Generated
+146 -3
View File
@@ -101,6 +101,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-global-shortcut",
"tauri-plugin-log",
"tauri-plugin-mcp",
"tauri-plugin-os",
@@ -1151,13 +1152,23 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
dependencies = [
"libc",
"windows-targets 0.48.5",
]
[[package]]
name = "gethostname"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55"
dependencies = [
"rustix",
"rustix 1.0.7",
"windows-targets 0.52.6",
]
@@ -1286,6 +1297,24 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "global-hotkey"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
dependencies = [
"crossbeam-channel",
"keyboard-types",
"objc2 0.6.1",
"objc2-app-kit",
"once_cell",
"serde",
"thiserror 2.0.12",
"windows-sys 0.59.0",
"x11rb",
"xkeysym",
]
[[package]]
name = "gobject-sys"
version = "0.18.0"
@@ -1863,6 +1892,12 @@ dependencies = [
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
@@ -2976,6 +3011,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.0.7"
@@ -2985,7 +3033,7 @@ dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.9.4",
"windows-sys 0.59.0",
]
@@ -3677,6 +3725,21 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-global-shortcut"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31919f3c07bcb585afef217c0c33cde80da9ebccf5b8e2c90e0e0a535b14ab47"
dependencies = [
"global-hotkey",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
]
[[package]]
name = "tauri-plugin-log"
version = "2.4.0"
@@ -3719,7 +3782,7 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "424f19432397850c2ddd42aa58078630c15287bbce3866eb1d90e7dbee680637"
dependencies = [
"gethostname",
"gethostname 1.0.2",
"log",
"os_info",
"serde",
@@ -4729,6 +4792,21 @@ dependencies = [
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -4769,6 +4847,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -4781,6 +4865,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -4793,6 +4883,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -4811,6 +4907,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -4823,6 +4925,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -4835,6 +4943,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -4847,6 +4961,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -4970,6 +5090,29 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "x11rb"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
dependencies = [
"gethostname 0.4.3",
"rustix 0.38.44",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
[[package]]
name = "xkeysym"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "yoke"
version = "0.8.0"
+1 -1
View File
@@ -42,7 +42,7 @@ Have you dreamed about having a cyber living being (cyber waifu / husbando, digi
With the power of modern large langauge models like [ChatGPT](https://chatgpt.com), and famous [Claude](https://claude.ai), asking a virtual being to have role playing and chat with us is already easy enough for everyone. Platforms like [Character.ai (a.k.a. c.ai)](https://character.ai) and [JanitorAI](https://janitorai.com/), and local playgrounds like [SillyTavern](https://github.com/SillyTavern/SillyTavern) is already a well-enough solution for chat based, or visuali adventure game like experience.
But, what about the abilities to play games? And see what you are coding at? Chatting while playing games, watching videos, and capable of doing many other things Perhaps you know [Neuro-sama](https://www.youtube.com/@Neurosama) already, she is currenly the best companion capable of playing games, chatting, and interacting with you and the participants (in VTuber community), some call this kind of being, "digital human" too. **Sadly, it's not open sourced, you cannot interact with her after she went offline from live stream**.
But, what about the abilities to play games? And see what you are coding at? Chatting while playing games, watching videos, and capable of doing many other things Perhaps you know [Neuro-sama](https://www.youtube.com/@Neurosama) already, she is currenly the best companion capable of playing games, chatting, and interacting with you and the participants (in VTuber community), some call this kind of being, "digital human" too. **Sadly, it's not open sourced, you cannot interact with her after she went offline from live stream**.
Therefore, this project, AIRI, offers another possibilty here: **let you own yours, easily, anywhere, anytime**.
+21
View File
@@ -354,7 +354,16 @@ settings:
description: Configure the environment where the character lives
title: Scene
themes:
color-scheme:
description: Change the color scheme of the stage.
title: Color Scheme
description: Customize your stage!
developer:
description: Some developer options.
title: Developers
general:
description: Dark theme, languages, etc.
title: General
sections:
section:
custom-color:
@@ -431,7 +440,19 @@ settings:
porcelain and paintings
title: Chinese Traditional Colors
title: Color Scheme Presets
window-shortcuts:
title: Keyboard Shortcuts
title: Appearance
window-shortcuts:
description: Modify the window shortcuts.
title: Window Shortcuts
toggle-move:
label: Toggle Move
toggle-resize:
label: Toggle Resize
toggle-ignore-mouse-event:
label: Toggle Ignore Mouse Event
press-keys: Press Keys...
sections:
section:
general:
+1
View File
@@ -27,6 +27,7 @@
"@proj-airi/ui": "workspace:^",
"@ricky0123/vad-web": "^0.0.24",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
"@tauri-apps/plugin-os": ">=2.2.1",
"@tresjs/cientos": "^4.3.1",
"@tresjs/core": "^4.3.5",
@@ -50,3 +50,6 @@ objc2-foundation = "0.3.1"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.61.1", features = ["Win32_UI_WindowsAndMessaging", "Win32_Foundation"] }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2.0.0"
@@ -10,6 +10,12 @@
"os:default",
"core:window:default",
"core:window:allow-start-dragging",
"mcp:default"
"core:window:allow-set-focus",
"core:window:allow-set-minimizable",
"mcp:default",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all"
]
}
@@ -56,17 +56,3 @@ pub async fn is_cursor_in_window(window: &tauri::Window) -> bool {
}
false
}
/// Check if modifier key is pressed (Option/Alt or Control)
#[cfg(target_os = "macos")]
pub fn is_modifier_pressed() -> bool {
unsafe {
let event_class: &'static objc2::runtime::AnyClass = class!(NSEvent);
let flags: u64 = msg_send![event_class, modifierFlags];
// Check for Alt/Option (1 << 19) or Control (1 << 18)
// let ctrl_pressed = (flags & (1 << 18)) != 0;
// alt_pressed || ctrl_pressed
(flags & (1 << 19)) != 0
}
}
+13 -7
View File
@@ -17,9 +17,9 @@ mod app_windows;
mod commands;
#[cfg(target_os = "macos")]
use app_click_through::native_macos::{is_cursor_in_window, is_modifier_pressed};
use app_click_through::native_macos::is_cursor_in_window;
#[cfg(target_os = "windows")]
use app_click_through::native_windows::{is_cursor_in_window, is_modifier_pressed};
use app_click_through::native_windows::is_cursor_in_window;
use app_click_through::state::{set_click_through_enabled, set_cursor_inside, WindowClickThroughState};
#[tauri::command]
@@ -55,23 +55,21 @@ async fn start_monitor_for_clicking_through(window: tauri::Window) -> Result<(),
#[cfg(target_os = "macos")]
{
let cursor_inside = is_cursor_in_window(&window).await;
let modifier_pressed = is_modifier_pressed();
// Only allow disabling click-through when:
// 1. Cursor is OUTSIDE the window AND
// 2. Modifier key is pressed
let _ = set_cursor_inside(&window, cursor_inside && !modifier_pressed);
let _ = set_cursor_inside(&window, cursor_inside);
}
#[cfg(target_os = "windows")]
{
let cursor_inside = is_cursor_in_window(&window).await;
let modifier_pressed = is_modifier_pressed();
// Only allow disabling click-through when:
// 1. Cursor is OUTSIDE the window AND
// 2. Modifier key is pressed
let _ = set_cursor_inside(&window, cursor_inside && !modifier_pressed);
let _ = set_cursor_inside(&window, cursor_inside);
}
}
});
@@ -113,6 +111,7 @@ pub fn run() {
.plugin(prevent_default_plugin)
.plugin(tauri_plugin_mcp::Builder.build())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.manage(WindowClickThroughState::default())
.setup(|app| {
let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
@@ -140,7 +139,8 @@ pub fn run() {
let settings_item = MenuItem::with_id(app, "settings", "Settings", true, None::<&str>)?;
let hide_item = MenuItem::with_id(app, "hide", "Hide", true, None::<&str>)?;
let show_item = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&settings_item, &hide_item, &show_item, &quit_item])?;
let show_devtools_item = MenuItem::with_id(app, "show-devtools", "Show Devtools", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&settings_item, &hide_item, &show_item, &show_devtools_item, &quit_item])?;
let _ = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone()) // TODO: use custom icon
@@ -173,6 +173,12 @@ pub fn run() {
let _ = window.show();
}
}
"show-devtools" => {
let window = app.get_webview_window("main");
if let Some(window) = window {
window.open_devtools();
}
}
_ => {}
})
.show_menu_on_left_click(true)
@@ -1,56 +1,88 @@
import type { EffectScope } from 'vue'
import type { ShortcutEvent } from '@tauri-apps/plugin-global-shortcut'
import { useMagicKeys, whenever } from '@vueuse/core'
import { register, unregisterAll } from '@tauri-apps/plugin-global-shortcut'
import { storeToRefs } from 'pinia'
import { computed, effectScope, watch } from 'vue'
import { computed, watch } from 'vue'
import { useShortcutsStore } from '../stores/shortcuts'
import { useWindowControlStore } from '../stores/window-controls'
import { WindowControlMode } from '../types/window-controls'
import { startClickThrough, stopClickThrough } from '../utils/windows'
export function useWindowShortcuts() {
const windowStore = useWindowControlStore()
const magicKeys = useMagicKeys()
const { shortcuts } = storeToRefs(useShortcutsStore())
const handlers = computed(() => [
{
handle: () => {
handle: async (event: ShortcutEvent) => {
if (event.state !== 'Pressed') {
return
}
windowStore.setMode(WindowControlMode.MOVE)
windowStore.toggleControl()
},
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'move')?.shortcut,
},
{
handle: () => {
handle: async (event: ShortcutEvent) => {
if (event.state !== 'Pressed') {
return
}
windowStore.setMode(WindowControlMode.RESIZE)
windowStore.toggleControl()
},
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'resize')?.shortcut,
},
{
handle: () => {
handle: async (event: ShortcutEvent) => {
if (event.state !== 'Pressed') {
return
}
windowStore.setMode(WindowControlMode.DEBUG)
windowStore.toggleControl()
},
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'debug')?.shortcut,
},
])
let currentScope: EffectScope | null = null
watch(handlers, () => {
if (currentScope) {
currentScope.stop()
}
currentScope = effectScope()
currentScope.run(() => {
handlers.value.forEach((handler) => {
if (!handler.shortcut) {
{
handle: async (event: ShortcutEvent) => {
if (event.state === 'Pressed') {
stopClickThrough()
return
}
whenever(magicKeys[handler.shortcut], handler.handle)
startClickThrough()
},
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'ignore-mouse-event')?.shortcut,
},
])
watch(handlers, async () => {
await unregisterAll()
for (const handler of handlers.value) {
if (!handler.shortcut) {
return
}
await register(handler.shortcut
.replaceAll('Meta', 'CommandOrControl')
.replaceAll('meta', 'CommandOrControl')
.replaceAll('Ctrl', 'CommandOrControl')
.replaceAll('ctrl', 'CommandOrControl')
.replaceAll('Option', 'OptionOrAlt')
.replaceAll('option', 'OptionOrAlt'), (event: ShortcutEvent) => {
if (event.state !== 'Pressed') {
return
}
handler.handle(event).catch((error) => {
console.error('Error handling shortcut', error)
})
})
})
}
}, { immediate: true })
}
@@ -23,6 +23,22 @@ const routeHeaderMetadataMap = computed(() => {
subtitle: t('settings.title'),
title: t('settings.pages.themes.title'),
},
'/settings/appearance/general': {
subtitle: t('settings.title'),
title: t('settings.pages.themes.general.title'),
},
'/settings/appearance/color-scheme': {
subtitle: t('settings.title'),
title: t('settings.pages.themes.color-scheme.title'),
},
'/settings/appearance/window-shortcuts': {
subtitle: t('settings.title'),
title: t('settings.pages.themes.window-shortcuts.title'),
},
'/settings/appearance/developer': {
subtitle: t('settings.title'),
title: t('settings.pages.themes.developer.title'),
},
'/settings/memory': {
subtitle: t('settings.title'),
title: t('settings.pages.memory.title'),
+8 -14
View File
@@ -10,11 +10,12 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useWindowShortcuts } from '../composables/window-shortcuts'
import { useWindowControlStore } from '../stores/window-controls'
import { WindowControlMode } from '../types/window-controls'
import { startClickThrough, stopClickThrough } from '../utils/windows'
const windowStore = useWindowControlStore()
useWindowShortcuts()
const isCursorInside = ref(false)
const viewRef = ref<HTMLDivElement>()
const mcpStore = useMcpStore()
const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore)
@@ -33,11 +34,11 @@ const modeIndicatorClass = computed(() => {
onMounted(async () => {
await invoke('start_monitor_for_clicking_through')
await invoke('start_click_through')
await startClickThrough()
})
onUnmounted(async () => {
await invoke('stop_click_through')
await stopClickThrough()
await invoke('stop_monitor_for_clicking_through')
})
@@ -54,15 +55,7 @@ function openChat() {
onMounted(async () => {
// Listen for click-through state changes
unlisten.push(await listen('tauri-app:window-click-through:is-inside', (event: { payload: boolean }) => {
if (!viewRef.value)
return
if (event.payload) {
viewRef.value.style.opacity = '0'
}
else {
viewRef.value.style.opacity = '1'
}
isCursorInside.value = event.payload
}))
if (connected.value)
@@ -85,8 +78,9 @@ onUnmounted(() => {
<template>
<div
ref="viewRef"
:class="[modeIndicatorClass]"
:class="[modeIndicatorClass, {
'op-0': isCursorInside && !windowStore.isControlActive,
}]"
relative
max-h="[100vh]"
max-w="[100vw]"
@@ -0,0 +1,224 @@
<script setup lang="ts">
import { Section } from '@proj-airi/stage-ui/components'
import { useSettings } from '@proj-airi/stage-ui/stores'
import { useI18n } from 'vue-i18n'
import ColorPalette from '../../../components/Settings/ColorPalette.vue'
import COLOR_PRESETS from './color-presets.json'
const settings = useSettings()
const { t } = useI18n()
</script>
<template>
<Section
v-motion
mb-2
:title="t('settings.pages.themes.sections.section.custom-color.title')"
icon="i-solar:pallete-2-bold-duotone"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (4 * 10)"
:delay="4 * 50"
transition="all ease-in-out duration-250"
>
<div
v-motion flex items-center
justify-between
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (5 * 10)"
:delay="5 * 50"
transition="all ease-in-out duration-250"
>
<span text-lg font-semibold>{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}</span>
<label relative flex cursor-pointer items-center gap-2>
<input
v-model="settings.themeColorsHueDynamic"
type="checkbox"
class="peer sr-only"
>
<div
class="h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 peer-checked:bg-primary-500 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
/>
{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.rgb-on.title') }}
</label>
</div>
<input
v-model="settings.themeColorsHue"
v-motion
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (6 * 10)"
:delay="6 * 50"
transition="all ease-in-out duration-250"
type="range" min="0" max="360" step="0.01" class="theme-hue-slider"
:disabled="settings.themeColorsHueDynamic"
:class="settings.themeColorsHueDynamic ? 'opacity-25 cursor-not-allowed' : 'cursor-pointer'"
>
<div
v-motion
class="color-bar"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (7 * 10)"
:delay="7 * 50"
transition="all ease-in-out duration-250"
>
<span bg-primary-50>50</span>
<span bg-primary-100>100</span>
<span bg-primary-200>200</span>
<span bg-primary-300>300</span>
<span bg-primary-400>400</span>
<span bg-primary-500>500</span>
<div
v-motion
text-white
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (8 * 10)"
:delay="8 * 50"
transition="all ease-in-out duration-250"
>
<span bg-primary-600>600</span>
<span bg-primary-700>700</span>
<span bg-primary-800>800</span>
<span bg-primary-900>900</span>
<span bg-primary-950>950</span>
</div>
</div>
<div
v-motion
class="color-bar transparency-grid"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (9 * 10)"
:delay="9 * 50"
transition="all ease-in-out duration-250"
>
<span bg="primary-500/5">500/5</span>
<span bg="primary-500/10">500/10</span>
<span bg="primary-500/20">500/20</span>
<span bg="primary-500/30">500/30</span>
<span bg="primary-500/40">500/40</span>
<span bg="primary-500/50">500/50</span>
<span bg="primary-500/60">500/60</span>
<span bg="primary-500/70">500/70</span>
<span bg="primary-500/80">500/80</span>
<span bg="primary-500/90">500/90</span>
<span bg="primary-500">500</span>
</div>
</Section>
<Section
v-motion
mb-2 :title="t('settings.pages.themes.sections.section.theme-presets.title')"
icon="i-solar:magic-stick-2-bold-duotone"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (10 * 10)"
:delay="10 * 50"
transition="all ease-in-out duration-250"
>
<div
v-for="({ title, description, colors }, i) in $tm('settings.pages.themes.sections.section.theme-presets.presets')" :key="i"
v-motion
class="w-full flex items-center justify-between rounded-lg px-4 py-3 outline-none transition-all duration-250 ease-in-out"
bg="neutral-100 dark:neutral-800"
hover="bg-neutral-200 dark:bg-neutral-700"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (11 * 10) + (i * 10)"
:delay="11 * 50 + (i * 50)"
transition="all ease-in-out duration-250"
>
<div>
<span font-medium>{{ $rt(title) }}</span>
<div text="sm neutral-500">
{{ $rt(description) }}
</div>
</div>
<ColorPalette :colors="(colors as any[]).map((name, j) => ({ hex: COLOR_PRESETS[i][j], name: $rt(name) }))" />
</div>
</Section>
<div
v-motion
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
fixed top="[65dvh]" right--15 z--1
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
:duration="250"
flex items-center justify-center
>
<div text="60" i-solar:pallete-2-bold-duotone />
</div>
</template>
<style scoped>
.theme-hue-slider {
--at-apply: appearance-none h-10 rounded-lg;
background: linear-gradient(
to right,
oklch(85% 0.2 0),
oklch(85% 0.2 60),
oklch(85% 0.2 120),
oklch(85% 0.2 180),
oklch(85% 0.2 240),
oklch(85% 0.2 300),
oklch(85% 0.2 360)
);
&::-webkit-slider-thumb {
--at-apply: w-1 h-12 appearance-none rounded-md bg-neutral-600 cursor-pointer shadow-lg border-2 border-neutral-500
hover: bg-neutral-800 transition-colors duration-200;
}
.dark &::-webkit-slider-thumb {
--at-apply: w-1 h-12 appearance-none rounded-md bg-neutral-100 cursor-pointer shadow-md border-2 border-white
hover: bg-neutral-300 transition-colors duration-200;
}
&::-moz-range-thumb {
--at-apply: w-1 h-12 appearance-none rounded-md bg-neutral-600 cursor-pointer shadow-lg border-2 border-neutral-500
hover: bg-neutral-800 transition-colors duration-200;
}
.dark &::-moz-range-thumb {
--at-apply: w-1 h-12 appearance-none rounded-md bg-neutral-100 cursor-pointer shadow-md border-2 border-white
hover: bg-neutral-300 transition-colors duration-200;
}
}
.color-bar {
--at-apply: flex of-hidden rounded-lg lh-10 text-center text-black;
* {
flex: 1;
}
div {
display: contents;
}
}
.transparency-grid {
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(-45deg, transparent 75%, #ccc 75%);
background-size: 20px 20px;
background-position:
0 0,
0 10px,
10px -10px,
-10px 0px;
background-color: #fff;
}
</style>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { useSettings } from '@proj-airi/stage-ui/stores'
import CheckBar from '../../../components/Settings/CheckBar.vue'
const settings = useSettings()
</script>
<template>
<CheckBar
v-model="settings.disableTransitions"
v-motion
mb-2
icon-on="i-solar:people-nearby-bold-duotone"
icon-off="i-solar:running-2-line-duotone"
text="settings.animations.stage-transitions.title"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (19 * 10)"
:delay="19 * 50"
transition="all ease-in-out duration-250"
/>
<CheckBar
v-model="settings.usePageSpecificTransitions"
v-motion
:disabled="settings.disableTransitions"
icon-on="i-solar:running-2-line-duotone"
icon-off="i-solar:people-nearby-bold-duotone"
text="settings.animations.use-page-specific-transitions.title"
description="settings.animations.use-page-specific-transitions.description"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (20 * 10)"
:delay="20 * 50"
transition="all ease-in-out duration-250"
/>
<div
v-motion
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
fixed top="[65dvh]" right--15 z--1
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
:duration="250"
flex items-center justify-center
>
<div text="60" i-solar:code-bold-duotone />
</div>
</template>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
@@ -0,0 +1,71 @@
<script setup lang="ts">
import { useSettings } from '@proj-airi/stage-ui/stores'
import { useDark } from '@vueuse/core'
import CheckBar from '../../../components/Settings/CheckBar.vue'
const settings = useSettings()
const dark = useDark()
</script>
<template>
<CheckBar
v-model="dark"
v-motion
mb-2
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (2 * 10)"
:delay="2 * 50"
icon-on="i-solar:moon-stars-bold-duotone"
icon-off="i-solar:sun-fog-bold-duotone"
text="settings.theme"
transition="all ease-in-out duration-250"
/>
<!-- Language Setting -->
<div
v-motion
class="w-full flex items-center justify-between rounded-lg px-4 py-3 text-sm outline-none transition-all duration-250 ease-in-out"
bg="neutral-50 dark:neutral-800"
hover="bg-neutral-200 dark:bg-neutral-700"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (3 * 10)"
:delay="3 * 50"
transition="all ease-in-out duration-250"
>
{{ $t('settings.language.title') }}
<select
v-model="settings.language"
transition="all ease-in-out duration-250"
cursor-pointer bg-transparent text-right outline-none
>
<option value="en-US">
{{ $t('settings.language.english') }}
</option>
<option value="zh-CN">
{{ $t('settings.language.chinese') }}
</option>
</select>
</div>
<div
v-motion
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
fixed top="[65dvh]" right--15 z--1
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
:duration="250"
flex items-center justify-center
>
<div text="60" i-solar:emoji-funny-square-bold-duotone />
</div>
</template>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
@@ -1,318 +1,73 @@
<script setup lang="ts">
import { Section } from '@proj-airi/stage-ui/components'
import { useSettings } from '@proj-airi/stage-ui/stores'
import { useDark } from '@vueuse/core'
import { ref, watch } from 'vue'
import { IconItem } from '@proj-airi/stage-ui/components'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import CheckBar from '../../../components/Settings/CheckBar.vue'
import ColorPalette from '../../../components/Settings/ColorPalette.vue'
import COLOR_PRESETS from './color-presets.json'
const settings = useSettings()
const dark = useDark()
const { t } = useI18n()
const usePageSpecificTransitionsSettingChanged = ref(false)
// avoid showing the animation component when the page specific transitions are enabled
watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions], () => {
usePageSpecificTransitionsSettingChanged.value = true
})
const settings = computed(() => [
{
title: t('settings.pages.themes.general.title'),
description: t('settings.pages.themes.general.description'),
icon: 'i-solar:emoji-funny-square-bold-duotone',
to: '/settings/appearance/general',
},
{
title: t('settings.pages.themes.color-scheme.title'),
description: t('settings.pages.themes.color-scheme.description'),
icon: 'i-solar:pallete-2-bold-duotone',
to: '/settings/appearance/color-scheme',
},
{
title: t('settings.pages.themes.window-shortcuts.title'),
description: t('settings.pages.themes.window-shortcuts.description'),
icon: 'i-solar:keyboard-bold-duotone',
to: '/settings/appearance/window-shortcuts',
},
{
title: t('settings.pages.themes.developer.title'),
description: t('settings.pages.themes.developer.description'),
icon: 'i-solar:code-bold-duotone',
to: '/settings/appearance/developer',
},
])
</script>
<template>
<Section
v-motion
:title="t('settings.sections.section.general.title')"
icon="i-solar:filters-bold-duotone"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (1 * 10)"
:delay="1 * 50"
transition="all ease-in-out duration-250"
>
<!-- Theme Setting -->
<CheckBar
v-model="dark"
v-motion
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (2 * 10)"
:delay="2 * 50"
icon-on="i-solar:moon-stars-bold-duotone"
icon-off="i-solar:sun-fog-bold-duotone"
text="settings.theme"
transition="all ease-in-out duration-250"
/>
<!-- Language Setting -->
<div
v-motion
class="w-full flex items-center justify-between rounded-lg px-4 py-3 text-sm outline-none transition-all duration-250 ease-in-out"
bg="neutral-50 dark:neutral-800"
hover="bg-neutral-200 dark:bg-neutral-700"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (3 * 10)"
:delay="3 * 50"
transition="all ease-in-out duration-250"
>
{{ $t('settings.language.title') }}
<select
v-model="settings.language"
transition="all ease-in-out duration-250"
cursor-pointer bg-transparent text-right outline-none
>
<option value="en-US">
{{ $t('settings.language.english') }}
</option>
<option value="zh-CN">
{{ $t('settings.language.chinese') }}
</option>
</select>
</div>
</Section>
<Section
v-motion
:title="t('settings.pages.themes.sections.section.custom-color.title')"
icon="i-solar:pallete-2-bold-duotone"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (4 * 10)"
:delay="4 * 50"
transition="all ease-in-out duration-250"
>
<div
v-motion flex items-center
justify-between
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (5 * 10)"
:delay="5 * 50"
transition="all ease-in-out duration-250"
>
<span text-lg font-semibold>{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}</span>
<label relative flex cursor-pointer items-center gap-2>
<input
v-model="settings.themeColorsHueDynamic"
type="checkbox"
class="peer sr-only"
>
<div
class="h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 peer-checked:bg-primary-500 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
/>
{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.rgb-on.title') }}
</label>
</div>
<input
v-model="settings.themeColorsHue"
v-motion
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (6 * 10)"
:delay="6 * 50"
transition="all ease-in-out duration-250"
type="range" min="0" max="360" step="0.01" class="theme-hue-slider"
:disabled="settings.themeColorsHueDynamic"
:class="settings.themeColorsHueDynamic ? 'opacity-25 cursor-not-allowed' : 'cursor-pointer'"
>
<div
v-motion
class="color-bar"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (7 * 10)"
:delay="7 * 50"
transition="all ease-in-out duration-250"
>
<span bg-primary-50>50</span>
<span bg-primary-100>100</span>
<span bg-primary-200>200</span>
<span bg-primary-300>300</span>
<span bg-primary-400>400</span>
<span bg-primary-500>500</span>
<div
<div flex="~ col gap-4" font-thin>
<div />
<div flex="~ col gap-4">
<IconItem
v-for="(setting, index) in settings"
:key="setting.to"
v-motion
text-white
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (8 * 10)"
:delay="8 * 50"
transition="all ease-in-out duration-250"
>
<span bg-primary-600>600</span>
<span bg-primary-700>700</span>
<span bg-primary-800>800</span>
<span bg-primary-900>900</span>
<span bg-primary-950>950</span>
</div>
:duration="250"
:style="{
transitionDelay: `${index * 50}ms`, // delay between each item, unocss doesn't support dynamic generation of classes now
}"
:title="setting.title"
:description="setting.description"
:icon="setting.icon"
:to="setting.to"
/>
</div>
<div
v-motion
class="color-bar transparency-grid"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (9 * 10)"
:delay="9 * 50"
transition="all ease-in-out duration-250"
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
fixed top="[calc(100dvh-12rem)]" bottom-0 right--10 z--1
:initial="{ scale: 0.9, opacity: 0, rotate: 180 }"
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
:duration="500"
size-60
flex items-center justify-center
>
<span bg="primary-500/5">500/5</span>
<span bg="primary-500/10">500/10</span>
<span bg="primary-500/20">500/20</span>
<span bg="primary-500/30">500/30</span>
<span bg="primary-500/40">500/40</span>
<span bg="primary-500/50">500/50</span>
<span bg="primary-500/60">500/60</span>
<span bg="primary-500/70">500/70</span>
<span bg="primary-500/80">500/80</span>
<span bg="primary-500/90">500/90</span>
<span bg="primary-500">500</span>
<div v-motion text="60" i-solar:settings-bold-duotone />
</div>
</Section>
<Section
v-motion title="settings.pages.themes.sections.section.theme-presets.title"
icon="i-solar:magic-stick-2-bold-duotone"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (10 * 10)"
:delay="10 * 50"
transition="all ease-in-out duration-250"
>
<div
v-for="({ title, description, colors }, i) in $tm('settings.pages.themes.sections.section.theme-presets.presets')" :key="i"
v-motion
class="w-full flex items-center justify-between rounded-lg px-4 py-3 outline-none transition-all duration-250 ease-in-out"
bg="neutral-100 dark:neutral-800"
hover="bg-neutral-200 dark:bg-neutral-700"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (11 * 10) + (i * 10)"
:delay="11 * 50 + (i * 50)"
transition="all ease-in-out duration-250"
>
<div>
<span font-medium>{{ $rt(title) }}</span>
<div text="sm neutral-500">
{{ $rt(description) }}
</div>
</div>
<ColorPalette :colors="(colors as any[]).map((name, j) => ({ hex: COLOR_PRESETS[i][j], name: $rt(name) }))" />
</div>
</Section>
<Section
v-motion title="settings.pages.themes.sections.section.developer.title"
icon="i-solar:code-bold-duotone"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (18 * 10)"
:delay="18 * 50"
transition="all ease-in-out duration-250"
>
<CheckBar
v-model="settings.disableTransitions"
v-motion
icon-on="i-solar:people-nearby-bold-duotone"
icon-off="i-solar:running-2-line-duotone"
text="settings.animations.stage-transitions.title"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (19 * 10)"
:delay="19 * 50"
transition="all ease-in-out duration-250"
/>
<CheckBar
v-model="settings.usePageSpecificTransitions"
v-motion
:disabled="settings.disableTransitions"
icon-on="i-solar:running-2-line-duotone"
icon-off="i-solar:people-nearby-bold-duotone"
text="settings.animations.use-page-specific-transitions.title"
description="settings.animations.use-page-specific-transitions.description"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (20 * 10)"
:delay="20 * 50"
transition="all ease-in-out duration-250"
/>
</Section>
<div
v-motion
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
fixed top="[65dvh]" right--15 z--1
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
:duration="250"
flex items-center justify-center
>
<div text="60" i-solar:filters-bold-duotone />
</div>
</template>
<style scoped>
.theme-hue-slider {
--at-apply: appearance-none h-10 rounded-lg;
background: linear-gradient(
to right,
oklch(85% 0.2 0),
oklch(85% 0.2 60),
oklch(85% 0.2 120),
oklch(85% 0.2 180),
oklch(85% 0.2 240),
oklch(85% 0.2 300),
oklch(85% 0.2 360)
);
&::-webkit-slider-thumb {
--at-apply: w-1 h-12 appearance-none rounded-md bg-neutral-600 cursor-pointer shadow-lg border-2 border-neutral-500
hover: bg-neutral-800 transition-colors duration-200;
}
.dark &::-webkit-slider-thumb {
--at-apply: w-1 h-12 appearance-none rounded-md bg-neutral-100 cursor-pointer shadow-md border-2 border-white
hover: bg-neutral-300 transition-colors duration-200;
}
&::-moz-range-thumb {
--at-apply: w-1 h-12 appearance-none rounded-md bg-neutral-600 cursor-pointer shadow-lg border-2 border-neutral-500
hover: bg-neutral-800 transition-colors duration-200;
}
.dark &::-moz-range-thumb {
--at-apply: w-1 h-12 appearance-none rounded-md bg-neutral-100 cursor-pointer shadow-md border-2 border-white
hover: bg-neutral-300 transition-colors duration-200;
}
}
.color-bar {
--at-apply: flex of-hidden rounded-lg lh-10 text-center text-black;
* {
flex: 1;
}
div {
display: contents;
}
}
.transparency-grid {
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(-45deg, transparent 75%, #ccc 75%);
background-size: 20px 20px;
background-position:
0 0,
0 10px,
10px -10px,
-10px 0px;
background-color: #fff;
}
</style>
<route lang="yaml">
meta:
layout: settings
@@ -0,0 +1,151 @@
<script setup lang="ts">
import { useSettings } from '@proj-airi/stage-ui/stores'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { useEventListener } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useShortcutsStore } from '../../../stores/shortcuts'
const settings = useSettings()
const { t } = useI18n()
const { shortcuts } = storeToRefs(useShortcutsStore())
const usePageSpecificTransitionsSettingChanged = ref(false)
// avoid showing the animation component when the page specific transitions are enabled
watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions], () => {
usePageSpecificTransitionsSettingChanged.value = true
})
const recordingFor = ref<string | null>(null)
const recordingKeys = ref<{
modifier: string[]
key: string
}>({
modifier: [],
key: '',
})
// Add function to handle shortcut recording
function startRecording(shortcut: typeof shortcuts.value[0]) {
recordingFor.value = shortcut.type
}
onMounted(() => {
getCurrentWindow().setMinimizable(false) // to avoid the window from being minimized when the shortcut is being recorded
})
onUnmounted(() => {
getCurrentWindow().setMinimizable(true)
})
// Handle key combinations
useEventListener('keydown', (e) => {
if (!recordingFor.value)
return
if (!e.code.startsWith('Key')) // ignore non-key events
return
if (e.metaKey)
recordingKeys.value.modifier.push('Meta')
if (e.ctrlKey)
recordingKeys.value.modifier.push('Control')
if (e.altKey)
recordingKeys.value.modifier.push('Alt')
if (e.shiftKey)
recordingKeys.value.modifier.push('Shift')
if (recordingKeys.value.modifier.length === 0)
return
recordingKeys.value.key = e.code.slice(3)
const shortcut = shortcuts.value.find(s => s.type === recordingFor.value)
if (shortcut)
shortcut.shortcut = `${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
recordingKeys.value = {
modifier: [],
key: '',
}
recordingFor.value = null
}, { passive: false })
// Add click outside handler to cancel recording
useEventListener('click', (e) => {
if (recordingFor.value) {
const target = e.target as HTMLElement
if (!target.closest('.shortcut-item')) {
recordingFor.value = null
recordingKeys.value = {
modifier: [],
key: '',
}
}
}
})
const pressKeysMessage = computed(() => {
if (recordingKeys.value.modifier.length === 0)
return t('settings.pages.themes.window-shortcuts.press-keys')
return `${t('settings.pages.themes.window-shortcuts.press-keys')}: ${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
})
function isConflict(shortcut: typeof shortcuts.value[0]) {
return shortcuts.value.some(s => s.type !== shortcut.type && s.shortcut === shortcut.shortcut)
}
</script>
<template>
<div pb-2>
<div
v-for="shortcut in shortcuts" :key="shortcut.type"
v-motion
mb-2
class="w-full flex items-center justify-between rounded-lg px-4 py-3 text-sm outline-none transition-all duration-250 ease-in-out"
bg="neutral-50 dark:neutral-800"
hover="bg-neutral-200 dark:bg-neutral-700"
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (3 * 10)"
:delay="3 * 50"
transition="all ease-in-out duration-250"
cursor-pointer
@click="startRecording(shortcut)"
>
<span text="xs">
{{ t(shortcut.name) }}
</span>
<div
class="shortcut-item flex items-center justify-end gap-x-2 px-2 py-0.5"
:class="{ recording: recordingFor === shortcut.type }"
text="xs"
>
<div v-if="recordingFor === shortcut.type" class="pointer-events-none animate-flash animate-count-infinite">
{{ pressKeysMessage }}
</div>
<div v-else class="pointer-events-none">
{{ shortcut.shortcut }}
</div>
<div v-if="isConflict(shortcut)" text="xs" i-solar:danger-square-bold w-4 />
</div>
</div>
</div>
<div
v-motion
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
fixed top="[65dvh]" right--15 z--1
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
:duration="250"
flex items-center justify-center
>
<div text="60" i-solar:keyboard-bold-duotone />
</div>
</template>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
@@ -66,22 +66,22 @@ function useVersionedLocalStorage<T>(
export const useShortcutsStore = defineStore('shortcuts', () => {
const shortcuts = ref([
{
name: 'settings.pages.shortcuts.sections.section.window-controls.fields.field.toggle-move.label',
shortcut: useVersionedLocalStorage('shortcuts/window/move', 'Shift+Alt+˜'), // Shift + Alt + N
name: 'settings.pages.themes.window-shortcuts.toggle-move.label',
shortcut: useVersionedLocalStorage('shortcuts/window/move', 'Shift+Alt+N'), // Shift + Alt + N
group: 'window',
type: 'move',
},
{
name: 'settings.pages.shortcuts.sections.section.window-controls.fields.field.toggle-resize.label',
shortcut: useVersionedLocalStorage('shortcuts/window/resize', 'Shift+Alt+®'), // Shift + Alt + R
name: 'settings.pages.themes.window-shortcuts.toggle-resize.label',
shortcut: useVersionedLocalStorage('shortcuts/window/resize', 'Shift+Alt+R'), // Shift + Alt + R
group: 'window',
type: 'resize',
},
{
name: 'settings.pages.shortcuts.sections.section.window-controls.fields.field.toggle-ignore-mouse-event.label',
shortcut: useVersionedLocalStorage('shortcuts/window/debug', 'Shift+Alt+ˆ'), // Shift + Alt + I
name: 'settings.pages.themes.window-shortcuts.toggle-ignore-mouse-event.label',
shortcut: useVersionedLocalStorage('shortcuts/window/debug', 'Shift+Alt+I'), // Shift + Alt + I
group: 'window',
type: 'debug',
type: 'ignore-mouse-event',
},
])
@@ -1,8 +1,10 @@
import { getCurrentWindow } from '@tauri-apps/api/window'
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { WindowControlMode } from '../types/window-controls'
import { startClickThrough, stopClickThrough } from '../utils/windows'
export const useWindowControlStore = defineStore('windowControl', () => {
const controlMode = ref<WindowControlMode>(WindowControlMode.NONE)
@@ -12,17 +14,20 @@ export const useWindowControlStore = defineStore('windowControl', () => {
function setMode(mode: WindowControlMode) {
controlMode.value = mode
if (mode === WindowControlMode.RESIZE) {
// return
}
}
function toggleControl() {
isControlActive.value = !isControlActive.value
if (!isControlActive.value) {
controlMode.value = WindowControlMode.NONE
startClickThrough()
return
}
stopClickThrough()
const window = getCurrentWindow()
window.setFocus()
}
return {
@@ -0,0 +1,9 @@
import { invoke } from '@tauri-apps/api/core'
export async function startClickThrough() {
await invoke('start_click_through')
}
export async function stopClickThrough() {
await invoke('stop_click_through')
}
+52 -41
View File
@@ -251,6 +251,9 @@ importers:
'@tauri-apps/api':
specifier: ^2.5.0
version: 2.5.0
'@tauri-apps/plugin-global-shortcut':
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-os':
specifier: '>=2.2.1'
version: 2.2.1
@@ -4400,6 +4403,9 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-global-shortcut@2.2.1':
resolution: {integrity: sha512-b64/TI1t5LIi2JY4OWlYjZpPRq60T5GVVL/no27sUuxaNUZY8dVtwsMtDUgxUpln2yR+P2PJsYlqY5V8sLSxEw==}
'@tauri-apps/plugin-os@2.2.1':
resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==}
@@ -5351,8 +5357,8 @@ packages:
'@xsai-ext/shared-providers@0.3.0-beta.1':
resolution: {integrity: sha512-5+AIOYe5moH0svsfjPwA2IhHJflLIEiOI8MIjoVgGL+VSgLEJoxr/KzAumQc4wR1KMdLK9PkBcCew6WRpmjzTw==}
'@xsai-ext/shared-providers@0.3.0-beta.2':
resolution: {integrity: sha512-AwQBwj2e3mxozjN+tF/bUbx9+K6jPM4gOsVnQP2GnYaQwvV1SHR6W8swDWb4ZiexIOoAD7TVzLVe9QaIh/D9PQ==}
'@xsai-ext/shared-providers@0.3.0-beta.3':
resolution: {integrity: sha512-aSR524BAjy651No425KYgVcLVHE7lDYWvhCE038P1Bk00G05sxxI4hwmZStrJjIOk5nksx/zPb2ZABcdeOaAhg==}
'@xsai-transformers/embed@0.0.6':
resolution: {integrity: sha512-Ktxl2V1jkInqN8AXB7XetFNluTLlztWu+mOyfvB3BHWBb/+mUDLDO0fDXliMrJsSk5+fNDNYRESZFpG85Joy2w==}
@@ -5392,8 +5398,8 @@ packages:
'@xsai/shared-chat@0.3.0-beta.1':
resolution: {integrity: sha512-1lx5+TLxdukM4hHlH8xma7x6WR1rGVDBghVaNDiY1WS7yrTsDyEiphkRrfFR7sOw1/3hRNOX7cZ55W+LBueI2Q==}
'@xsai/shared-chat@0.3.0-beta.2':
resolution: {integrity: sha512-RsUXOewyEiyMkjt68DHC5M+P41M0zq0wjQyA48ydpxrM/fqNQrG2aeTcsV6ylbNyAQaolauhwPG1CPC5A/9xhQ==}
'@xsai/shared-chat@0.3.0-beta.3':
resolution: {integrity: sha512-LtHGPVog0YjZ+lp7FM7MX0BfuEczSGsWC8EIeucdQ3IrZf9MYn23AUnhKZZNSz4cIkywmNGAF1oQzRrRiT/CoA==}
'@xsai/shared@0.2.0':
resolution: {integrity: sha512-fFMJ1T/9P4o7iOmLvc4/UIFMy2tYkXUmL8aJWULmV36Ddq/gYYOndBZJZfoy3mNiDtxgDBkmIBMdIOFQu9iamg==}
@@ -5401,8 +5407,8 @@ packages:
'@xsai/shared@0.2.2':
resolution: {integrity: sha512-bgalLd3samx36hDRRmu3M75LY5NfWwh3YRaMwY4Woy7X09JQx+vEqVpJcaIeqcmVqztm8NGYzj8hUru7lwPh0w==}
'@xsai/shared@0.3.0-beta.2':
resolution: {integrity: sha512-H9a6K0n8p05qGz9EutqhEXzP86YYMYAiwWv/d4eEVdnjHa51pdxiB93PTNFn3snkCcCG5oahli6am5RDIf23Zg==}
'@xsai/shared@0.3.0-beta.3':
resolution: {integrity: sha512-oEwhwLeh2xXb5J0Wt/K/4zQrdBxBvFV9v3aG5UOr4AoZpXsUm3F6Hf+XB2aMX0KegFVhV2dhXe00f7XBa2vFeg==}
'@xsai/stream-text@0.2.2':
resolution: {integrity: sha512-GTVGuKEpYBOglPQ7cjO18/v8jiZxY/lejXMt+KsZqyMQo/WmH6gUkYkRME/cTyxlQEUiF0L/0CqRjQvtaDlB+Q==}
@@ -8761,6 +8767,7 @@ packages:
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch-native@1.6.6:
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
@@ -11619,8 +11626,8 @@ packages:
zod-to-json-schema:
optional: true
xsschema@0.3.0-beta.2:
resolution: {integrity: sha512-J3DZQuitaGtYqikylm+cJvlI/aatbWvPj2GIzjeUO03opEG/+nIQMLVdZhUjFFE6eEWYDjgtrBLevkqewVcjqQ==}
xsschema@0.3.0-beta.3:
resolution: {integrity: sha512-8fKI0Kqxs7npz3ElebNCeGdS0HDuS2qL3IqHK5O53yCdh419hcr3GQillwN39TNFasHjbMLQ+DjSwpY0NONdnQ==}
peerDependencies:
'@valibot/to-json-schema': ^1.0.0
arktype: ^2.1.16
@@ -15120,6 +15127,10 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.5.0
'@tauri-apps/cli-win32-x64-msvc': 2.5.0
'@tauri-apps/plugin-global-shortcut@2.2.1':
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-os@2.2.1':
dependencies:
'@tauri-apps/api': 2.5.0
@@ -16478,25 +16489,25 @@ snapshots:
'@xsai-ext/providers-cloud@0.3.0-beta.1':
dependencies:
'@xsai-ext/shared-providers': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.2
'@xsai-ext/shared-providers': 0.3.0-beta.3
'@xsai/shared': 0.3.0-beta.3
'@xsai-ext/providers-local@0.3.0-beta.1':
dependencies:
'@xsai-ext/shared-providers': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.2
'@xsai-ext/shared-providers': 0.3.0-beta.3
'@xsai/shared': 0.3.0-beta.3
'@xsai-ext/shared-providers@0.2.2':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai-ext/shared-providers@0.3.0-beta.1':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai-ext/shared-providers@0.3.0-beta.2':
'@xsai-ext/shared-providers@0.3.0-beta.3':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai-transformers/embed@0.0.6(web-worker@1.5.0)':
dependencies:
@@ -16520,65 +16531,65 @@ snapshots:
'@xsai/embed@0.2.2':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/embed@0.3.0-beta.1':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/generate-speech@0.3.0-beta.1':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/generate-text@0.2.2':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared-chat': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/shared-chat': 0.3.0-beta.3
'@xsai/generate-text@0.3.0-beta.1':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared-chat': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/shared-chat': 0.3.0-beta.3
'@xsai/generate-transcription@0.3.0-beta.1':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/model@0.3.0-beta.1':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/shared-chat@0.2.2':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/shared-chat@0.3.0-beta.1':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/shared-chat@0.3.0-beta.2':
'@xsai/shared-chat@0.3.0-beta.3':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared': 0.3.0-beta.3
'@xsai/shared@0.2.0': {}
'@xsai/shared@0.2.2': {}
'@xsai/shared@0.3.0-beta.2': {}
'@xsai/shared@0.3.0-beta.3': {}
'@xsai/stream-text@0.2.2':
dependencies:
'@xsai/shared-chat': 0.3.0-beta.2
'@xsai/shared-chat': 0.3.0-beta.3
'@xsai/stream-text@0.3.0-beta.1':
dependencies:
'@xsai/shared-chat': 0.3.0-beta.2
'@xsai/shared-chat': 0.3.0-beta.3
'@xsai/tool@0.2.2(zod-to-json-schema@3.24.5(zod@3.25.28))(zod@3.25.28)':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared-chat': 0.3.0-beta.2
xsschema: 0.3.0-beta.2(zod-to-json-schema@3.24.5(zod@3.25.28))(zod@3.25.28)
'@xsai/shared': 0.3.0-beta.3
'@xsai/shared-chat': 0.3.0-beta.3
xsschema: 0.3.0-beta.3(zod-to-json-schema@3.24.5(zod@3.25.28))(zod@3.25.28)
transitivePeerDependencies:
- '@valibot/to-json-schema'
- arktype
@@ -16589,9 +16600,9 @@ snapshots:
'@xsai/tool@0.3.0-beta.1(zod-to-json-schema@3.24.5(zod@3.25.28))(zod@3.25.28)':
dependencies:
'@xsai/shared': 0.3.0-beta.2
'@xsai/shared-chat': 0.3.0-beta.2
xsschema: 0.3.0-beta.2(zod-to-json-schema@3.24.5(zod@3.25.28))(zod@3.25.28)
'@xsai/shared': 0.3.0-beta.3
'@xsai/shared-chat': 0.3.0-beta.3
xsschema: 0.3.0-beta.3(zod-to-json-schema@3.24.5(zod@3.25.28))(zod@3.25.28)
transitivePeerDependencies:
- '@valibot/to-json-schema'
- arktype
@@ -16602,11 +16613,11 @@ snapshots:
'@xsai/utils-chat@0.2.2':
dependencies:
'@xsai/shared-chat': 0.3.0-beta.2
'@xsai/shared-chat': 0.3.0-beta.3
'@xsai/utils-chat@0.3.0-beta.1':
dependencies:
'@xsai/shared-chat': 0.3.0-beta.2
'@xsai/shared-chat': 0.3.0-beta.3
abbrev@1.1.1:
optional: true
@@ -24434,7 +24445,7 @@ snapshots:
zod: 3.25.28
zod-to-json-schema: 3.24.5(zod@3.25.28)
xsschema@0.3.0-beta.2(zod-to-json-schema@3.24.5(zod@3.25.28))(zod@3.25.28):
xsschema@0.3.0-beta.3(zod-to-json-schema@3.24.5(zod@3.25.28))(zod@3.25.28):
optionalDependencies:
zod: 3.25.28
zod-to-json-schema: 3.24.5(zod@3.25.28)