Compare commits

..
Author SHA1 Message Date
dependabot[bot] be60657933 Bump pylint from 4.0.5 to 4.0.9
Bumps [pylint](https://github.com/pylint-dev/pylint) from 4.0.5 to 4.0.9.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v4.0.5...v4.0.9)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 4.0.9
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-30 20:17:53 +00:00
ajurna bb47478511 Add keyboard shortcut help and improved navigation controls
Build and push image / deploy (push) Canceled after 0s
- Introduced a modal to display keyboard shortcuts with a toggle button (`?`) in the interface.
- Added support for toggling shortcuts list visibility via `?` and `Escape` keys.
- Enabled navigation with additional shortcuts: `n/p`, `Home/End`, `m`, and `Space/Shift+Space` for improved usability.
- Modified keypress handling to accommodate new shortcuts while preventing default browser scroll actions where necessary.
- Updated styles to ensure proper positioning and accessibility of the shortcut list.
2026-10-16 09:41:45 +01:00
3 changed files with 207 additions and 11 deletions

No files matched your search

+108 -4
View File
@@ -46,6 +46,29 @@
@nextComic="nextComic"
/>
</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>
</template>
@@ -84,6 +107,18 @@ export default {
save_timeout: null,
key_locked: false,
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: {
@@ -181,6 +216,10 @@ export default {
return
}
if (!this.deck) {
return
}
this.current_page = pageNum-1
this.deck.slide(this.current_page)
},
@@ -197,6 +236,17 @@ export default {
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
// otherwise page as well as activate that control.
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
// 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.
if (e.key === 'ArrowRight') {
this.nextPage('auto')
} else if (e.key === 'ArrowLeft') {
this.prevPage('auto')
const key = e.key.toLowerCase()
switch (key) {
case 'arrowright':
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
@@ -415,6 +489,36 @@ section {
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'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
+92
View File
@@ -231,6 +231,98 @@ describe('TheComicReader', () => {
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 () => {
const wrapper = mountReader()
await flushPromises()
Generated
+7 -7
View File
@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 5
requires-python = ">=3.14"
resolution-markers = [
"python_full_version >= '3.15'",
@@ -1068,16 +1068,16 @@ wheels = [
[[package]]
name = "pyjwt"
version = "2.15.0"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/a5/5197bfd06417837ac079921c66fa6393f1dea3557272a263cebfef69e432/pyjwt-2.15.0.tar.gz", hash = "sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4", size = 120513, upload-time = "2026-09-23T16:56:00.689Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/55/40e45bf052ee8ee12a4dfd785519660f8effa7b065442b91646ec6828619/pyjwt-2.15.0-py3-none-any.whl", hash = "sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8", size = 33680, upload-time = "2026-09-23T16:55:59.241Z" },
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
]
[[package]]
name = "pylint"
version = "4.0.5"
version = "4.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "astroid" },
@@ -1088,9 +1088,9 @@ dependencies = [
{ name = "platformdirs" },
{ name = "tomlkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/5e/d5ec2c861010cf8577a3e5baaf2405fdf1004f1657d9245c4efb006c1dfd/pylint-4.0.9.tar.gz", hash = "sha256:6f1305780229ae720e89b8aff2b3ff857cbb268fbab98c8b586960267667ad34", size = 1604187, upload-time = "2026-09-23T15:26:43.144Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" },
{ url = "https://files.pythonhosted.org/packages/66/46/12e94ae1f9db89152223bdb17920b12c79e94ac51e19dce1da9fb63627b0/pylint-4.0.9-py3-none-any.whl", hash = "sha256:c4ba5bbaf1e70ecf05f08cd7d6091ea04824b79a2b2d9f5b10dc25fc34cb1124", size = 542068, upload-time = "2026-09-23T15:26:41.626Z" },
]
[[package]]