Skip to content

Commit

Permalink
feat(webreader): add shortcut information menu
Browse files Browse the repository at this point in the history
  • Loading branch information
primetoxinz authored Jul 25, 2020
1 parent 40c1ca5 commit 1885f32
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 76 deletions.
65 changes: 65 additions & 0 deletions komga-webui/src/components/menus/ShortcutHelpMenu.vue
Original file line number Diff line number Diff line change
@@ -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>
230 changes: 155 additions & 75 deletions komga-webui/src/functions/shortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,152 @@
import { ReadingDirection } from '@/types/enum-books'

enum Shortcut {
// Navigation
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'
interface Map<V> {
[key: string]: V
}
class MultiMap<V> {
dict: Map<V[]> = {}

interface KeyMapping {
[key: string]: Shortcut
add (key:string, value: V) {
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

interface Shortcuts {
[key: string]: Action
class Shortcut {
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
}
}

const KEY_DISPLAY = {
'ArrowRight': 'mdi-arrow-right',
'ArrowLeft': 'mdi-arrow-left',
'PageUp': 'PgUp',
'PageDown': 'PgDn',
'ArrowUp': 'mdi-arrow-up',
'ArrowDown': 'mdi-arrow-down',
'Escape': 'Esc',
} as Map<string>

const SHORTCUTS: Shortcut[] = []

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'
}

// consider making this configurable on the server side?
const keyMapping = {
'PageUp': Shortcut.SEEK_FORWARD,
'ArrowRight': Shortcut.SEEK_FORWARD,
'PageDown': Shortcut.SEEK_BACKWARD,
'ArrowLeft': Shortcut.SEEK_BACKWARD,
'ArrowDown': Shortcut.SEEK_DOWN,
'ArrowUp': Shortcut.SEEK_UP,
'Home': Shortcut.SEEK_BEGIN,
'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 = {
[Shortcut.SEEK_FORWARD]: (ctx: any) => {
// Reader Navigation
shortcut('seekForward', ShortcutCategory.READER_NAVIGATION, 'Next Page',
(ctx: any) => {
ctx.flipDirection ? ctx.prev() : ctx.next()
},
[Shortcut.SEEK_BACKWARD]: (ctx: any) => {
}, 'PageUp', 'ArrowRight')

shortcut('seekBackward', ShortcutCategory.READER_NAVIGATION, 'Prev Page',
(ctx: any) => {
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() },
[Shortcut.SEEK_DOWN]: (ctx: any) => { if (ctx.vertical) ctx.next() },
[Shortcut.SEEK_BEGIN]: (ctx: any) => { ctx.goToFirst() },
[Shortcut.SEEK_END]: (ctx: any) => { ctx.goToLast() },
[Shortcut.TOGGLE_TOOLBAR]: (ctx: any) => { ctx.toolbar = !ctx.toolbar },
[Shortcut.TOGGLE_MENU]: (ctx: any) => { ctx.menu = !ctx.menu },
[Shortcut.TOGGLE_THUMBNAIL_EXPLORER]: (ctx: any) => { ctx.showThumbnailsExplorer = !ctx.showThumbnailsExplorer },
[Shortcut.TOGGLE_DOUBLE_PAGE]: (ctx: any) => ctx.toggleDoublePages(),
[Shortcut.CYCLE_SCALE]: (ctx: any) => ctx.cycleScale(),
[Shortcut.DIR_LTR]: (ctx: any) => ctx.changeReadingDir(ReadingDirection.LEFT_TO_RIGHT),
[Shortcut.DIR_RTL]: (ctx: any) => ctx.changeReadingDir(ReadingDirection.RIGHT_TO_LEFT),
[Shortcut.DIR_VRT]: (ctx: any) => ctx.changeReadingDir(ReadingDirection.VERTICAL),
[Shortcut.ESCAPE]: (ctx: any) => {
'm')

shortcut('toggleMenu', ShortcutCategory.MENUS, 'Toggle Settings Menu',
(ctx: any) => {
ctx.menu = !ctx.menu
},
's')

shortcut('toggleExplorer', ShortcutCategory.MENUS, 'Toggle Explorer',
(ctx: any) => {
ctx.showThumbnailsExplorer = !ctx.showThumbnailsExplorer
}, 't')

shortcut('escape', ShortcutCategory.MENUS, 'Close',
(ctx: any) => {
if (ctx.showThumbnailsExplorer) {
ctx.showThumbnailsExplorer = false
return
Expand All @@ -86,20 +160,26 @@ const shortcuts = {
return
}
ctx.closeBook()
},
} as Shortcuts
}, 'Escape')

export function executeShortcut (ctx: any, e: KeyboardEvent): boolean {
let k: string = e.key
if (k in keyMapping) {
let s: Shortcut = keyMapping[k]
if (s in shortcuts) {
let action: Action = shortcuts[s]
if (action) {
action(ctx)
return true
}
// 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,
})
}
}
return false
}
setupShortcuts()

export function executeShortcut (ctx: any, e: KeyboardEvent): boolean {
let k: string = e.key
return keyMapping[k]?.execute(ctx)
}
4 changes: 3 additions & 1 deletion komga-webui/src/views/BookReader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
</v-btn>
<v-toolbar-title> {{ bookTitle }}</v-toolbar-title>
<v-spacer></v-spacer>
<shortcut-help-menu/>
<v-btn
icon
@click="showThumbnailsExplorer = !showThumbnailsExplorer"
Expand Down Expand Up @@ -270,6 +271,7 @@
import SettingsSelect from '@/components/SettingsSelect.vue'
import SettingsSwitch from '@/components/SettingsSwitch.vue'
import ThumbnailExplorerDialog from '@/components/dialogs/ThumbnailExplorerDialog.vue'
import ShortcutHelpMenu from '@/components/menus/ShortcutHelpMenu.vue'
import { getBookTitleCompact } from '@/functions/book-title'
import { checkWebpFeature } from '@/functions/check-webp'
import { bookPageUrl } from '@/functions/urls'
Expand All @@ -292,7 +294,7 @@ enum ImageFit {
export default Vue.extend({
name: 'BookReader',
components: { SettingsSwitch, SettingsSelect, ThumbnailExplorerDialog },
components: { SettingsSwitch, SettingsSelect, ThumbnailExplorerDialog, ShortcutHelpMenu },
data: () => {
return {
ImageFit,
Expand Down

0 comments on commit 1885f32

Please sign in to comment.