mirror of
https://github.com/moeru-ai/airi.git
synced 2026-07-10 00:08:33 +00:00
feat(stage-tamagotchi,stage-shared): listen keyup for global shortcut with uiohook
Some checks are pending
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Update Nix pnpmDeps Hash / update (push) Waiting to run
Some checks are pending
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Update Nix pnpmDeps Hash / update (push) Waiting to run
This commit is contained in:
parent
10f340a651
commit
f86d86c04c
10 changed files with 829 additions and 282 deletions
|
|
@ -192,8 +192,10 @@
|
|||
"h3": "2.0.1-rc.20",
|
||||
"less": "^4.6.4",
|
||||
"mkcert": "catalog:",
|
||||
"node-abi": "catalog:",
|
||||
"std-env": "^4.1.0",
|
||||
"superjson": "catalog:",
|
||||
"uiohook-napi": "catalog:",
|
||||
"unocss-preset-scrollbar": "^4.0.0",
|
||||
"unplugin-info": "^1.3.2",
|
||||
"unplugin-yaml": "^4.1.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,368 @@
|
|||
import type { ShortcutBinding } from '@proj-airi/stage-shared/global-shortcut'
|
||||
|
||||
import { ShortcutFailureReasons } from '@proj-airi/stage-shared/global-shortcut'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Builds a binding for the uiohook driver.
|
||||
*
|
||||
* Defaults to `receiveKeyUps: true` because that flag is the dispatch
|
||||
* signal in the orchestrator; the driver itself does not inspect it,
|
||||
* but tests stay closer to how callers will use the driver this way.
|
||||
*
|
||||
* @example
|
||||
* exampleBinding('ptt')
|
||||
* // => { id: 'ptt', accelerator: { modifiers: ['shift'], key: 'KeyK' },
|
||||
* // scope: 'global', receiveKeyUps: true }
|
||||
*/
|
||||
function exampleBinding(id: string, modifiers: ShortcutBinding['accelerator']['modifiers'] = ['shift'], key = 'KeyK'): ShortcutBinding {
|
||||
return {
|
||||
id,
|
||||
accelerator: { modifiers, key },
|
||||
scope: 'global',
|
||||
receiveKeyUps: true,
|
||||
}
|
||||
}
|
||||
|
||||
interface KeyboardEvent {
|
||||
keycode: number
|
||||
altKey: boolean
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
shiftKey: boolean
|
||||
}
|
||||
|
||||
function event(partial: Partial<KeyboardEvent> & Pick<KeyboardEvent, 'keycode'>): KeyboardEvent {
|
||||
return {
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires mocks for `uiohook-napi` (singleton + listeners) and the
|
||||
* `electron` `systemPreferences` surface, then imports the driver
|
||||
* factory under test.
|
||||
*
|
||||
* @example
|
||||
* const m = await setupMocks()
|
||||
* const driver = m.createUiohookDriver({ ... })
|
||||
* m.fire('keydown', event({ keycode: 37 }))
|
||||
*/
|
||||
async function setupMocks() {
|
||||
const onMock = vi.fn()
|
||||
const removeListenerMock = vi.fn()
|
||||
const startMock = vi.fn()
|
||||
const stopMock = vi.fn()
|
||||
const isTrustedAccessibilityClientMock = vi.fn(() => true)
|
||||
|
||||
const listeners = new Map<string, Array<(e: KeyboardEvent) => void>>()
|
||||
|
||||
onMock.mockImplementation((event: string, listener: (e: KeyboardEvent) => void) => {
|
||||
const arr = listeners.get(event) ?? []
|
||||
arr.push(listener)
|
||||
listeners.set(event, arr)
|
||||
})
|
||||
|
||||
removeListenerMock.mockImplementation((event: string, listener: (e: KeyboardEvent) => void) => {
|
||||
const arr = listeners.get(event)
|
||||
if (!arr)
|
||||
return
|
||||
listeners.set(event, arr.filter(l => l !== listener))
|
||||
})
|
||||
|
||||
// Mirrors the literal subset of `UiohookKey` that exercises the
|
||||
// mapper. KeyK = 37, KeyA = 30 (matches real upstream constants so
|
||||
// tests assert real keycodes, not arbitrary numbers).
|
||||
const UiohookKey = {
|
||||
K: 37,
|
||||
A: 30,
|
||||
Q: 16,
|
||||
} as const
|
||||
|
||||
vi.doMock('uiohook-napi', () => ({
|
||||
uIOhook: {
|
||||
on: onMock,
|
||||
removeListener: removeListenerMock,
|
||||
start: startMock,
|
||||
stop: stopMock,
|
||||
},
|
||||
UiohookKey,
|
||||
}))
|
||||
|
||||
vi.doMock('electron', () => ({
|
||||
systemPreferences: {
|
||||
isTrustedAccessibilityClient: isTrustedAccessibilityClientMock,
|
||||
},
|
||||
}))
|
||||
|
||||
const { createUiohookDriver } = await import('./global-shortcut-uiohook')
|
||||
|
||||
function fire(name: 'keydown' | 'keyup', e: KeyboardEvent): void {
|
||||
for (const listener of listeners.get(name) ?? [])
|
||||
listener(e)
|
||||
}
|
||||
|
||||
function createDriver(overrides: { platform?: NodeJS.Platform, sessionType?: string } = {}) {
|
||||
const broadcastTriggered = vi.fn<(id: string, phase: 'down' | 'up') => void>()
|
||||
const logger = {
|
||||
warn: vi.fn(),
|
||||
withError: vi.fn(() => ({ warn: vi.fn() })),
|
||||
}
|
||||
const driver = createUiohookDriver({
|
||||
broadcastTriggered,
|
||||
logger: logger as unknown as Parameters<typeof createUiohookDriver>[0]['logger'],
|
||||
platform: overrides.platform ?? 'darwin',
|
||||
sessionType: overrides.sessionType,
|
||||
})
|
||||
return { driver, broadcastTriggered, logger }
|
||||
}
|
||||
|
||||
return {
|
||||
onMock,
|
||||
removeListenerMock,
|
||||
startMock,
|
||||
stopMock,
|
||||
isTrustedAccessibilityClientMock,
|
||||
fire,
|
||||
createDriver,
|
||||
}
|
||||
}
|
||||
|
||||
describe('createUiohookDriver', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('starts the OS hook lazily and installs keydown/keyup listeners on first registration', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver } = m.createDriver()
|
||||
|
||||
expect(m.startMock).not.toHaveBeenCalled()
|
||||
expect(m.onMock).not.toHaveBeenCalled()
|
||||
|
||||
const result = driver.tryRegister(exampleBinding('ptt'))
|
||||
|
||||
expect(result).toEqual({ id: 'ptt', ok: true })
|
||||
expect(m.startMock).toHaveBeenCalledTimes(1)
|
||||
expect(m.onMock).toHaveBeenCalledWith('keydown', expect.any(Function))
|
||||
expect(m.onMock).toHaveBeenCalledWith('keyup', expect.any(Function))
|
||||
})
|
||||
|
||||
it('stops the OS hook only after the last binding is unregistered', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver } = m.createDriver()
|
||||
|
||||
driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA'))
|
||||
driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ'))
|
||||
expect(m.stopMock).not.toHaveBeenCalled()
|
||||
|
||||
driver.unregisterById('a')
|
||||
expect(m.stopMock).not.toHaveBeenCalled()
|
||||
|
||||
driver.unregisterById('b')
|
||||
expect(m.stopMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('broadcasts a "down" event when a matching keydown arrives', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver()
|
||||
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
|
||||
|
||||
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
|
||||
|
||||
expect(broadcastTriggered).toHaveBeenCalledTimes(1)
|
||||
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down')
|
||||
})
|
||||
|
||||
it('suppresses OS auto-repeat — repeated keydowns between matching down/up collapse to one broadcast', async () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// libuiohook reports the OS-level keydown stream verbatim, which
|
||||
// includes auto-repeat events while the key remains physically
|
||||
// held. Without per-binding `pressed` tracking, a held PTT key
|
||||
// would emit hundreds of `down` broadcasts per second and the mic
|
||||
// would start/stop frantically.
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver()
|
||||
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
|
||||
|
||||
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
|
||||
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
|
||||
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
|
||||
|
||||
const downCalls = broadcastTriggered.mock.calls.filter(c => c[1] === 'down')
|
||||
expect(downCalls).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('broadcasts "up" on matching keyup and re-arms the binding for the next press', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver()
|
||||
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
|
||||
|
||||
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
|
||||
m.fire('keyup', event({ keycode: 37, shiftKey: false }))
|
||||
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
|
||||
|
||||
expect(broadcastTriggered).toHaveBeenNthCalledWith(1, 'ptt', 'down')
|
||||
expect(broadcastTriggered).toHaveBeenNthCalledWith(2, 'ptt', 'up')
|
||||
expect(broadcastTriggered).toHaveBeenNthCalledWith(3, 'ptt', 'down')
|
||||
})
|
||||
|
||||
it('matches keyup by keycode even when modifiers were released before the main key', async () => {
|
||||
// NOTICE:
|
||||
// Users routinely release the modifier first (e.g. let Cmd go
|
||||
// before letting K go). The keyup event for K therefore carries
|
||||
// `metaKey: false`, which would fail strict modifier matching.
|
||||
// The driver keys the "up" broadcast off the prior `pressed`
|
||||
// state rather than the modifier predicate.
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver()
|
||||
driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK'))
|
||||
|
||||
m.fire('keydown', event({ keycode: 37, metaKey: true }))
|
||||
m.fire('keyup', event({ keycode: 37, metaKey: false }))
|
||||
|
||||
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down')
|
||||
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'up')
|
||||
})
|
||||
|
||||
it('ignores keyup when no matching keydown was tracked', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver()
|
||||
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
|
||||
|
||||
m.fire('keyup', event({ keycode: 37, shiftKey: true }))
|
||||
|
||||
expect(broadcastTriggered).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not match a keydown that carries an extra modifier', async () => {
|
||||
// Strict matching mirrors Electron's accelerator semantics: a
|
||||
// `Shift+K` binding must not fire on `Cmd+Shift+K`.
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver()
|
||||
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
|
||||
|
||||
m.fire('keydown', event({ keycode: 37, shiftKey: true, metaKey: true }))
|
||||
|
||||
expect(broadcastTriggered).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('maps cmd-or-ctrl to metaKey on darwin', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver({ platform: 'darwin' })
|
||||
driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK'))
|
||||
|
||||
m.fire('keydown', event({ keycode: 37, metaKey: true }))
|
||||
m.fire('keydown', event({ keycode: 37, ctrlKey: true }))
|
||||
|
||||
expect(broadcastTriggered).toHaveBeenCalledTimes(1)
|
||||
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down')
|
||||
})
|
||||
|
||||
it('maps cmd-or-ctrl to ctrlKey on non-darwin platforms', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver({ platform: 'win32' })
|
||||
driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK'))
|
||||
|
||||
m.fire('keydown', event({ keycode: 37, ctrlKey: true }))
|
||||
m.fire('keydown', event({ keycode: 37, metaKey: true }))
|
||||
|
||||
// First (ctrl) matches; second (meta) does not — the pressed
|
||||
// state stays cleared and produces no extra broadcast.
|
||||
expect(broadcastTriggered).toHaveBeenCalledTimes(1)
|
||||
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down')
|
||||
})
|
||||
|
||||
it('rejects duplicate ids with reason "duplicate-id" and does not double-start the hook', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver } = m.createDriver()
|
||||
|
||||
expect(driver.tryRegister(exampleBinding('ptt'))).toEqual({ id: 'ptt', ok: true })
|
||||
const second = driver.tryRegister(exampleBinding('ptt'))
|
||||
expect(second).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.DuplicateId })
|
||||
expect(m.startMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('returns Unsupported under a native Wayland session', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver } = m.createDriver({ platform: 'linux', sessionType: 'wayland' })
|
||||
|
||||
const result = driver.tryRegister(exampleBinding('ptt'))
|
||||
expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Unsupported })
|
||||
expect(m.startMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('permits registration on Linux under X11 / XWayland', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver } = m.createDriver({ platform: 'linux', sessionType: 'x11' })
|
||||
|
||||
expect(driver.tryRegister(exampleBinding('ptt'))).toEqual({ id: 'ptt', ok: true })
|
||||
expect(m.startMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('returns Denied when macOS Accessibility permission is not granted', async () => {
|
||||
const m = await setupMocks()
|
||||
m.isTrustedAccessibilityClientMock.mockReturnValue(false)
|
||||
const { driver } = m.createDriver({ platform: 'darwin' })
|
||||
|
||||
const result = driver.tryRegister(exampleBinding('ptt'))
|
||||
expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Denied })
|
||||
expect(m.isTrustedAccessibilityClientMock).toHaveBeenCalledWith(true)
|
||||
expect(m.startMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips the Accessibility check entirely on non-darwin', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver } = m.createDriver({ platform: 'win32' })
|
||||
driver.tryRegister(exampleBinding('ptt'))
|
||||
expect(m.isTrustedAccessibilityClientMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('unregisterAll clears every binding and stops the hook in one shot', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver()
|
||||
|
||||
driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA'))
|
||||
driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ'))
|
||||
driver.unregisterAll()
|
||||
|
||||
m.fire('keydown', event({ keycode: 30, shiftKey: true }))
|
||||
m.fire('keydown', event({ keycode: 16, shiftKey: true }))
|
||||
|
||||
expect(broadcastTriggered).not.toHaveBeenCalled()
|
||||
expect(m.stopMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('dispose removes the keydown/keyup listeners', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver } = m.createDriver()
|
||||
driver.tryRegister(exampleBinding('ptt'))
|
||||
|
||||
driver.dispose()
|
||||
|
||||
expect(m.removeListenerMock).toHaveBeenCalledWith('keydown', expect.any(Function))
|
||||
expect(m.removeListenerMock).toHaveBeenCalledWith('keyup', expect.any(Function))
|
||||
})
|
||||
|
||||
it('keeps per-binding pressed state independent across multiple bindings', async () => {
|
||||
const m = await setupMocks()
|
||||
const { driver, broadcastTriggered } = m.createDriver()
|
||||
driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA'))
|
||||
driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ'))
|
||||
|
||||
m.fire('keydown', event({ keycode: 30, shiftKey: true }))
|
||||
m.fire('keydown', event({ keycode: 16, shiftKey: true }))
|
||||
m.fire('keyup', event({ keycode: 30 }))
|
||||
|
||||
expect(broadcastTriggered).toHaveBeenNthCalledWith(1, 'a', 'down')
|
||||
expect(broadcastTriggered).toHaveBeenNthCalledWith(2, 'b', 'down')
|
||||
expect(broadcastTriggered).toHaveBeenNthCalledWith(3, 'a', 'up')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
import type { useLogg } from '@guiiai/logg'
|
||||
import type {
|
||||
ShortcutAccelerator,
|
||||
ShortcutBinding,
|
||||
ShortcutKey,
|
||||
ShortcutModifier,
|
||||
ShortcutRegistrationResult,
|
||||
} from '@proj-airi/stage-shared/global-shortcut'
|
||||
import type { UiohookKeyboardEvent } from 'uiohook-napi'
|
||||
|
||||
import process from 'node:process'
|
||||
|
||||
import { ShortcutFailureReasons } from '@proj-airi/stage-shared/global-shortcut'
|
||||
import { systemPreferences } from 'electron'
|
||||
import { uIOhook, UiohookKey } from 'uiohook-napi'
|
||||
|
||||
type Logger = ReturnType<ReturnType<typeof useLogg>['useGlobalConfig']>
|
||||
|
||||
interface ModifierMask {
|
||||
ctrl: boolean
|
||||
shift: boolean
|
||||
alt: boolean
|
||||
meta: boolean
|
||||
}
|
||||
|
||||
interface UiohookEntry {
|
||||
binding: ShortcutBinding
|
||||
predicate: (event: UiohookKeyboardEvent) => boolean
|
||||
expectedKeycode: number
|
||||
pressed: boolean
|
||||
}
|
||||
|
||||
const W3C_TO_UIOHOOK: Readonly<Record<ShortcutKey, number>> = buildKeycodeMap()
|
||||
|
||||
function buildKeycodeMap(): Record<ShortcutKey, number> {
|
||||
const map: Record<string, number> = {}
|
||||
|
||||
for (let i = 0; i < 26; i++) {
|
||||
const letter = String.fromCharCode(65 + i)
|
||||
map[`Key${letter}`] = (UiohookKey as unknown as Record<string, number>)[letter]
|
||||
}
|
||||
|
||||
for (let i = 0; i <= 9; i++) {
|
||||
map[`Digit${i}`] = (UiohookKey as unknown as Record<string, number>)[String(i)]
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 24; i++) {
|
||||
map[`F${i}`] = (UiohookKey as unknown as Record<string, number>)[`F${i}`]
|
||||
}
|
||||
|
||||
const named: Record<string, keyof typeof UiohookKey> = {
|
||||
Space: 'Space',
|
||||
Tab: 'Tab',
|
||||
Enter: 'Enter',
|
||||
Escape: 'Escape',
|
||||
Backspace: 'Backspace',
|
||||
Delete: 'Delete',
|
||||
Insert: 'Insert',
|
||||
ArrowUp: 'ArrowUp',
|
||||
ArrowDown: 'ArrowDown',
|
||||
ArrowLeft: 'ArrowLeft',
|
||||
ArrowRight: 'ArrowRight',
|
||||
Home: 'Home',
|
||||
End: 'End',
|
||||
PageUp: 'PageUp',
|
||||
PageDown: 'PageDown',
|
||||
Backquote: 'Backquote',
|
||||
Minus: 'Minus',
|
||||
Equal: 'Equal',
|
||||
BracketLeft: 'BracketLeft',
|
||||
BracketRight: 'BracketRight',
|
||||
Backslash: 'Backslash',
|
||||
Semicolon: 'Semicolon',
|
||||
Quote: 'Quote',
|
||||
Comma: 'Comma',
|
||||
Period: 'Period',
|
||||
Slash: 'Slash',
|
||||
}
|
||||
for (const [w3c, uioName] of Object.entries(named))
|
||||
map[w3c] = (UiohookKey as unknown as Record<string, number>)[uioName as string]
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
function resolveModifierMask(modifiers: readonly ShortcutModifier[], platform: NodeJS.Platform): ModifierMask {
|
||||
const mask: ModifierMask = { ctrl: false, shift: false, alt: false, meta: false }
|
||||
for (const m of modifiers) {
|
||||
switch (m) {
|
||||
case 'cmd-or-ctrl':
|
||||
if (platform === 'darwin')
|
||||
mask.meta = true
|
||||
else
|
||||
mask.ctrl = true
|
||||
break
|
||||
case 'cmd':
|
||||
case 'super':
|
||||
// libuiohook surfaces macOS Cmd, Windows key, and X11 Super
|
||||
// through the same `metaKey` flag.
|
||||
mask.meta = true
|
||||
break
|
||||
case 'ctrl':
|
||||
mask.ctrl = true
|
||||
break
|
||||
case 'alt':
|
||||
mask.alt = true
|
||||
break
|
||||
case 'shift':
|
||||
mask.shift = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
function buildPredicate(acc: ShortcutAccelerator, platform: NodeJS.Platform): { predicate: UiohookEntry['predicate'], expectedKeycode: number } | undefined {
|
||||
const expectedKeycode = W3C_TO_UIOHOOK[acc.key]
|
||||
if (expectedKeycode === undefined)
|
||||
return undefined
|
||||
const required = resolveModifierMask(acc.modifiers, platform)
|
||||
const predicate: UiohookEntry['predicate'] = e =>
|
||||
e.keycode === expectedKeycode
|
||||
&& e.ctrlKey === required.ctrl
|
||||
&& e.shiftKey === required.shift
|
||||
&& e.altKey === required.alt
|
||||
&& e.metaKey === required.meta
|
||||
return { predicate, expectedKeycode }
|
||||
}
|
||||
|
||||
function isNativeWayland(platform: NodeJS.Platform, sessionType: string | undefined): boolean {
|
||||
return platform === 'linux' && sessionType === 'wayland'
|
||||
}
|
||||
|
||||
function isMacAccessibilityTrusted(platform: NodeJS.Platform, prompt: boolean): boolean {
|
||||
if (platform !== 'darwin')
|
||||
return true
|
||||
try {
|
||||
return systemPreferences.isTrustedAccessibilityClient(prompt)
|
||||
}
|
||||
catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export interface UiohookDriverOptions {
|
||||
broadcastTriggered: (id: string, phase: 'down' | 'up') => void
|
||||
logger: Logger
|
||||
/**
|
||||
* Host platform; injected so tests can exercise cross-platform
|
||||
* modifier mapping without stubbing `process`.
|
||||
*
|
||||
* @default process.platform
|
||||
*/
|
||||
platform?: NodeJS.Platform
|
||||
/**
|
||||
* `XDG_SESSION_TYPE` value used for the Wayland refusal check;
|
||||
* injected for the same reason as `platform`.
|
||||
*
|
||||
* @default process.env.XDG_SESSION_TYPE
|
||||
*/
|
||||
sessionType?: string
|
||||
}
|
||||
|
||||
export interface UiohookDriver {
|
||||
tryRegister: (binding: ShortcutBinding) => ShortcutRegistrationResult
|
||||
unregisterById: (id: string) => void
|
||||
unregisterAll: () => void
|
||||
dispose: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Driver that captures global key-down and key-up events via
|
||||
* libuiohook (through `uiohook-napi`).
|
||||
*
|
||||
* Use when:
|
||||
* - A binding asks for `receiveKeyUps: true` (push-to-talk and similar
|
||||
* hold-driven flows)
|
||||
*
|
||||
* Lifecycle:
|
||||
* - The OS hook starts lazily on the first successful registration and
|
||||
* stops once the last binding is unregistered, so apps that never
|
||||
* bind a PTT shortcut never pay the permission/perf cost.
|
||||
*
|
||||
* Constraints:
|
||||
* - macOS: requires the Accessibility permission. First registration
|
||||
* triggers the system prompt; subsequent failures return `Denied`.
|
||||
* - Linux: requires X11 or XWayland. Native Wayland sessions return
|
||||
* `Unsupported` because XRecord cannot observe Wayland clients.
|
||||
*/
|
||||
export function createUiohookDriver(options: UiohookDriverOptions): UiohookDriver {
|
||||
const {
|
||||
broadcastTriggered,
|
||||
logger,
|
||||
platform = process.platform,
|
||||
sessionType = process.env.XDG_SESSION_TYPE,
|
||||
} = options
|
||||
const entries = new Map<string, UiohookEntry>()
|
||||
let started = false
|
||||
let listenersInstalled = false
|
||||
|
||||
function ensureListeners(): void {
|
||||
if (listenersInstalled)
|
||||
return
|
||||
listenersInstalled = true
|
||||
uIOhook.on('keydown', onKeydown)
|
||||
uIOhook.on('keyup', onKeyup)
|
||||
}
|
||||
|
||||
function startIfNeeded(): void {
|
||||
if (started || entries.size === 0)
|
||||
return
|
||||
try {
|
||||
uIOhook.start()
|
||||
started = true
|
||||
}
|
||||
catch (error) {
|
||||
logger.withError(error).warn('Failed to start uIOhook')
|
||||
}
|
||||
}
|
||||
|
||||
function stopIfIdle(): void {
|
||||
if (!started || entries.size > 0)
|
||||
return
|
||||
try {
|
||||
uIOhook.stop()
|
||||
}
|
||||
catch (error) {
|
||||
logger.withError(error).warn('Failed to stop uIOhook')
|
||||
}
|
||||
started = false
|
||||
}
|
||||
|
||||
function onKeydown(event: UiohookKeyboardEvent): void {
|
||||
for (const entry of entries.values()) {
|
||||
if (!entry.predicate(event))
|
||||
continue
|
||||
// Auto-repeat suppression: OS may deliver repeated keydown
|
||||
// while the key stays physically held. Emit one `down` per
|
||||
// physical press until the matching keyup clears the flag.
|
||||
if (entry.pressed)
|
||||
continue
|
||||
entry.pressed = true
|
||||
broadcastTriggered(entry.binding.id, 'down')
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyup(event: UiohookKeyboardEvent): void {
|
||||
// NOTICE:
|
||||
// Match keyup by keycode alone; modifier flags may be released
|
||||
// before the main key (e.g. Cmd released before K), in which case
|
||||
// the strict predicate would not match. Pairing keyup to the
|
||||
// binding via the prior `pressed` state ensures every `down`
|
||||
// emits a matching `up`.
|
||||
for (const entry of entries.values()) {
|
||||
if (!entry.pressed)
|
||||
continue
|
||||
if (event.keycode !== entry.expectedKeycode)
|
||||
continue
|
||||
entry.pressed = false
|
||||
broadcastTriggered(entry.binding.id, 'up')
|
||||
}
|
||||
}
|
||||
|
||||
function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult {
|
||||
if (entries.has(binding.id))
|
||||
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId }
|
||||
|
||||
if (isNativeWayland(platform, sessionType)) {
|
||||
// libuiohook hooks install but never receive events under
|
||||
// native Wayland. Refuse rather than register a binding that
|
||||
// would silently no-op.
|
||||
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported }
|
||||
}
|
||||
|
||||
if (!isMacAccessibilityTrusted(platform, true))
|
||||
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Denied }
|
||||
|
||||
const built = buildPredicate(binding.accelerator, platform)
|
||||
if (built === undefined) {
|
||||
logger.warn(`uiohook driver: no keycode mapping for "${binding.accelerator.key}"`)
|
||||
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported }
|
||||
}
|
||||
|
||||
entries.set(binding.id, {
|
||||
binding,
|
||||
predicate: built.predicate,
|
||||
expectedKeycode: built.expectedKeycode,
|
||||
pressed: false,
|
||||
})
|
||||
ensureListeners()
|
||||
startIfNeeded()
|
||||
return { id: binding.id, ok: true }
|
||||
}
|
||||
|
||||
function unregisterById(id: string): void {
|
||||
if (!entries.delete(id))
|
||||
return
|
||||
stopIfIdle()
|
||||
}
|
||||
|
||||
function unregisterAll(): void {
|
||||
if (entries.size === 0)
|
||||
return
|
||||
entries.clear()
|
||||
stopIfIdle()
|
||||
}
|
||||
|
||||
function dispose(): void {
|
||||
unregisterAll()
|
||||
if (listenersInstalled) {
|
||||
uIOhook.removeListener('keydown', onKeydown)
|
||||
uIOhook.removeListener('keyup', onKeyup)
|
||||
listenersInstalled = false
|
||||
}
|
||||
}
|
||||
|
||||
return { tryRegister, unregisterById, unregisterAll, dispose }
|
||||
}
|
||||
|
|
@ -101,6 +101,19 @@ async function setupMocks() {
|
|||
unregister: unregisterMock,
|
||||
unregisterAll: unregisterAllMock,
|
||||
},
|
||||
systemPreferences: {
|
||||
isTrustedAccessibilityClient: vi.fn(() => true),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('uiohook-napi', () => ({
|
||||
uIOhook: {
|
||||
on: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
},
|
||||
UiohookKey: new Proxy({}, { get: () => 0 }),
|
||||
}))
|
||||
|
||||
vi.doMock('@moeru/eventa', async (importOriginal) => {
|
||||
|
|
@ -162,19 +175,15 @@ describe('setupGlobalShortcutService', () => {
|
|||
expect(m.registerMock).toHaveBeenCalledWith('CmdOrCtrl+Shift+K', expect.any(Function))
|
||||
})
|
||||
|
||||
it('refuses receiveKeyUps with reason "unsupported" and does not call globalShortcut', async () => {
|
||||
// Electron's `globalShortcut` does not deliver key-release
|
||||
// events. The driver refuses `receiveKeyUps: true` honestly so
|
||||
// callers can switch to (or fail back from) the uiohook driver
|
||||
// path that will handle it.
|
||||
it('routes receiveKeyUps:true to the uiohook driver and bypasses electron.globalShortcut', async () => {
|
||||
const m = await setupMocks()
|
||||
const service = m.setupGlobalShortcutService()
|
||||
const ctx = createMockContext()
|
||||
registerMockWindow(service, ctx)
|
||||
|
||||
const handler = ctx.invokeHandlers.get('eventa:invoke:electron:shortcut:register')!
|
||||
const result = handler({ ...exampleBinding('ptt'), receiveKeyUps: true }) as { id: string, ok: boolean, reason?: string }
|
||||
expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Unsupported })
|
||||
const result = handler({ ...exampleBinding('ptt'), receiveKeyUps: true }) as { id: string, ok: boolean }
|
||||
expect(result).toEqual({ id: 'ptt', ok: true })
|
||||
expect(m.registerMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
electronShortcutUnregisterAll,
|
||||
} from '../../../shared/eventa'
|
||||
import { onAppBeforeQuit } from '../../libs/bootkit/lifecycle'
|
||||
import { createUiohookDriver } from './global-shortcut-uiohook'
|
||||
|
||||
export type EventaContext = ReturnType<typeof createContext>['context']
|
||||
|
||||
|
|
@ -34,10 +35,10 @@ export interface GlobalShortcutService {
|
|||
dispose: () => void
|
||||
}
|
||||
|
||||
interface ActiveBinding {
|
||||
binding: ShortcutBinding
|
||||
electronAccelerator: string
|
||||
}
|
||||
type ActiveBinding = { binding: ShortcutBinding } & (
|
||||
| { driver: 'electron', electronAccelerator: string }
|
||||
| { driver: 'uiohook' }
|
||||
)
|
||||
|
||||
export function setupGlobalShortcutService(): GlobalShortcutService {
|
||||
const log = useLogg('global-shortcut').useGlobalConfig()
|
||||
|
|
@ -56,20 +57,12 @@ export function setupGlobalShortcutService(): GlobalShortcutService {
|
|||
}
|
||||
}
|
||||
|
||||
function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult {
|
||||
if (binding.receiveKeyUps) {
|
||||
// Electron's `globalShortcut` only fires on press. A separate
|
||||
// driver path (uiohook-napi) handles `receiveKeyUps: true`;
|
||||
// this driver refuses honestly until that path is wired.
|
||||
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported }
|
||||
}
|
||||
|
||||
if (active.has(binding.id)) {
|
||||
// Callers must `unregister` first to rebind. Avoids silent overrides
|
||||
// between unrelated registration sites.
|
||||
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId }
|
||||
}
|
||||
const uiohookDriver = createUiohookDriver({
|
||||
broadcastTriggered,
|
||||
logger: log,
|
||||
})
|
||||
|
||||
function tryRegisterElectron(binding: ShortcutBinding): ShortcutRegistrationResult {
|
||||
const electronAccelerator = formatElectronAccelerator(binding.accelerator)
|
||||
const ok = globalShortcut.register(electronAccelerator, () => broadcastTriggered(binding.id, 'down'))
|
||||
|
||||
|
|
@ -78,30 +71,37 @@ export function setupGlobalShortcutService(): GlobalShortcutService {
|
|||
// causes (held by another app, or denied by the OS for media
|
||||
// keys / Accessibility-gated combos on macOS). Electron does not
|
||||
// expose which case applied, so this driver reports `Conflict`
|
||||
// for both. A future driver path (XDG portal, native macOS) can
|
||||
// emit `Denied` directly.
|
||||
// for both. The uiohook driver path can emit `Denied` directly.
|
||||
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Conflict }
|
||||
}
|
||||
|
||||
active.set(binding.id, { binding, electronAccelerator })
|
||||
active.set(binding.id, { binding, driver: 'electron', electronAccelerator })
|
||||
return { id: binding.id, ok: true }
|
||||
}
|
||||
|
||||
function tryRegisterUiohook(binding: ShortcutBinding): ShortcutRegistrationResult {
|
||||
const result = uiohookDriver.tryRegister(binding)
|
||||
if (result.ok)
|
||||
active.set(binding.id, { binding, driver: 'uiohook' })
|
||||
return result
|
||||
}
|
||||
|
||||
function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult {
|
||||
if (active.has(binding.id)) {
|
||||
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId }
|
||||
}
|
||||
|
||||
return binding.receiveKeyUps
|
||||
? tryRegisterUiohook(binding)
|
||||
: tryRegisterElectron(binding)
|
||||
}
|
||||
|
||||
function unregisterById(id: string): void {
|
||||
const entry = active.get(id)
|
||||
if (!entry)
|
||||
return
|
||||
try {
|
||||
globalShortcut.unregister(entry.electronAccelerator)
|
||||
}
|
||||
catch (error) {
|
||||
log.withError(error).warn(`Failed to unregister accelerator for "${id}"`)
|
||||
}
|
||||
active.delete(id)
|
||||
}
|
||||
|
||||
function unregisterAll(): void {
|
||||
for (const [id, entry] of active) {
|
||||
if (entry.driver === 'electron') {
|
||||
try {
|
||||
globalShortcut.unregister(entry.electronAccelerator)
|
||||
}
|
||||
|
|
@ -109,6 +109,24 @@ export function setupGlobalShortcutService(): GlobalShortcutService {
|
|||
log.withError(error).warn(`Failed to unregister accelerator for "${id}"`)
|
||||
}
|
||||
}
|
||||
else {
|
||||
uiohookDriver.unregisterById(id)
|
||||
}
|
||||
active.delete(id)
|
||||
}
|
||||
|
||||
function unregisterAll(): void {
|
||||
for (const [id, entry] of active) {
|
||||
if (entry.driver !== 'electron')
|
||||
continue
|
||||
try {
|
||||
globalShortcut.unregister(entry.electronAccelerator)
|
||||
}
|
||||
catch (error) {
|
||||
log.withError(error).warn(`Failed to unregister accelerator for "${id}"`)
|
||||
}
|
||||
}
|
||||
uiohookDriver.unregisterAll()
|
||||
active.clear()
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +160,7 @@ export function setupGlobalShortcutService(): GlobalShortcutService {
|
|||
|
||||
const dispose: GlobalShortcutService['dispose'] = () => {
|
||||
unregisterAll()
|
||||
uiohookDriver.dispose()
|
||||
contexts.clear()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,18 @@ function formatTime(ms: number) {
|
|||
return new Date(ms).toLocaleTimeString()
|
||||
}
|
||||
|
||||
function deltaForTrigger(index: number): string {
|
||||
const current = triggers.value[index]
|
||||
if (!current)
|
||||
return ''
|
||||
for (let i = index + 1; i < triggers.value.length; i++) {
|
||||
const prev = triggers.value[i]
|
||||
if (prev.id === current.id)
|
||||
return String(current.time - prev.time)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
let disposeTriggerListener: (() => void) | undefined
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
@ -195,7 +207,7 @@ onUnmounted(() => {
|
|||
<FieldCheckbox
|
||||
v-model="form.receiveKeyUps"
|
||||
label="Receive key-ups"
|
||||
description="Asks the driver to also emit on release. Electron driver refuses with 'unsupported'."
|
||||
description="Routes the binding through the uiohook driver so both 'down' and 'up' fire. macOS needs Accessibility; native Wayland returns 'unsupported'."
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
|
|
@ -349,6 +361,9 @@ onUnmounted(() => {
|
|||
<th class="px-3 py-2 text-left">
|
||||
Phase
|
||||
</th>
|
||||
<th class="px-3 py-2 text-left">
|
||||
Δ ms
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -366,6 +381,9 @@ onUnmounted(() => {
|
|||
<td class="px-3 py-2 font-mono">
|
||||
{{ t.phase }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-neutral-500 font-mono dark:text-neutral-400">
|
||||
{{ deltaForTrigger(i) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ words:
|
|||
- lerp
|
||||
- libsamplerate
|
||||
- libsodium
|
||||
- libuiohook
|
||||
- lightningcss
|
||||
- lipsync
|
||||
- listhen
|
||||
|
|
|
|||
|
|
@ -62,10 +62,12 @@ export interface ShortcutBinding {
|
|||
/**
|
||||
* Whether the driver should also emit key-release events.
|
||||
*
|
||||
* Drivers that cannot deliver release events refuse the registration
|
||||
* with `{ ok: false, reason: ShortcutFailureReasons.Unsupported }`. The Electron
|
||||
* `globalShortcut` driver currently refuses; a uiohook-based driver
|
||||
* path is planned to honour this flag.
|
||||
* Routes the binding through the uiohook driver, which delivers
|
||||
* both `down` and `up` phases. Required for push-to-talk and any
|
||||
* hold-driven flow. Drivers that genuinely cannot deliver release
|
||||
* events under the current session (e.g. native Wayland for the
|
||||
* uiohook path) refuse with `{ ok: false, reason: ShortcutFailureReasons.Unsupported }`;
|
||||
* macOS without Accessibility permission refuses with `Denied`.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
|
|
|
|||
288
pnpm-lock.yaml
generated
288
pnpm-lock.yaml
generated
|
|
@ -276,6 +276,9 @@ catalogs:
|
|||
nanoid:
|
||||
specifier: 5.1.6
|
||||
version: 5.1.6
|
||||
node-abi:
|
||||
specifier: ^4.31.0
|
||||
version: 4.31.0
|
||||
node-pty:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
|
|
@ -321,6 +324,9 @@ catalogs:
|
|||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
uiohook-napi:
|
||||
specifier: ^1.5.5
|
||||
version: 1.5.5
|
||||
uncrypto:
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3
|
||||
|
|
@ -1471,7 +1477,7 @@ importers:
|
|||
version: 3.0.2(electron@41.2.1)
|
||||
'@electron-toolkit/tsconfig':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(@types/node@24.12.2)
|
||||
version: 2.0.0(@types/node@25.6.0)
|
||||
'@electron-toolkit/utils':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(electron@41.2.1)
|
||||
|
|
@ -1510,7 +1516,7 @@ importers:
|
|||
version: 3.1.0
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^11.0.7
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 'catalog:'
|
||||
version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
|
||||
|
|
@ -1546,10 +1552,10 @@ importers:
|
|||
version: link:../../packages/ui-transitions
|
||||
'@proj-airi/unplugin-fetch':
|
||||
specifier: 'catalog:'
|
||||
version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@proj-airi/unplugin-live2d-sdk':
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
'@types/audioworklet':
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.97
|
||||
|
|
@ -1576,7 +1582,7 @@ importers:
|
|||
version: 2.10.3
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.6
|
||||
version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/volar':
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
|
|
@ -1609,7 +1615,7 @@ importers:
|
|||
version: 6.8.3
|
||||
electron-vite:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
get-port-please:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
|
|
@ -1622,39 +1628,45 @@ importers:
|
|||
mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
node-abi:
|
||||
specifier: 'catalog:'
|
||||
version: 4.31.0
|
||||
std-env:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
superjson:
|
||||
specifier: 'catalog:'
|
||||
version: 2.2.6
|
||||
uiohook-napi:
|
||||
specifier: 'catalog:'
|
||||
version: 1.5.5
|
||||
unocss-preset-scrollbar:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
unplugin-info:
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-bundle-visualizer:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
|
||||
vite-plugin-mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: ^8.1.1
|
||||
version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vite-plugin-vue-layouts:
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-macros:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6(typescript@5.9.3)
|
||||
|
|
@ -15122,8 +15134,8 @@ packages:
|
|||
resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
node-abi@4.24.0:
|
||||
resolution: {integrity: sha512-u2EC1CeNe25uVtX3EZbdQ275c74zdZmmpzrHEQh2aIYqoVjlglfUpOX9YY85x1nlBydEKDVaSmMNhR7N82Qj8A==}
|
||||
node-abi@4.31.0:
|
||||
resolution: {integrity: sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
node-addon-api@1.7.2:
|
||||
|
|
@ -17213,6 +17225,10 @@ packages:
|
|||
uint4@0.1.2:
|
||||
resolution: {integrity: sha512-lhEx78gdTwFWG+mt6cWAZD/R6qrIj0TTBeH5xwyuDJyswLNlGe+KVlUPQ6+mx5Ld332pS0AMUTo9hIly7YsWxQ==}
|
||||
|
||||
uiohook-napi@1.5.5:
|
||||
resolution: {integrity: sha512-oSlTdnECw2GBfsJPTbBQBeE4v/EXP0EZmX6BJq5nzH/JgFaBE8JpFwEA/kLhiEP7HxQw28FViWiYgdIZzWuuJQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
unbash@2.2.0:
|
||||
resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -19947,9 +19963,9 @@ snapshots:
|
|||
dependencies:
|
||||
electron: 41.2.1
|
||||
|
||||
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
|
||||
'@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)':
|
||||
dependencies:
|
||||
'@types/node': 24.12.2
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@electron-toolkit/utils@4.0.0(electron@41.2.1)':
|
||||
dependencies:
|
||||
|
|
@ -20028,7 +20044,7 @@ snapshots:
|
|||
detect-libc: 2.1.2
|
||||
got: 11.8.6
|
||||
graceful-fs: 4.2.11
|
||||
node-abi: 4.24.0
|
||||
node-abi: 4.31.0
|
||||
node-api-version: 0.2.1
|
||||
node-gyp: 11.5.0
|
||||
ora: 5.4.1
|
||||
|
|
@ -20861,31 +20877,6 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
|
||||
'@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))
|
||||
'@intlify/shared': 11.3.2
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@typescript-eslint/scope-manager': 8.58.1
|
||||
'@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
pathe: 2.0.3
|
||||
picocolors: 1.1.1
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/compiler-dom'
|
||||
- eslint
|
||||
- rollup
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
|
||||
|
|
@ -22867,35 +22858,11 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
yauzl: 3.3.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- '@vitejs/devtools'
|
||||
- esbuild
|
||||
- jiti
|
||||
- less
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
|
|
@ -23796,7 +23763,6 @@ snapshots:
|
|||
'@types/node@25.6.0':
|
||||
dependencies:
|
||||
undici-types: 7.19.2
|
||||
optional: true
|
||||
|
||||
'@types/nprogress@0.2.3': {}
|
||||
|
||||
|
|
@ -24462,12 +24428,6 @@ snapshots:
|
|||
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||
|
|
@ -24770,15 +24730,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
|
|
@ -26920,7 +26871,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
|
||||
|
|
@ -26928,7 +26879,7 @@ snapshots:
|
|||
esbuild: 0.25.12
|
||||
magic-string: 0.30.21
|
||||
picocolors: 1.1.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -30257,7 +30208,7 @@ snapshots:
|
|||
dependencies:
|
||||
semver: 7.7.4
|
||||
|
||||
node-abi@4.24.0:
|
||||
node-abi@4.31.0:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
|
||||
|
|
@ -32809,6 +32760,10 @@ snapshots:
|
|||
|
||||
uint4@0.1.2: {}
|
||||
|
||||
uiohook-napi@1.5.5:
|
||||
dependencies:
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
unbash@2.2.0: {}
|
||||
|
||||
unconfig-core@7.5.0:
|
||||
|
|
@ -32840,8 +32795,7 @@ snapshots:
|
|||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
undici-types@7.19.2:
|
||||
optional: true
|
||||
undici-types@7.19.2: {}
|
||||
|
||||
undici@6.24.1: {}
|
||||
|
||||
|
|
@ -32953,6 +32907,11 @@ snapshots:
|
|||
'@unocss/preset-mini': 66.6.8
|
||||
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))):
|
||||
dependencies:
|
||||
'@unocss/preset-mini': 66.6.8
|
||||
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@unocss/cli': 66.6.8
|
||||
|
|
@ -33049,14 +33008,6 @@ snapshots:
|
|||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-rc.16
|
||||
rollup: 4.60.1
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
|
|
@ -33091,19 +33042,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
git-url-parse: 16.1.0
|
||||
simple-git: 3.36.0
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rollup: 4.60.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
|
|
@ -33213,17 +33151,6 @@ snapshots:
|
|||
rollup: 4.60.1
|
||||
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
unplugin: 3.0.0
|
||||
yaml: 2.8.3
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-rc.16
|
||||
rollup: 4.60.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin@2.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
|
|
@ -33455,22 +33382,12 @@ snapshots:
|
|||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
|
@ -33516,21 +33433,6 @@ snapshots:
|
|||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
error-stack-parser-es: 1.0.5
|
||||
ohash: 2.0.11
|
||||
open: 10.2.0
|
||||
perfect-debounce: 2.1.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
|
|
@ -33562,13 +33464,6 @@ snapshots:
|
|||
- typescript
|
||||
- ws
|
||||
|
||||
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
supports-color: 10.2.2
|
||||
undici: 8.1.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
|
|
@ -33587,20 +33482,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.1.1
|
||||
'@vue/devtools-shared': 8.1.1
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
|
||||
|
|
@ -33615,21 +33496,6 @@ snapshots:
|
|||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0)
|
||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0)
|
||||
'@vue/compiler-dom': 3.5.32
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
|
|
@ -33645,16 +33511,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
vue-router: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
|
|
@ -33921,54 +33777,6 @@ snapshots:
|
|||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3)
|
||||
'@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
unplugin: 2.3.11
|
||||
unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@rspack/core'
|
||||
- '@vueuse/core'
|
||||
- esbuild
|
||||
- rolldown
|
||||
- rollup
|
||||
- typescript
|
||||
- vite
|
||||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ catalog:
|
|||
mkcert: ^3.2.0
|
||||
nano-staged: ^1.0.2
|
||||
nanoid: 5.1.6
|
||||
node-abi: ^4.31.0
|
||||
node-pty: ^1.1.0
|
||||
ofetch: ^1.5.1
|
||||
oxc-minify: ^0.126.0
|
||||
|
|
@ -135,6 +136,7 @@ catalog:
|
|||
tinyexec: ^1.1.1
|
||||
tsdown: ^0.21.9
|
||||
tsx: ^4.21.0
|
||||
uiohook-napi: ^1.5.5
|
||||
uncrypto: ^0.1.3
|
||||
unplugin-info: ^1.3.2
|
||||
unstorage: ^1.17.5
|
||||
|
|
@ -194,6 +196,7 @@ onlyBuiltDependencies:
|
|||
- sharp
|
||||
- spawn-sync
|
||||
- stockfish
|
||||
- uiohook-napi
|
||||
- utf-8-validate
|
||||
- vue-demi
|
||||
packageExtensions:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue