mirror of
https://github.com/ajurna/cbwebreader.git
synced 2026-11-02 18:46:53 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb47478511 |
No files matched your search
@@ -46,6 +46,29 @@
|
|||||||
@nextComic="nextComic"
|
@nextComic="nextComic"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
:class="{ active: show_shortcuts }"
|
||||||
|
:aria-pressed="show_shortcuts ? 'true' : 'false'"
|
||||||
|
@click="show_shortcuts = !show_shortcuts"
|
||||||
|
>?</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shortcuts" v-if="show_shortcuts" role="dialog" aria-label="Keyboard shortcuts">
|
||||||
|
<div class="shortcuts-head">
|
||||||
|
<strong>Keyboard shortcuts</strong>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" @click="show_shortcuts = false">Close</button>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="shortcut in shortcuts" :key="shortcut.keys">
|
||||||
|
<td><kbd>{{ shortcut.keys }}</kbd></td>
|
||||||
|
<td>{{ shortcut.action }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
@@ -84,6 +107,18 @@ export default {
|
|||||||
save_timeout: null,
|
save_timeout: null,
|
||||||
key_locked: false,
|
key_locked: false,
|
||||||
key_timeout: null,
|
key_timeout: null,
|
||||||
|
show_shortcuts: false,
|
||||||
|
shortcuts: [
|
||||||
|
{ keys: '← / →', action: 'Previous / next page' },
|
||||||
|
{ keys: '↑ / ↓', action: 'Scroll within the page' },
|
||||||
|
{ keys: 'Space', action: 'Next page' },
|
||||||
|
{ keys: 'Shift + Space', action: 'Previous page' },
|
||||||
|
{ keys: 'n / p', action: 'Next / previous page' },
|
||||||
|
{ keys: 'Home / End', action: 'First / last page' },
|
||||||
|
{ keys: 'm', action: 'Switch reading mode' },
|
||||||
|
{ keys: '?', action: 'Show or hide this list' },
|
||||||
|
{ keys: 'Esc', action: 'Hide this list' },
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -181,6 +216,10 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.deck) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.current_page = pageNum-1
|
this.current_page = pageNum-1
|
||||||
this.deck.slide(this.current_page)
|
this.deck.slide(this.current_page)
|
||||||
},
|
},
|
||||||
@@ -197,6 +236,17 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These only show or hide the shortcut list, so they still work when a control
|
||||||
|
// has focus.
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.show_shortcuts = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === '?') {
|
||||||
|
this.show_shortcuts = !this.show_shortcuts
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// The reader listens on window, so a key pressed while a control has focus would
|
// The reader listens on window, so a key pressed while a control has focus would
|
||||||
// otherwise page as well as activate that control.
|
// otherwise page as well as activate that control.
|
||||||
if (e.target && e.target.closest && e.target.closest('input, select, textarea, button')) {
|
if (e.target && e.target.closest && e.target.closest('input, select, textarea, button')) {
|
||||||
@@ -213,10 +263,34 @@ export default {
|
|||||||
// scrolling: the page is taller than the viewport, and intercepting them here would
|
// scrolling: the page is taller than the viewport, and intercepting them here would
|
||||||
// double up with the native scroll. In continuous mode the observer records whichever
|
// double up with the native scroll. In continuous mode the observer records whichever
|
||||||
// page is topmost, so scrolling inside a page does not change the saved page.
|
// page is topmost, so scrolling inside a page does not change the saved page.
|
||||||
if (e.key === 'ArrowRight') {
|
const key = e.key.toLowerCase()
|
||||||
this.nextPage('auto')
|
switch (key) {
|
||||||
} else if (e.key === 'ArrowLeft') {
|
case 'arrowright':
|
||||||
this.prevPage('auto')
|
case 'n':
|
||||||
|
this.nextPage('auto')
|
||||||
|
return
|
||||||
|
case 'arrowleft':
|
||||||
|
case 'p':
|
||||||
|
this.prevPage('auto')
|
||||||
|
return
|
||||||
|
case ' ':
|
||||||
|
// Space would otherwise scroll the page as well as turning it.
|
||||||
|
e.preventDefault?.()
|
||||||
|
if (e.shiftKey) {
|
||||||
|
this.prevPage('auto')
|
||||||
|
} else {
|
||||||
|
this.nextPage('auto')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case 'home':
|
||||||
|
case 'end':
|
||||||
|
// Same for Home/End, which the browser would scroll to the page edges.
|
||||||
|
e.preventDefault?.()
|
||||||
|
this.setPage(key === 'home' ? 1 : this.pages)
|
||||||
|
return
|
||||||
|
case 'm':
|
||||||
|
this.setMode(this.mode === 'paged' ? 'scroll' : 'paged')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Continuous mode: reveal is not used for visibility here, so the page in view is
|
// Continuous mode: reveal is not used for visibility here, so the page in view is
|
||||||
@@ -415,6 +489,36 @@ section {
|
|||||||
padding-bottom: 60px;
|
padding-bottom: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shortcuts {
|
||||||
|
position: fixed;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 64px;
|
||||||
|
max-width: 340px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #adb5bd;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts td {
|
||||||
|
padding: 2px 8px 2px 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts kbd {
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* reveal.css is deliberately not loaded, so visibility is the `hidden` attribute and
|
/* reveal.css is deliberately not loaded, so visibility is the `hidden` attribute and
|
||||||
reveal's own transition classes do nothing. A transition cannot run across
|
reveal's own transition classes do nothing. A transition cannot run across
|
||||||
display:none -> block, so paged mode uses an animation on the slide that becomes
|
display:none -> block, so paged mode uses an animation on the slide that becomes
|
||||||
|
|||||||
@@ -231,6 +231,98 @@ describe('TheComicReader', () => {
|
|||||||
expect(api.put).not.toHaveBeenCalled()
|
expect(api.put).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows the shortcut list with ? and hides it again', async () => {
|
||||||
|
const wrapper = mountReader()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.find('.shortcuts').exists()).toBe(false)
|
||||||
|
|
||||||
|
wrapper.vm.keyPress({ key: '?' })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('.shortcuts').exists()).toBe(true)
|
||||||
|
|
||||||
|
wrapper.vm.keyPress({ key: '?' })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('.shortcuts').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the shortcut list with Escape even when a control has focus', async () => {
|
||||||
|
const wrapper = mountReader()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
wrapper.vm.keyPress({ key: '?' })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('.shortcuts').exists()).toBe(true)
|
||||||
|
|
||||||
|
wrapper.vm.keyPress({ key: 'Escape', target: document.createElement('input') })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('.shortcuts').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles the shortcut list from the ? button', async () => {
|
||||||
|
const wrapper = mountReader()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const button = wrapper.find('button[aria-pressed]')
|
||||||
|
await button.trigger('click')
|
||||||
|
expect(wrapper.find('.shortcuts').exists()).toBe(true)
|
||||||
|
expect(button.attributes('aria-pressed')).toBe('true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('pages with Space and Shift+Space', async () => {
|
||||||
|
const wrapper = mountReader()
|
||||||
|
await flushPromises()
|
||||||
|
deck.slide.mockClear()
|
||||||
|
|
||||||
|
const preventDefault = vi.fn()
|
||||||
|
wrapper.vm.keyPress({ key: ' ', preventDefault })
|
||||||
|
expect(preventDefault).toHaveBeenCalled()
|
||||||
|
expect(deck.slide).toHaveBeenCalledWith(3)
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(100)
|
||||||
|
wrapper.vm.keyPress({ key: ' ', shiftKey: true, preventDefault })
|
||||||
|
expect(deck.slide).toHaveBeenCalledWith(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('pages with n and p whatever the case', async () => {
|
||||||
|
const wrapper = mountReader()
|
||||||
|
await flushPromises()
|
||||||
|
deck.slide.mockClear()
|
||||||
|
|
||||||
|
wrapper.vm.keyPress({ key: 'n' })
|
||||||
|
expect(deck.slide).toHaveBeenCalledWith(3)
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(100)
|
||||||
|
wrapper.vm.keyPress({ key: 'P' })
|
||||||
|
expect(deck.slide).toHaveBeenCalledWith(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('jumps to the first and last page with Home and End', async () => {
|
||||||
|
const wrapper = mountReader()
|
||||||
|
await flushPromises()
|
||||||
|
deck.slide.mockClear()
|
||||||
|
|
||||||
|
const preventDefault = vi.fn()
|
||||||
|
wrapper.vm.keyPress({ key: 'Home', preventDefault })
|
||||||
|
expect(preventDefault).toHaveBeenCalled()
|
||||||
|
expect(deck.slide).toHaveBeenCalledWith(0)
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(100)
|
||||||
|
wrapper.vm.keyPress({ key: 'End', preventDefault })
|
||||||
|
expect(deck.slide).toHaveBeenCalledWith(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches reading mode with m', async () => {
|
||||||
|
const wrapper = mountReader()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
wrapper.vm.keyPress({ key: 'm' })
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(store.commit).toHaveBeenCalledWith('setReaderMode', 'scroll')
|
||||||
|
expect(wrapper.vm.mode).toBe('scroll')
|
||||||
|
})
|
||||||
|
|
||||||
it('shows the top of the page when changing pages in paged mode', async () => {
|
it('shows the top of the page when changing pages in paged mode', async () => {
|
||||||
const wrapper = mountReader()
|
const wrapper = mountReader()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 5
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.15'",
|
"python_full_version >= '3.15'",
|
||||||
@@ -1472,14 +1472,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "werkzeug"
|
name = "werkzeug"
|
||||||
version = "3.1.9"
|
version = "3.1.8"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "markupsafe" },
|
{ name = "markupsafe" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/34/4dd12fc8bb7d61c91467ec3efe415ffa7d5456f799954b40c5bbaeae470e/werkzeug-3.1.9.tar.gz", hash = "sha256:55ca7c70a75689be937aa27f8ff4b018f06ff4838fc73045560bf0f5a1291060", size = 940188, upload-time = "2026-09-27T18:33:41.637Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/38/df03f564f43cec2684823f3cccae1a652ee7face1cbaa76fb223096e64d7/werkzeug-3.1.9-py3-none-any.whl", hash = "sha256:6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab", size = 228700, upload-time = "2026-09-27T18:33:39.685Z" },
|
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in new issue
Block a user