diff --git a/src/client/directives/hotkey.ts b/src/client/directives/hotkey.ts index a1c49f0074..d813a95074 100644 --- a/src/client/directives/hotkey.ts +++ b/src/client/directives/hotkey.ts @@ -1,100 +1,11 @@ import { Directive } from 'vue'; -import keyCode from '../scripts/keycode'; -import { concat } from '../../prelude/array'; - -type pattern = { - which: string[]; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; -}; - -type action = { - patterns: pattern[]; - - callback: Function; - - allowRepeat: boolean; -}; - -const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => { - const result = { - patterns: [], - callback: callback, - allowRepeat: true - } as action; - - if (patterns.match(/^\(.*\)$/) !== null) { - result.allowRepeat = false; - patterns = patterns.slice(1, -1); - } - - result.patterns = patterns.split('|').map(part => { - const pattern = { - which: [], - ctrl: false, - alt: false, - shift: false - } as pattern; - - const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); - for (const key of keys) { - switch (key) { - case 'ctrl': pattern.ctrl = true; break; - case 'alt': pattern.alt = true; break; - case 'shift': pattern.shift = true; break; - default: pattern.which = keyCode(key).map(k => k.toLowerCase()); - } - } - - return pattern; - }); - - return result; -}); - -const ignoreElemens = ['input', 'textarea']; - -function match(e: KeyboardEvent, patterns: action['patterns']): boolean { - const key = e.code.toLowerCase(); - return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl === e.ctrlKey && - pattern.shift === e.shiftKey && - pattern.alt === e.altKey && - !e.metaKey - ); -} +import { makeHotkey } from '../scripts/hotkey'; export default { mounted(el, binding) { el._hotkey_global = binding.modifiers.global === true; - const actions = getKeyMap(binding.value); - - // flatten - const reservedKeys = concat(actions.map(a => a.patterns)); - - el._misskey_reservedKeys = reservedKeys; - - el._keyHandler = (e: KeyboardEvent) => { - const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : []; - if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; - if (document.activeElement && document.activeElement.attributes['contenteditable']) return; - - for (const action of actions) { - const matched = match(e, action.patterns); - - if (matched) { - if (!action.allowRepeat && e.repeat) return; - if (el._hotkey_global && match(e, targetReservedKeys)) return; - - e.preventDefault(); - e.stopPropagation(); - action.callback(e); - break; - } - } - }; + el._keyHandler = makeHotkey(binding.value); if (el._hotkey_global) { document.addEventListener('keydown', el._keyHandler); diff --git a/src/client/init.ts b/src/client/init.ts index 4af6f25780..05fbec3a34 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -45,15 +45,17 @@ import { router } from '@/router'; import { applyTheme } from '@/scripts/theme'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; import { i18n } from '@/i18n'; -import { stream, isMobile, dialog } from '@/os'; +import { stream, isMobile, dialog, post } from '@/os'; import * as sound from '@/scripts/sound'; import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; import { defaultStore, ColdDeviceStorage } from '@/store'; import { fetchInstance, instance } from '@/instance'; +import { makeHotkey } from './scripts/hotkey'; +import { search } from './scripts/search'; console.info(`Misskey v${version}`); -window.clearTimeout(window.mkBootTimer); +window.clearTimeout((window as any).mkBootTimer); if (_DEV_) { console.warn('Development mode!!!'); @@ -214,6 +216,16 @@ window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { }); //#endregion +// shortcut +document.addEventListener('keydown', makeHotkey({ + 'd': () => { + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 'p|n': post, + 's': search, + //TODO: 'h|/': help +})); + watch(defaultStore.reactiveState.useBlurEffectForModal, v => { document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); }, { immediate: true }); diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue index 720874fd54..d83e243081 100644 --- a/src/client/pages/settings/theme.vue +++ b/src/client/pages/settings/theme.vue @@ -99,7 +99,7 @@ export default defineComponent({ const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light')); const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); - const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); + const darkMode = defaultStore.reactiveState.darkMode; const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); const wallpaper = ref(localStorage.getItem('wallpaper')); diff --git a/src/client/scripts/hotkey.ts b/src/client/scripts/hotkey.ts new file mode 100644 index 0000000000..2b3f491fd8 --- /dev/null +++ b/src/client/scripts/hotkey.ts @@ -0,0 +1,88 @@ +import keyCode from './keycode'; + +type Keymap = Record; + +type Pattern = { + which: string[]; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; +}; + +type Action = { + patterns: Pattern[]; + callback: Function; + allowRepeat: boolean; +}; + +const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { + const result = { + patterns: [], + callback: callback, + allowRepeat: true + } as Action; + + if (patterns.match(/^\(.*\)$/) !== null) { + result.allowRepeat = false; + patterns = patterns.slice(1, -1); + } + + result.patterns = patterns.split('|').map(part => { + const pattern = { + which: [], + ctrl: false, + alt: false, + shift: false + } as Pattern; + + const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); + for (const key of keys) { + switch (key) { + case 'ctrl': pattern.ctrl = true; break; + case 'alt': pattern.alt = true; break; + case 'shift': pattern.shift = true; break; + default: pattern.which = keyCode(key).map(k => k.toLowerCase()); + } + } + + return pattern; + }); + + return result; +}); + +const ignoreElemens = ['input', 'textarea']; + +function match(e: KeyboardEvent, patterns: Action['patterns']): boolean { + const key = e.code.toLowerCase(); + return patterns.some(pattern => pattern.which.includes(key) && + pattern.ctrl === e.ctrlKey && + pattern.shift === e.shiftKey && + pattern.alt === e.altKey && + !e.metaKey + ); +} + +export const makeHotkey = (keymap: Keymap) => { + const actions = parseKeymap(keymap); + + return (e: KeyboardEvent) => { + if (document.activeElement) { + if (ignoreElemens.some(el => document.activeElement!.matches(el))) return; + if (document.activeElement.attributes['contenteditable']) return; + } + + for (const action of actions) { + const matched = match(e, action.patterns); + + if (matched) { + if (!action.allowRepeat && e.repeat) return; + + e.preventDefault(); + e.stopPropagation(); + action.callback(e); + break; + } + } + }; +}; diff --git a/src/client/ui/_common_/common.vue b/src/client/ui/_common_/common.vue index a4d1661f46..e5cdaca235 100644 --- a/src/client/ui/_common_/common.vue +++ b/src/client/ui/_common_/common.vue @@ -17,7 +17,7 @@ import { defineAsyncComponent, defineComponent } from 'vue'; import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os'; import * as sound from '@/scripts/sound'; -import { $i, $i } from '@/account'; +import { $i } from '@/account'; export default defineComponent({ components: { diff --git a/src/client/ui/deck.vue b/src/client/ui/deck.vue index 0cbfd7a48b..6cdf56f3c3 100644 --- a/src/client/ui/deck.vue +++ b/src/client/ui/deck.vue @@ -1,5 +1,5 @@