release: v0.13.0 (#535)
Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list
and the upgrade notes.
Ten additions — the SEO package (site URL, robots.txt, sitemap.xml,
JSON-LD), slideshow mode, opt-in original downloads, copy-link,
previous/next album navigation, alt text from the Immich description,
per-album location precision, the update notice, a Help tab, and a
subpage cover grid separate from its photo grid. Two fixes: CJK album
slugs, and the lightbox control bar.
Photo of the Day (#476) was built on `dev` and reverted as not planned;
it never appears in a release.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:08:12 +02:00
|
|
|
/**
|
|
|
|
|
* The lightbox's keyboard shortcuts, in one place (#473).
|
|
|
|
|
*
|
|
|
|
|
* Three things read this list: the `?` overlay the visitor sees, the help
|
|
|
|
|
* section in the admin panel where the site owner finds out these keys exist
|
|
|
|
|
* at all, and — since this commit — the viewer's own key handler.
|
|
|
|
|
*
|
|
|
|
|
* That last one is the point. The catalogue used to describe the keys while
|
|
|
|
|
* the handler independently decided which ones it listened for, so a key added
|
|
|
|
|
* to the viewer and forgotten here stayed exactly as undiscoverable as the
|
|
|
|
|
* whole set was before, and nothing would have said so. Now a key reaches the
|
|
|
|
|
* handler only by being declared here, and `LightboxAction` is a closed union
|
|
|
|
|
* the handler switches over exhaustively: declaring an action the viewer does
|
|
|
|
|
* not implement is a type error, not a silent gap.
|
|
|
|
|
*
|
|
|
|
|
* The labels stay out of here: they live in the dictionaries, and the two
|
|
|
|
|
* dynamic ones (fullscreen flips, the slideshow names its speed) are resolved
|
|
|
|
|
* by the viewer, which is the only place that knows the current state.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** What has to be true for a shortcut to do anything. */
|
|
|
|
|
export type ShortcutAvailability =
|
|
|
|
|
/** Always present. */
|
|
|
|
|
| 'always'
|
|
|
|
|
/** Journal entries hide the EXIF toggle, so `i` does nothing there. */
|
|
|
|
|
| 'exifPanel'
|
|
|
|
|
/** Absent where the browser has no element fullscreen to give (iPhone). */
|
|
|
|
|
| 'fullscreen'
|
|
|
|
|
/** Only where the album offers its originals. */
|
2026-10-02 18:14:23 +02:00
|
|
|
| 'download'
|
|
|
|
|
/** Only where the photo on screen can be zoomed (#467). */
|
|
|
|
|
| 'zoom';
|
release: v0.13.0 (#535)
Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list
and the upgrade notes.
Ten additions — the SEO package (site URL, robots.txt, sitemap.xml,
JSON-LD), slideshow mode, opt-in original downloads, copy-link,
previous/next album navigation, alt text from the Immich description,
per-album location precision, the update notice, a Help tab, and a
subpage cover grid separate from its photo grid. Two fixes: CJK album
slugs, and the lightbox control bar.
Photo of the Day (#476) was built on `dev` and reverted as not planned;
it never appears in a release.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:08:12 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* What a key does. Closed on purpose: the viewer switches over this union
|
|
|
|
|
* exhaustively, so adding a member without handling it fails to compile.
|
|
|
|
|
*/
|
|
|
|
|
export type LightboxAction =
|
|
|
|
|
| 'close'
|
|
|
|
|
| 'prev'
|
|
|
|
|
| 'next'
|
|
|
|
|
| 'info'
|
|
|
|
|
| 'fullscreen'
|
|
|
|
|
| 'slideshow'
|
|
|
|
|
| 'copyLink'
|
|
|
|
|
| 'download'
|
2026-10-02 18:14:23 +02:00
|
|
|
| 'zoomIn'
|
|
|
|
|
| 'zoomOut'
|
|
|
|
|
| 'zoomReset'
|
release: v0.13.0 (#535)
Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list
and the upgrade notes.
Ten additions — the SEO package (site URL, robots.txt, sitemap.xml,
JSON-LD), slideshow mode, opt-in original downloads, copy-link,
previous/next album navigation, alt text from the Immich description,
per-album location precision, the update notice, a Help tab, and a
subpage cover grid separate from its photo grid. Two fixes: CJK album
slugs, and the lightbox control bar.
Photo of the Day (#476) was built on `dev` and reverted as not planned;
it never appears in a release.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:08:12 +02:00
|
|
|
| 'shortcutList';
|
|
|
|
|
|
|
|
|
|
export interface ShortcutBinding {
|
|
|
|
|
/**
|
|
|
|
|
* How the key is written in the two lists. Arrows are stored as names rather
|
|
|
|
|
* than glyphs so the list survives an editor that mangles them; both
|
|
|
|
|
* renderers ask for the display form via `shortcutKeyLabel()`.
|
|
|
|
|
*/
|
|
|
|
|
display: string;
|
|
|
|
|
/**
|
|
|
|
|
* The exact `KeyboardEvent.key` values that trigger it, case variants
|
|
|
|
|
* included. `?` is matched on the produced character, not the physical key:
|
|
|
|
|
* Shift+/ on a US layout, Shift+ß on a German one.
|
|
|
|
|
*/
|
|
|
|
|
eventKeys: readonly string[];
|
|
|
|
|
action: LightboxAction;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface LightboxShortcut {
|
|
|
|
|
bindings: readonly ShortcutBinding[];
|
|
|
|
|
/** Key into the `lightbox` dictionary section. */
|
|
|
|
|
labelKey:
|
|
|
|
|
| 'shortcutNavigate'
|
|
|
|
|
| 'shortcutInfo'
|
|
|
|
|
| 'shortcutFullscreen'
|
|
|
|
|
| 'shortcutSlideshow'
|
|
|
|
|
| 'shortcutCopyLink'
|
|
|
|
|
| 'shortcutDownload'
|
2026-10-02 18:14:23 +02:00
|
|
|
| 'shortcutZoom'
|
|
|
|
|
| 'shortcutZoomReset'
|
release: v0.13.0 (#535)
Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list
and the upgrade notes.
Ten additions — the SEO package (site URL, robots.txt, sitemap.xml,
JSON-LD), slideshow mode, opt-in original downloads, copy-link,
previous/next album navigation, alt text from the Immich description,
per-album location precision, the update notice, a Help tab, and a
subpage cover grid separate from its photo grid. Two fixes: CJK album
slugs, and the lightbox control bar.
Photo of the Day (#476) was built on `dev` and reverted as not planned;
it never appears in a release.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:08:12 +02:00
|
|
|
| 'shortcutList'
|
|
|
|
|
| 'shortcutClose';
|
|
|
|
|
availability: ShortcutAvailability;
|
|
|
|
|
/** Shown in the admin help to explain when the key is absent. */
|
|
|
|
|
note?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** In the order they are worth learning. */
|
|
|
|
|
export const LIGHTBOX_SHORTCUTS: readonly LightboxShortcut[] = [
|
|
|
|
|
{
|
|
|
|
|
// One row, two actions: the arrows are learnt together but do opposite
|
|
|
|
|
// things, which is why a binding carries the action rather than the row.
|
|
|
|
|
bindings: [
|
|
|
|
|
{ display: 'ARROW_LEFT', eventKeys: ['ArrowLeft'], action: 'prev' },
|
|
|
|
|
{ display: 'ARROW_RIGHT', eventKeys: ['ArrowRight'], action: 'next' },
|
|
|
|
|
],
|
|
|
|
|
labelKey: 'shortcutNavigate',
|
|
|
|
|
availability: 'always',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
bindings: [{ display: 'I', eventKeys: ['i', 'I'], action: 'info' }],
|
|
|
|
|
labelKey: 'shortcutInfo',
|
|
|
|
|
availability: 'exifPanel',
|
|
|
|
|
note: 'Hidden where every EXIF group is switched off, and in journal entries.',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
bindings: [{ display: 'F', eventKeys: ['f', 'F'], action: 'fullscreen' }],
|
|
|
|
|
labelKey: 'shortcutFullscreen',
|
|
|
|
|
availability: 'fullscreen',
|
|
|
|
|
note: 'Absent on browsers with no element fullscreen, such as Safari on iPhone.',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
bindings: [{ display: 'S', eventKeys: ['s', 'S'], action: 'slideshow' }],
|
|
|
|
|
labelKey: 'shortcutSlideshow',
|
|
|
|
|
availability: 'always',
|
|
|
|
|
note: 'Cycles off, 3s, 5s, 10s and back to off. Any arrow or swipe stops it.',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
bindings: [{ display: 'C', eventKeys: ['c', 'C'], action: 'copyLink' }],
|
|
|
|
|
labelKey: 'shortcutCopyLink',
|
|
|
|
|
availability: 'always',
|
|
|
|
|
note: 'Copies a link to the photo on screen. Positional: reordering the album moves where it lands.',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
bindings: [{ display: 'D', eventKeys: ['d', 'D'], action: 'download' }],
|
|
|
|
|
labelKey: 'shortcutDownload',
|
|
|
|
|
availability: 'download',
|
|
|
|
|
note: 'Only for albums with download: true in gallery.yaml.',
|
|
|
|
|
},
|
2026-10-02 18:14:23 +02:00
|
|
|
{
|
|
|
|
|
// `=` is `+` without Shift on US and many other layouts; the numpad sends
|
|
|
|
|
// `+` and `-` directly. Ctrl/Cmd with these stays the browser's own zoom:
|
|
|
|
|
// the viewer only listens to bare keys.
|
|
|
|
|
bindings: [
|
|
|
|
|
{ display: '+', eventKeys: ['+', '='], action: 'zoomIn' },
|
|
|
|
|
{ display: '-', eventKeys: ['-'], action: 'zoomOut' },
|
|
|
|
|
],
|
|
|
|
|
labelKey: 'shortcutZoom',
|
|
|
|
|
availability: 'zoom',
|
|
|
|
|
note: 'Only where zoom is on (settings, page or album) and the photo has a full-resolution source.',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
bindings: [{ display: '0', eventKeys: ['0'], action: 'zoomReset' }],
|
|
|
|
|
labelKey: 'shortcutZoomReset',
|
|
|
|
|
availability: 'zoom',
|
|
|
|
|
note: 'Back to the whole photo. Esc does the same before it closes the viewer.',
|
|
|
|
|
},
|
release: v0.13.0 (#535)
Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list
and the upgrade notes.
Ten additions — the SEO package (site URL, robots.txt, sitemap.xml,
JSON-LD), slideshow mode, opt-in original downloads, copy-link,
previous/next album navigation, alt text from the Immich description,
per-album location precision, the update notice, a Help tab, and a
subpage cover grid separate from its photo grid. Two fixes: CJK album
slugs, and the lightbox control bar.
Photo of the Day (#476) was built on `dev` and reverted as not planned;
it never appears in a release.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:08:12 +02:00
|
|
|
{
|
|
|
|
|
// `h` is the escape hatch for layouts where `?` is awkward — and the one
|
|
|
|
|
// key someone guesses without having been told.
|
|
|
|
|
bindings: [
|
|
|
|
|
{ display: '?', eventKeys: ['?'], action: 'shortcutList' },
|
|
|
|
|
{ display: 'H', eventKeys: ['h', 'H'], action: 'shortcutList' },
|
|
|
|
|
],
|
|
|
|
|
labelKey: 'shortcutList',
|
|
|
|
|
availability: 'always',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
bindings: [{ display: 'Esc', eventKeys: ['Escape'], action: 'close' }],
|
|
|
|
|
labelKey: 'shortcutClose',
|
|
|
|
|
availability: 'always',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Arrow names to glyphs. Ordinary keys pass through, so both renderers can map
|
|
|
|
|
* every display key through this without asking which kind it is.
|
|
|
|
|
*/
|
|
|
|
|
export function shortcutKeyLabel(key: string): string {
|
|
|
|
|
if (key === 'ARROW_LEFT') return String.fromCharCode(0x2190);
|
|
|
|
|
if (key === 'ARROW_RIGHT') return String.fromCharCode(0x2192);
|
|
|
|
|
return key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** The keys of one shortcut, ready to render. */
|
|
|
|
|
export function shortcutDisplayKeys(shortcut: LightboxShortcut): string[] {
|
|
|
|
|
return shortcut.bindings.map((binding) => shortcutKeyLabel(binding.display));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ACTION_BY_EVENT_KEY: ReadonlyMap<string, LightboxAction> = new Map(
|
|
|
|
|
LIGHTBOX_SHORTCUTS.flatMap((shortcut) =>
|
|
|
|
|
shortcut.bindings.flatMap((binding) =>
|
|
|
|
|
binding.eventKeys.map((key) => [key, binding.action] as const),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* What a `KeyboardEvent.key` should do, or null if the viewer does not bind it.
|
|
|
|
|
*
|
|
|
|
|
* This is the only route from a keypress to an action, so a key the catalogue
|
|
|
|
|
* does not list cannot reach the viewer, and one it does list cannot be
|
|
|
|
|
* silently missing from the help.
|
|
|
|
|
*/
|
|
|
|
|
export function lightboxActionFor(eventKey: string): LightboxAction | null {
|
|
|
|
|
return ACTION_BY_EVENT_KEY.get(eventKey) ?? null;
|
|
|
|
|
}
|