diff --git a/frontend/src/components/FileTabs.vue b/frontend/src/components/FileTabs.vue index b23089a1..2de25ae8 100644 --- a/frontend/src/components/FileTabs.vue +++ b/frontend/src/components/FileTabs.vue @@ -8,6 +8,7 @@ import Tabs from './Tabs.vue' import File from '../useful/file' import { defineComponent, onBeforeMount, onBeforeUnmount, ref, toRefs, watch } from 'vue' import { Components } from '../types' +import { isAction } from '../useful/shortcut' const blankUri = File.toUri(null) @@ -72,14 +73,14 @@ export default defineComponent({ } // 快捷键切换最近文档 - if (e.altKey && e.ctrlKey) { - if (e.key === 'ArrowLeft') { - const prev = findTab(-1) - prev && switchTab(prev) - } else if (e.key === 'ArrowRight') { - const next = findTab(1) - next && switchTab(next) - } + if (isAction(e, 'file-tabs-switch-left')) { + const prev = findTab(-1) + prev && switchTab(prev) + } + + if (isAction(e, 'file-tabs-switch-right')) { + const next = findTab(1) + next && switchTab(next) } } diff --git a/frontend/src/components/Filter.vue b/frontend/src/components/Filter.vue index efccceac..7def0b7f 100644 --- a/frontend/src/components/Filter.vue +++ b/frontend/src/components/Filter.vue @@ -8,6 +8,7 @@ import { useStore } from 'vuex' import { computed, defineComponent, onMounted, onUnmounted, ref, toRef } from 'vue' import { encodeMarkdownLink } from '../useful/utils' +import { getCurrentAction } from '../useful/shortcut' import { useBus } from '../useful/bus' import XMask from './Mask.vue' import QuickOpen from './QuickOpen.vue' @@ -24,26 +25,29 @@ export default defineComponent({ const withMarked = ref(true) function keydownHandler (e: KeyboardEvent) { - if (e.key === 'i' && e.ctrlKey && e.altKey) { - callback.value = (f: any) => { - const file = currentFile.value - if (file) { - const relativePath = f.path.replace(file.path.substr(0, file.path.lastIndexOf('/')), '.') - bus.emit('editor-insert-value', `[${f.name.replace(/\.[^.]+$/, '')}](${encodeMarkdownLink(relativePath)})`) + switch (getCurrentAction(e)) { + case 'insert-document': + callback.value = (f: any) => { + const file = currentFile.value + if (file) { + const relativePath = f.path.replace(file.path.substr(0, file.path.lastIndexOf('/')), '.') + bus.emit('editor-insert-value', `[${f.name.replace(/\.[^.]+$/, '')}](${encodeMarkdownLink(relativePath)})`) + } + callback.value = null } - callback.value = null - } - withMarked.value = false - e.preventDefault() - e.stopPropagation() - } else if (e.key === 'p' && e.ctrlKey) { - callback.value = (f: any) => { - store.commit('setCurrentFile', f) - callback.value = null - } - withMarked.value = true - e.preventDefault() - e.stopPropagation() + withMarked.value = false + e.preventDefault() + e.stopPropagation() + break + case 'show-quick-open': + callback.value = (f: any) => { + store.commit('setCurrentFile', f) + callback.value = null + } + withMarked.value = true + e.preventDefault() + e.stopPropagation() + break } } diff --git a/frontend/src/components/MonacoEditor.vue b/frontend/src/components/MonacoEditor.vue index 8aa00338..ae3d492d 100644 --- a/frontend/src/components/MonacoEditor.vue +++ b/frontend/src/components/MonacoEditor.vue @@ -12,7 +12,7 @@ import env from '../useful/env' const models: {[key: string]: monaco.editor.ITextModel} = {} -const keys: {[key: string]: boolean} = {} +let keys: {[key: string]: boolean} = {} export default defineComponent({ name: 'monaco-editor', @@ -80,17 +80,17 @@ export default defineComponent({ const KM = getMonaco().KeyMod const KC = getMonaco().KeyCode - getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_F, addAttachment) + getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_F, addAttachment) - getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_D, () => { + getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_D, () => { insert(dayjs().format('YYYY-MM-DD')) }) - getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_T, () => { + getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_T, () => { insert(dayjs().format('HH:mm:ss')) }) - getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_R, () => { + getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_R, () => { emit('xterm-run', getEditor().getModel()!!.getValueInRange(getEditor().getSelection()!!)) }) @@ -156,7 +156,7 @@ export default defineComponent({ function paste (e: ClipboardEvent) { if (getEditor().hasTextFocus()) { const items = e.clipboardData!!.items - if (keys.m || keys.M) { // 粘贴 HTML 转为 markdown + if (keys.d || keys.D) { // 粘贴 HTML 转为 markdown for (let i = 0; i < items.length; i++) { if (items[i].type.match(/^text\/html$/i)) { items[i].getAsString(str => { @@ -172,7 +172,7 @@ export default defineComponent({ for (let i = 0; i < items.length; i++) { const fileType = items[i].type if (fileType.match(/^image\/(png|jpg|jpeg|gif)$/i)) { - const asBase64 = keys.b || keys.B // Ctrl + v 粘贴的同时 按下了 B 键,就粘贴 base64 图像 + const asBase64 = keys.b || keys.B // 粘贴的同时 按下了 B 键,就粘贴 base64 图像 emit('paste-img', items[i].getAsFile(), asBase64) } } @@ -243,7 +243,7 @@ export default defineComponent({ if (e.type === 'keydown') { keys[e.key] = true } else { - keys[e.key] = false + keys = {} } } diff --git a/frontend/src/components/Preview.vue b/frontend/src/components/Preview.vue index 2e401335..fc019311 100644 --- a/frontend/src/components/Preview.vue +++ b/frontend/src/components/Preview.vue @@ -71,6 +71,8 @@ import 'highlight.js/styles/atom-one-dark.css' import 'katex/dist/katex.min.css' import { useBus } from '../useful/bus' import { useToast } from '../useful/toast' +import { hasCtrlCmd, isAction } from '../useful/shortcut' + HighlightLineNumber.addStyles() const markdown = Markdown({ @@ -311,7 +313,7 @@ export default defineComponent({ async function keydownHandler (e: KeyboardEvent) { // 转换所有外链图片到本地 - if (e.key === 'l' && e.ctrlKey && e.altKey) { + if (isAction(e, 'transform-img-link')) { e.preventDefault() e.stopPropagation() const result = [] @@ -420,14 +422,19 @@ export default defineComponent({ } // 复制标题链接 - if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && e.ctrlKey) { + if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && hasCtrlCmd(e)) { bus.emit('copy-text', encodeMarkdownLink(filePath.value) + '#' + encodeMarkdownLink(decodeURIComponent(target.id))) return preventEvent() } + // 复制内容 + if (target.classList.contains('copy-inner-text')) { + bus.emit('copy-text', target.innerText) + } + if (target.tagName === 'IMG') { const img = target as HTMLImageElement - if (e.ctrlKey && e.shiftKey) { // 转换外链图片到本地 + if (isAction(e, 'transform-img-link-by-click')) { // 转换外链图片到本地 const data = await transformImgOutLink(img) if (data) { bus.emit('tree-refresh') @@ -450,7 +457,7 @@ export default defineComponent({ return } - if (target.tagName === 'TD' && target.classList.contains('yank-td') && e.ctrlKey) { + if (target.tagName === 'TD' && target.classList.contains('yank-td') && hasCtrlCmd(e)) { const start = parseInt(target.dataset.sourceLine || '0') const end = parseInt(target.dataset.sourceLineEnd || '0') const td = target as HTMLTableDataCellElement diff --git a/frontend/src/components/StatusBar.vue b/frontend/src/components/StatusBar.vue index 2b5b651c..49325b31 100644 --- a/frontend/src/components/StatusBar.vue +++ b/frontend/src/components/StatusBar.vue @@ -9,11 +9,11 @@ 字符数:{{documentInfo.textLength}} 已选中:{{documentInfo.selectedLength}} -
切换侧栏
-
切换换行
-
切换预览
-
切换终端
-
README
+
切换侧栏
+
切换换行
+
切换预览
+
切换终端
+
README
特色功能说明
{{autoPreview ? '同步渲染-已开启' : '同步渲染-已关闭'}}
@@ -27,6 +27,7 @@ import { useStore } from 'vuex' import { useBus } from '../useful/bus' import RepositorySwitch from './RepositorySwitch.vue' import SvgIcon from '../components/SvgIcon.vue' +import { isAction, getActionLabel } from '../useful/shortcut' export default defineComponent({ name: 'status-bar', @@ -69,31 +70,31 @@ export default defineComponent({ } function keydownHandler (e: KeyboardEvent) { - if (e.key === 'e' && e.altKey) { + if (isAction(e, 'toggle-side')) { toggleSide() e.preventDefault() e.stopPropagation() } - if (e.key === 'v' && e.altKey) { + if (isAction(e, 'toggle-view')) { toggleView() e.preventDefault() e.stopPropagation() } - if (e.key === 'o' && e.altKey) { + if (isAction(e, 'toggle-xterm')) { toggleXterm() e.preventDefault() e.stopPropagation() } - if (e.key === 'w' && e.altKey) { + if (isAction(e, 'toggle-wrap')) { toggleWrap() e.preventDefault() e.stopPropagation() } - if (e.key === 'h' && e.altKey) { + if (isAction(e, 'toggle-readme')) { toggleReadme() e.preventDefault() e.stopPropagation() @@ -119,6 +120,7 @@ export default defineComponent({ toggleFeature, toggleRender, rerenderView, + getActionLabel, } }, }) diff --git a/frontend/src/plugins/CodePlugin.ts b/frontend/src/plugins/CodePlugin.ts index ec7d862d..7d96bf36 100644 --- a/frontend/src/plugins/CodePlugin.ts +++ b/frontend/src/plugins/CodePlugin.ts @@ -1,10 +1,11 @@ +import { getKeyLabel } from '@/useful/shortcut' import Markdown from 'markdown-it' export default (md: Markdown) => { const Fun = (fn: Function) => (tokens: any, idx: any, options: any, env: any, slf: any) => { if (tokens[idx].attrIndex('title') < 0) { - tokens[idx].attrPush(['title', 'Ctrl + 单击复制代码']) - tokens[idx].attrPush(['onclick', "event.ctrlKey && window.globalBus.emit('copy-text', this.innerText)"]) + tokens[idx].attrJoin('class', 'copy-inner-text') + tokens[idx].attrPush(['title', getKeyLabel('CtrlCmd') + ' + 单击复制']) } return (fn)(tokens, idx, options, env, slf) diff --git a/frontend/src/plugins/TocPlugin.ts b/frontend/src/plugins/TocPlugin.ts index 1243ea1f..99d9cb2c 100644 --- a/frontend/src/plugins/TocPlugin.ts +++ b/frontend/src/plugins/TocPlugin.ts @@ -1,6 +1,7 @@ // https://github.com/Oktavilla/markdown-it-table-of-contents import Markdown from 'markdown-it' +import { getKeyLabel } from '@/useful/shortcut' import { injectLineNumbers } from './SourceLinePlugin' @@ -185,7 +186,7 @@ export default (md: Markdown, o: any) => { } if (header.attrIndex('title') < 0) { - header.attrSet('title', 'Ctrl + 单击复制链接') + header.attrSet('title', getKeyLabel('CtrlCmd') + ' + 单击复制链接') } return injectLineNumbers(tokens, idx, opt, env, slf) diff --git a/frontend/src/useful/env.ts b/frontend/src/useful/env.ts index b7589d3c..a66bf01b 100644 --- a/frontend/src/useful/env.ts +++ b/frontend/src/useful/env.ts @@ -28,7 +28,7 @@ const openAlwaysOnTopWindow = (url: string, target = '_blank') => { } window.addEventListener('keydown', e => { - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { if (e.key === '0') { webContents.setZoomFactor(1) } else if (e.key === '=') { @@ -40,7 +40,7 @@ const openAlwaysOnTopWindow = (url: string, target = '_blank') => { }) window.addEventListener('mousewheel', e => { - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { changeZoomFactor(e.deltaY < 0) } }) diff --git a/frontend/src/useful/shortcut.ts b/frontend/src/useful/shortcut.ts new file mode 100644 index 00000000..52d2603d --- /dev/null +++ b/frontend/src/useful/shortcut.ts @@ -0,0 +1,85 @@ +import { getLogger } from './utils' + +const logger = getLogger('shortcut') + +const isMacOS = /macintosh|mac os x/i.test(navigator.userAgent) +const Ctrl = 'Ctrl' +const CtrlCmd = 'CtrlCmd' +const Alt = 'Alt' +const Shift = 'Shift' +const LeftClick = 0 + +type XKey = typeof Ctrl | typeof CtrlCmd | typeof Alt | typeof Shift + +const actions = { + 'toggle-side': [Alt, 'e'], + 'toggle-wrap': [Alt, 'w'], + 'toggle-view': [Alt, 'v'], + 'toggle-xterm': [Alt, 't'], + 'toggle-readme': [Alt, 'h'], + 'insert-document': [CtrlCmd, Alt, 'i'], + 'show-quick-open': [CtrlCmd, 'p'], + 'transform-img-link-by-click': [CtrlCmd, Shift, LeftClick], + 'transform-img-link': [CtrlCmd, Shift, 'l'], + 'file-tabs-switch-left': [Ctrl, Alt, 'ArrowLeft'], + 'file-tabs-switch-right': [Ctrl, Alt, 'ArrowRight'], +} + +type ActionName = keyof typeof actions + +export const getKeyLabel = (key: XKey | string | number) => { + switch (key) { + case CtrlCmd: + return isMacOS ? 'Cmd' : 'Ctrl' + + case Alt: + return isMacOS ? 'Option' : 'Ctrl' + + default: + return key.toString().toUpperCase() + } +} + +export const getActionLabel = (name: ActionName) => { + const keys: any[] = actions[name] + return keys.map(getKeyLabel).join(' + ') +} + +export const hasCtrlCmd = (e: KeyboardEvent | MouseEvent) => isMacOS ? e.metaKey : e.ctrlKey + +export const matchKeys = (e: KeyboardEvent | MouseEvent, keys: (string | number)[]) => { + for (const key of keys) { + switch (key) { + case CtrlCmd: + if (!hasCtrlCmd(e)) return false + break + case Alt: + if (!e.altKey) return false + break + case Ctrl: + if (!e.ctrlKey) return false + break + case Shift: + if (!e.shiftKey) return false + break + default: + if (e instanceof KeyboardEvent) { + if (key !== e.key && `Key${key.toString().toUpperCase()}` !== e.code) return false + } else { + if (key !== e.button) return false + } + } + } + + return true +} + +export const getCurrentAction = (e: KeyboardEvent | MouseEvent) => { + logger.debug('getCurrentAction', { e }) + return (Object.keys(actions) as ActionName[]).find(action => matchKeys(e, actions[action])) +} + +export const isAction = (e: KeyboardEvent | MouseEvent, name: ActionName) => { + logger.debug('isAction', name, e) + return matchKeys(e, actions[name]) +} diff --git a/frontend/src/useful/startup.ts b/frontend/src/useful/startup.ts index f770a683..2396b7be 100644 --- a/frontend/src/useful/startup.ts +++ b/frontend/src/useful/startup.ts @@ -35,7 +35,7 @@ export default function startup () { } window.addEventListener('keydown', e => { - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { if (e.key === '0') { webContents.setZoomFactor(1) } else if (e.key === '=') { @@ -49,7 +49,7 @@ export default function startup () { window.addEventListener('mousewheel', event => { const e = event as WheelEvent - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { changeZoomFactor(e.deltaY < 0) } }) diff --git a/frontend/src/useful/utils.ts b/frontend/src/useful/utils.ts index 634db6c8..8ef739aa 100644 --- a/frontend/src/useful/utils.ts +++ b/frontend/src/useful/utils.ts @@ -44,3 +44,18 @@ export const dataURItoBlobLink = (dataURI: string) => { const blob = new Blob([ab], { type: mimeString }) return window.URL.createObjectURL(blob) } + +export const getLogger = (subject: string) => { + const logger = (level: string) => (...args: any) => { + const time = `${new Date().toLocaleString()}.${Date.now() % 1000}` + ;(console as any)[level](`[${time}] ${subject} >`, ...args) + } + + return { + debug: logger('debug'), + log: logger('log'), + info: logger('info'), + warn: logger('warn'), + error: logger('error') + } +}