mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-28 15:23:09 +02: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 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);
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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'));
|
||||
|
||||
|
|
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 { 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: {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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' }"
|
||||
>
|
||||
<XSidebar ref="nav"/>
|
||||
|
@ -35,7 +35,6 @@ import { faPlus, faPencilAlt, faChevronLeft, faBars, faCircle } from '@fortaweso
|
|||
import { } from '@fortawesome/free-regular-svg-icons';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { host } from '@/config';
|
||||
import { search } from '@/scripts/search';
|
||||
import DeckColumnCore from '@/ui/deck/column-core.vue';
|
||||
import XSidebar from '@/components/sidebar.vue';
|
||||
import { getScrollContainer } from '@/scripts/scroll';
|
||||
|
@ -75,14 +74,6 @@ export default defineComponent({
|
|||
}
|
||||
return false;
|
||||
},
|
||||
keymap(): any {
|
||||
return {
|
||||
'p': this.post,
|
||||
'n': this.post,
|
||||
's': this.search,
|
||||
'h|/': this.help
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mk-app" v-hotkey.global="keymap" :class="{ wallpaper }">
|
||||
<div class="mk-app" :class="{ wallpaper }">
|
||||
<XSidebar ref="nav" class="sidebar"/>
|
||||
|
||||
<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 { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||
import { host } from '@/config';
|
||||
import { search } from '@/scripts/search';
|
||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||
import XSidebar from '@/components/sidebar.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
|
@ -65,7 +64,6 @@ import XHeader from './_common_/header.vue';
|
|||
import XSide from './default.side.vue';
|
||||
import * as os from '@/os';
|
||||
import { sidebarDef } from '@/sidebar';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
|
||||
const DESKTOP_THRESHOLD = 1100;
|
||||
|
||||
|
@ -101,19 +99,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
|
||||
};
|
||||
},
|
||||
|
||||
navIndicated(): boolean {
|
||||
for (const def in this.menuDef) {
|
||||
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
||||
|
@ -199,10 +184,6 @@ export default defineComponent({
|
|||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
|
||||
help() {
|
||||
this.$router.push('/docs/keyboard-shortcut');
|
||||
},
|
||||
|
||||
onTransition() {
|
||||
if (window._scroll) window._scroll();
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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"/>
|
||||
|
||||
<XCommon/>
|
||||
|
@ -31,19 +31,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
|
||||
};
|
||||
},
|
||||
|
||||
menu(): string[] {
|
||||
return this.$store.state.menu;
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mk-app" v-hotkey.global="keymap">
|
||||
<div class="mk-app">
|
||||
<div class="contents">
|
||||
<header class="header">
|
||||
<XHeader :info="pageInfo"/>
|
||||
|
@ -26,11 +26,8 @@ import { defineComponent, defineAsyncComponent } from 'vue';
|
|||
import { faLayerGroup, faBars, faHome, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||
import { host } from '@/config';
|
||||
import { search } from '@/scripts/search';
|
||||
import XHeader from './_common_/header.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
|
||||
export default defineComponent({
|
||||
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: {
|
||||
$route(to, from) {
|
||||
this.pageKey++;
|
||||
|
|
Loading…
Reference in a new issue