mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02:00
feat(webreader): add shortcut information menu
This commit is contained in:
parent
40c1ca53e4
commit
1885f32416
3 changed files with 222 additions and 75 deletions
65
komga-webui/src/components/menus/ShortcutHelpMenu.vue
Normal file
65
komga-webui/src/components/menus/ShortcutHelpMenu.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<v-menu offset-y :max-height="$vuetify.breakpoint.height">
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-information</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row no-gutters>
|
||||||
|
<template v-for="(item,i) in shortcutHelp.items()">
|
||||||
|
<v-col :key="i" cols="12" md="4">
|
||||||
|
<div class="text-center">
|
||||||
|
{{ item.key }}
|
||||||
|
</div>
|
||||||
|
<v-simple-table>
|
||||||
|
<template v-slot:default>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Key</th>
|
||||||
|
<th class="text-left">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(s,i) in item.value" :key="i">
|
||||||
|
<td>
|
||||||
|
<kbd style="height: 24px;" class="text-truncate">
|
||||||
|
<v-icon class="white--text text-capitalize" style="font-size: 1rem;">
|
||||||
|
{{ s.key }}
|
||||||
|
</v-icon>
|
||||||
|
</kbd>
|
||||||
|
</td>
|
||||||
|
<td>{{ s.desc }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-col>
|
||||||
|
</template>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { shortcutHelp } from '@/functions/shortcuts'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'ShortcutHelpMenu',
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
shortcutHelp: shortcutHelp,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -1,78 +1,152 @@
|
||||||
import { ReadingDirection } from '@/types/enum-books'
|
import { ReadingDirection } from '@/types/enum-books'
|
||||||
|
|
||||||
enum Shortcut {
|
interface Map<V> {
|
||||||
// Navigation
|
[key: string]: V
|
||||||
SEEK_FORWARD = 'seekForward',
|
|
||||||
SEEK_BACKWARD = 'seekBackward',
|
|
||||||
// Vertical mode
|
|
||||||
SEEK_UP = 'seekUp',
|
|
||||||
SEEK_DOWN = 'seekDown',
|
|
||||||
SEEK_BEGIN = 'seekBegin',
|
|
||||||
SEEK_END = 'seekEnd',
|
|
||||||
// SETTINGS
|
|
||||||
DIR_LTR = 'directionLTR',
|
|
||||||
DIR_RTL = 'directionRTL',
|
|
||||||
DIR_VRT = 'directionVRT',
|
|
||||||
TOGGLE_DOUBLE_PAGE = 'toggleDoublePage',
|
|
||||||
CYCLE_SCALE = 'cycleScale',
|
|
||||||
// OTHER
|
|
||||||
TOGGLE_TOOLBAR = 'toggleToolbar',
|
|
||||||
TOGGLE_MENU = 'toggleMenu',
|
|
||||||
TOGGLE_THUMBNAIL_EXPLORER = 'toggleExplorer',
|
|
||||||
ESCAPE = 'escape'
|
|
||||||
}
|
}
|
||||||
|
class MultiMap<V> {
|
||||||
|
dict: Map<V[]> = {}
|
||||||
|
|
||||||
interface KeyMapping {
|
add (key:string, value: V) {
|
||||||
[key: string]: Shortcut
|
this.dict[key] = (this.dict[key]?.concat([value])) || [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
get (key: string): V[] {
|
||||||
|
return this.dict[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
items () {
|
||||||
|
return Object.keys(this.dict).map((k) => ({ key: k, value: this.dict[k] }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = (ctx: any) => void
|
type Action = (ctx: any) => void
|
||||||
|
|
||||||
interface Shortcuts {
|
class Shortcut {
|
||||||
[key: string]: Action
|
name: string
|
||||||
|
category: string
|
||||||
|
description: string
|
||||||
|
action: Action
|
||||||
|
|
||||||
|
keys: string[]
|
||||||
|
|
||||||
|
constructor (name: string, category: string, description: string, action: Action, keys: string[]) {
|
||||||
|
this.name = name
|
||||||
|
this.category = category
|
||||||
|
this.description = description
|
||||||
|
this.action = action
|
||||||
|
this.keys = keys
|
||||||
|
}
|
||||||
|
|
||||||
|
execute (ctx: any): boolean {
|
||||||
|
this.action(ctx)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// consider making this configurable on the server side?
|
const KEY_DISPLAY = {
|
||||||
const keyMapping = {
|
'ArrowRight': 'mdi-arrow-right',
|
||||||
'PageUp': Shortcut.SEEK_FORWARD,
|
'ArrowLeft': 'mdi-arrow-left',
|
||||||
'ArrowRight': Shortcut.SEEK_FORWARD,
|
'PageUp': 'PgUp',
|
||||||
'PageDown': Shortcut.SEEK_BACKWARD,
|
'PageDown': 'PgDn',
|
||||||
'ArrowLeft': Shortcut.SEEK_BACKWARD,
|
'ArrowUp': 'mdi-arrow-up',
|
||||||
'ArrowDown': Shortcut.SEEK_DOWN,
|
'ArrowDown': 'mdi-arrow-down',
|
||||||
'ArrowUp': Shortcut.SEEK_UP,
|
'Escape': 'Esc',
|
||||||
'Home': Shortcut.SEEK_BEGIN,
|
} as Map<string>
|
||||||
'End': Shortcut.SEEK_END,
|
|
||||||
'm': Shortcut.TOGGLE_TOOLBAR,
|
|
||||||
's': Shortcut.TOGGLE_MENU,
|
|
||||||
't': Shortcut.TOGGLE_THUMBNAIL_EXPLORER,
|
|
||||||
'Escape': Shortcut.ESCAPE,
|
|
||||||
'l': Shortcut.DIR_LTR,
|
|
||||||
'r': Shortcut.DIR_RTL,
|
|
||||||
'v': Shortcut.DIR_VRT,
|
|
||||||
'd': Shortcut.TOGGLE_DOUBLE_PAGE,
|
|
||||||
'f': Shortcut.CYCLE_SCALE,
|
|
||||||
} as KeyMapping
|
|
||||||
|
|
||||||
const shortcuts = {
|
const SHORTCUTS: Shortcut[] = []
|
||||||
[Shortcut.SEEK_FORWARD]: (ctx: any) => {
|
|
||||||
|
function shortcut (name: string, category: string, description: string, action: Action, ...keys: string[]) {
|
||||||
|
SHORTCUTS.push(new Shortcut(name, category, description, action, keys))
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ShortcutCategory {
|
||||||
|
READER_NAVIGATION = 'Reader Navigation',
|
||||||
|
READER_SETTINGS = 'Reader Settings',
|
||||||
|
MENUS = 'Menus'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader Navigation
|
||||||
|
shortcut('seekForward', ShortcutCategory.READER_NAVIGATION, 'Next Page',
|
||||||
|
(ctx: any) => {
|
||||||
ctx.flipDirection ? ctx.prev() : ctx.next()
|
ctx.flipDirection ? ctx.prev() : ctx.next()
|
||||||
},
|
}, 'PageUp', 'ArrowRight')
|
||||||
[Shortcut.SEEK_BACKWARD]: (ctx: any) => {
|
|
||||||
|
shortcut('seekBackward', ShortcutCategory.READER_NAVIGATION, 'Prev Page',
|
||||||
|
(ctx: any) => {
|
||||||
ctx.flipDirection ? ctx.next() : ctx.prev()
|
ctx.flipDirection ? ctx.next() : ctx.prev()
|
||||||
|
}, 'PageDown', 'ArrowLeft')
|
||||||
|
|
||||||
|
shortcut('seekUp', ShortcutCategory.READER_NAVIGATION, 'Prev Page (Vertical)',
|
||||||
|
(ctx: any) => {
|
||||||
|
if (ctx.vertical) {
|
||||||
|
ctx.prev()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, 'ArrowUp')
|
||||||
|
|
||||||
|
shortcut('seekDown', ShortcutCategory.READER_NAVIGATION, 'Next Page (Vertical)',
|
||||||
|
(ctx: any) => {
|
||||||
|
if (ctx.vertical) {
|
||||||
|
ctx.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, 'ArrowDown')
|
||||||
|
|
||||||
|
shortcut('seekBegin', ShortcutCategory.READER_NAVIGATION, 'Goto First Page',
|
||||||
|
(ctx: any) => {
|
||||||
|
ctx.goToFirst()
|
||||||
|
}
|
||||||
|
, 'Home')
|
||||||
|
|
||||||
|
shortcut('seekEnd', ShortcutCategory.READER_NAVIGATION, 'Goto Last Page',
|
||||||
|
(ctx: any) => {
|
||||||
|
ctx.goToLast()
|
||||||
|
}
|
||||||
|
, 'End')
|
||||||
|
|
||||||
|
// Reader Settings
|
||||||
|
|
||||||
|
shortcut('directionLTR', ShortcutCategory.READER_SETTINGS, 'Direction: Left to Right',
|
||||||
|
(ctx: any) => ctx.changeReadingDir(ReadingDirection.LEFT_TO_RIGHT)
|
||||||
|
, 'l')
|
||||||
|
|
||||||
|
shortcut('directionRTL', ShortcutCategory.READER_SETTINGS, 'Direction: Right to Left',
|
||||||
|
(ctx: any) => ctx.changeReadingDir(ReadingDirection.RIGHT_TO_LEFT)
|
||||||
|
, 'r')
|
||||||
|
|
||||||
|
shortcut('directionVRT', ShortcutCategory.READER_SETTINGS, 'Direction: Vertical',
|
||||||
|
(ctx: any) => ctx.changeReadingDir(ReadingDirection.VERTICAL),
|
||||||
|
'v')
|
||||||
|
|
||||||
|
shortcut('toggleDoublePage', ShortcutCategory.READER_SETTINGS, 'Toggle Double Page',
|
||||||
|
(ctx: any) => ctx.toggleDoublePages()
|
||||||
|
, 'd')
|
||||||
|
|
||||||
|
shortcut('cycleScale', ShortcutCategory.READER_SETTINGS, 'Cycle Scale',
|
||||||
|
(ctx: any) => ctx.cycleScale()
|
||||||
|
, 'c')
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
|
||||||
|
shortcut('toggleToolbar', ShortcutCategory.MENUS, 'Toggle Toolbar',
|
||||||
|
(ctx: any) => {
|
||||||
|
ctx.toolbar = !ctx.toolbar
|
||||||
},
|
},
|
||||||
[Shortcut.SEEK_UP]: (ctx: any) => { if (ctx.vertical) ctx.prev() },
|
'm')
|
||||||
[Shortcut.SEEK_DOWN]: (ctx: any) => { if (ctx.vertical) ctx.next() },
|
|
||||||
[Shortcut.SEEK_BEGIN]: (ctx: any) => { ctx.goToFirst() },
|
shortcut('toggleMenu', ShortcutCategory.MENUS, 'Toggle Settings Menu',
|
||||||
[Shortcut.SEEK_END]: (ctx: any) => { ctx.goToLast() },
|
(ctx: any) => {
|
||||||
[Shortcut.TOGGLE_TOOLBAR]: (ctx: any) => { ctx.toolbar = !ctx.toolbar },
|
ctx.menu = !ctx.menu
|
||||||
[Shortcut.TOGGLE_MENU]: (ctx: any) => { ctx.menu = !ctx.menu },
|
},
|
||||||
[Shortcut.TOGGLE_THUMBNAIL_EXPLORER]: (ctx: any) => { ctx.showThumbnailsExplorer = !ctx.showThumbnailsExplorer },
|
's')
|
||||||
[Shortcut.TOGGLE_DOUBLE_PAGE]: (ctx: any) => ctx.toggleDoublePages(),
|
|
||||||
[Shortcut.CYCLE_SCALE]: (ctx: any) => ctx.cycleScale(),
|
shortcut('toggleExplorer', ShortcutCategory.MENUS, 'Toggle Explorer',
|
||||||
[Shortcut.DIR_LTR]: (ctx: any) => ctx.changeReadingDir(ReadingDirection.LEFT_TO_RIGHT),
|
(ctx: any) => {
|
||||||
[Shortcut.DIR_RTL]: (ctx: any) => ctx.changeReadingDir(ReadingDirection.RIGHT_TO_LEFT),
|
ctx.showThumbnailsExplorer = !ctx.showThumbnailsExplorer
|
||||||
[Shortcut.DIR_VRT]: (ctx: any) => ctx.changeReadingDir(ReadingDirection.VERTICAL),
|
}, 't')
|
||||||
[Shortcut.ESCAPE]: (ctx: any) => {
|
|
||||||
|
shortcut('escape', ShortcutCategory.MENUS, 'Close',
|
||||||
|
(ctx: any) => {
|
||||||
if (ctx.showThumbnailsExplorer) {
|
if (ctx.showThumbnailsExplorer) {
|
||||||
ctx.showThumbnailsExplorer = false
|
ctx.showThumbnailsExplorer = false
|
||||||
return
|
return
|
||||||
|
|
@ -86,20 +160,26 @@ const shortcuts = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.closeBook()
|
ctx.closeBook()
|
||||||
},
|
}, 'Escape')
|
||||||
} as Shortcuts
|
|
||||||
|
// Make sure all shortcuts are registered before this is called
|
||||||
|
export const shortcutHelp = new MultiMap<object>()
|
||||||
|
const keyMapping = {} as Map<Shortcut>
|
||||||
|
|
||||||
|
function setupShortcuts () {
|
||||||
|
for (const s of SHORTCUTS) {
|
||||||
|
for (const key of s.keys) {
|
||||||
|
keyMapping[key] = s
|
||||||
|
shortcutHelp.add(s.category, {
|
||||||
|
key: KEY_DISPLAY[key] || key,
|
||||||
|
desc: s.description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setupShortcuts()
|
||||||
|
|
||||||
export function executeShortcut (ctx: any, e: KeyboardEvent): boolean {
|
export function executeShortcut (ctx: any, e: KeyboardEvent): boolean {
|
||||||
let k: string = e.key
|
let k: string = e.key
|
||||||
if (k in keyMapping) {
|
return keyMapping[k]?.execute(ctx)
|
||||||
let s: Shortcut = keyMapping[k]
|
|
||||||
if (s in shortcuts) {
|
|
||||||
let action: Action = shortcuts[s]
|
|
||||||
if (action) {
|
|
||||||
action(ctx)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-toolbar-title> {{ bookTitle }}</v-toolbar-title>
|
<v-toolbar-title> {{ bookTitle }}</v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
<shortcut-help-menu/>
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
@click="showThumbnailsExplorer = !showThumbnailsExplorer"
|
@click="showThumbnailsExplorer = !showThumbnailsExplorer"
|
||||||
|
|
@ -270,6 +271,7 @@
|
||||||
import SettingsSelect from '@/components/SettingsSelect.vue'
|
import SettingsSelect from '@/components/SettingsSelect.vue'
|
||||||
import SettingsSwitch from '@/components/SettingsSwitch.vue'
|
import SettingsSwitch from '@/components/SettingsSwitch.vue'
|
||||||
import ThumbnailExplorerDialog from '@/components/dialogs/ThumbnailExplorerDialog.vue'
|
import ThumbnailExplorerDialog from '@/components/dialogs/ThumbnailExplorerDialog.vue'
|
||||||
|
import ShortcutHelpMenu from '@/components/menus/ShortcutHelpMenu.vue'
|
||||||
import { getBookTitleCompact } from '@/functions/book-title'
|
import { getBookTitleCompact } from '@/functions/book-title'
|
||||||
import { checkWebpFeature } from '@/functions/check-webp'
|
import { checkWebpFeature } from '@/functions/check-webp'
|
||||||
import { bookPageUrl } from '@/functions/urls'
|
import { bookPageUrl } from '@/functions/urls'
|
||||||
|
|
@ -292,7 +294,7 @@ enum ImageFit {
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'BookReader',
|
name: 'BookReader',
|
||||||
components: { SettingsSwitch, SettingsSelect, ThumbnailExplorerDialog },
|
components: { SettingsSwitch, SettingsSelect, ThumbnailExplorerDialog, ShortcutHelpMenu },
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
ImageFit,
|
ImageFit,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue