mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-30 20:29:13 +01:00
nanka iroiro
This commit is contained in:
parent
03667e1fe6
commit
3fc427b699
9 changed files with 110 additions and 158 deletions
|
@ -1,100 +1,11 @@
|
||||||
import { Directive } from 'vue';
|
import { Directive } from 'vue';
|
||||||
import keyCode from '../scripts/keycode';
|
import { makeHotkey } from '../scripts/hotkey';
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
el._hotkey_global = binding.modifiers.global === true;
|
el._hotkey_global = binding.modifiers.global === true;
|
||||||
|
|
||||||
const actions = getKeyMap(binding.value);
|
el._keyHandler = makeHotkey(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (el._hotkey_global) {
|
if (el._hotkey_global) {
|
||||||
document.addEventListener('keydown', el._keyHandler);
|
document.addEventListener('keydown', el._keyHandler);
|
||||||
|
|
|
@ -45,15 +45,17 @@ import { router } from '@/router';
|
||||||
import { applyTheme } from '@/scripts/theme';
|
import { applyTheme } from '@/scripts/theme';
|
||||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { stream, isMobile, dialog } from '@/os';
|
import { stream, isMobile, dialog, post } from '@/os';
|
||||||
import * as sound from '@/scripts/sound';
|
import * as sound from '@/scripts/sound';
|
||||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store';
|
import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||||
import { fetchInstance, instance } from '@/instance';
|
import { fetchInstance, instance } from '@/instance';
|
||||||
|
import { makeHotkey } from './scripts/hotkey';
|
||||||
|
import { search } from './scripts/search';
|
||||||
|
|
||||||
console.info(`Misskey v${version}`);
|
console.info(`Misskey v${version}`);
|
||||||
|
|
||||||
window.clearTimeout(window.mkBootTimer);
|
window.clearTimeout((window as any).mkBootTimer);
|
||||||
|
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
console.warn('Development mode!!!');
|
console.warn('Development mode!!!');
|
||||||
|
@ -214,6 +216,16 @@ window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
|
||||||
});
|
});
|
||||||
//#endregion
|
//#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 => {
|
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
||||||
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
|
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default defineComponent({
|
||||||
const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light'));
|
const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light'));
|
||||||
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
|
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
|
||||||
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
|
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
|
||||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
const darkMode = defaultStore.reactiveState.darkMode;
|
||||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||||
const wallpaper = ref(localStorage.getItem('wallpaper'));
|
const wallpaper = ref(localStorage.getItem('wallpaper'));
|
||||||
|
|
||||||
|
|
88
src/client/scripts/hotkey.ts
Normal file
88
src/client/scripts/hotkey.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import keyCode from './keycode';
|
||||||
|
|
||||||
|
type Keymap = Record<string, Function>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -17,7 +17,7 @@
|
||||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os';
|
import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os';
|
||||||
import * as sound from '@/scripts/sound';
|
import * as sound from '@/scripts/sound';
|
||||||
import { $i, $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-deck" :class="`${deckStore.reactiveState.columnAlign.value}`" v-hotkey.global="keymap" @contextmenu.self.prevent="onContextmenu"
|
<div class="mk-deck" :class="`${deckStore.reactiveState.columnAlign.value}`" @contextmenu.self.prevent="onContextmenu"
|
||||||
:style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
|
:style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
|
||||||
>
|
>
|
||||||
<XSidebar ref="nav"/>
|
<XSidebar ref="nav"/>
|
||||||
|
@ -35,7 +35,6 @@ import { faPlus, faPencilAlt, faChevronLeft, faBars, faCircle } from '@fortaweso
|
||||||
import { } from '@fortawesome/free-regular-svg-icons';
|
import { } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { host } from '@/config';
|
import { host } from '@/config';
|
||||||
import { search } from '@/scripts/search';
|
|
||||||
import DeckColumnCore from '@/ui/deck/column-core.vue';
|
import DeckColumnCore from '@/ui/deck/column-core.vue';
|
||||||
import XSidebar from '@/components/sidebar.vue';
|
import XSidebar from '@/components/sidebar.vue';
|
||||||
import { getScrollContainer } from '@/scripts/scroll';
|
import { getScrollContainer } from '@/scripts/scroll';
|
||||||
|
@ -75,14 +74,6 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
keymap(): any {
|
|
||||||
return {
|
|
||||||
'p': this.post,
|
|
||||||
'n': this.post,
|
|
||||||
's': this.search,
|
|
||||||
'h|/': this.help
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-app" v-hotkey.global="keymap" :class="{ wallpaper }">
|
<div class="mk-app" :class="{ wallpaper }">
|
||||||
<XSidebar ref="nav" class="sidebar"/>
|
<XSidebar ref="nav" class="sidebar"/>
|
||||||
|
|
||||||
<div class="contents" ref="contents">
|
<div class="contents" ref="contents">
|
||||||
|
@ -57,7 +57,6 @@ import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
|
||||||
import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { host } from '@/config';
|
import { host } from '@/config';
|
||||||
import { search } from '@/scripts/search';
|
|
||||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||||
import XSidebar from '@/components/sidebar.vue';
|
import XSidebar from '@/components/sidebar.vue';
|
||||||
import XCommon from './_common_/common.vue';
|
import XCommon from './_common_/common.vue';
|
||||||
|
@ -65,7 +64,6 @@ import XHeader from './_common_/header.vue';
|
||||||
import XSide from './default.side.vue';
|
import XSide from './default.side.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { sidebarDef } from '@/sidebar';
|
import { sidebarDef } from '@/sidebar';
|
||||||
import { ColdDeviceStorage } from '@/store';
|
|
||||||
|
|
||||||
const DESKTOP_THRESHOLD = 1100;
|
const DESKTOP_THRESHOLD = 1100;
|
||||||
|
|
||||||
|
@ -101,19 +99,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
keymap(): any {
|
|
||||||
return {
|
|
||||||
'd': () => {
|
|
||||||
if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
|
|
||||||
this.$store.set('darkMode', !this.$store.state.darkMode);
|
|
||||||
},
|
|
||||||
'p': os.post,
|
|
||||||
'n': os.post,
|
|
||||||
's': () => search(),
|
|
||||||
'h|/': this.help
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
navIndicated(): boolean {
|
navIndicated(): boolean {
|
||||||
for (const def in this.menuDef) {
|
for (const def in this.menuDef) {
|
||||||
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
||||||
|
@ -199,10 +184,6 @@ export default defineComponent({
|
||||||
window.scroll({ top: 0, behavior: 'smooth' });
|
window.scroll({ top: 0, behavior: 'smooth' });
|
||||||
},
|
},
|
||||||
|
|
||||||
help() {
|
|
||||||
this.$router.push('/docs/keyboard-shortcut');
|
|
||||||
},
|
|
||||||
|
|
||||||
onTransition() {
|
onTransition() {
|
||||||
if (window._scroll) window._scroll();
|
if (window._scroll) window._scroll();
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-app" v-hotkey.global="keymap" :class="{ wallpaper }" @contextmenu.prevent="() => {}">
|
<div class="mk-app" :class="{ wallpaper }" @contextmenu.prevent="() => {}">
|
||||||
<XSidebar ref="nav" class="sidebar"/>
|
<XSidebar ref="nav" class="sidebar"/>
|
||||||
|
|
||||||
<XCommon/>
|
<XCommon/>
|
||||||
|
@ -31,19 +31,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
keymap(): any {
|
|
||||||
return {
|
|
||||||
'd': () => {
|
|
||||||
if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
|
|
||||||
this.$store.set('darkMode', !this.$store.state.darkMode);
|
|
||||||
},
|
|
||||||
'p': os.post,
|
|
||||||
'n': os.post,
|
|
||||||
's': () => search(),
|
|
||||||
'h|/': this.help
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
menu(): string[] {
|
menu(): string[] {
|
||||||
return this.$store.state.menu;
|
return this.$store.state.menu;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-app" v-hotkey.global="keymap">
|
<div class="mk-app">
|
||||||
<div class="contents">
|
<div class="contents">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<XHeader :info="pageInfo"/>
|
<XHeader :info="pageInfo"/>
|
||||||
|
@ -26,11 +26,8 @@ import { defineComponent, defineAsyncComponent } from 'vue';
|
||||||
import { faLayerGroup, faBars, faHome, faCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faLayerGroup, faBars, faHome, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { host } from '@/config';
|
import { host } from '@/config';
|
||||||
import { search } from '@/scripts/search';
|
|
||||||
import XHeader from './_common_/header.vue';
|
import XHeader from './_common_/header.vue';
|
||||||
import XCommon from './_common_/common.vue';
|
import XCommon from './_common_/common.vue';
|
||||||
import * as os from '@/os';
|
|
||||||
import { ColdDeviceStorage } from '@/store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -47,21 +44,6 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
keymap(): any {
|
|
||||||
return {
|
|
||||||
'd': () => {
|
|
||||||
if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
|
|
||||||
this.$store.set('darkMode', !this.$store.state.darkMode);
|
|
||||||
},
|
|
||||||
'p': os.post,
|
|
||||||
'n': os.post,
|
|
||||||
's': search,
|
|
||||||
'h|/': this.help
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
$route(to, from) {
|
||||||
this.pageKey++;
|
this.pageKey++;
|
||||||
|
|
Loading…
Reference in a new issue