From b2909a11863cc46de250d13a0c5180dc49b997c6 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Thu, 21 May 2026 19:15:46 +0300 Subject: [PATCH] feat(workspace): add keyboard shortcuts help (#957) * feat(workspace): add keyboard shortcuts help * style(workspace): polish keyboard shortcuts dialog * fix(workspace): address shortcuts review feedback * docs(workspace): clarify desktop shortcut availability * refactor(workspace): split keyboard shortcut definitions --- README.md | 21 ++ apps/web-e2e/src/basic.e2e.ts | 18 + apps/web/src/assets/i18n/ar.json | 35 ++ apps/web/src/assets/i18n/ary.json | 35 ++ apps/web/src/assets/i18n/by.json | 35 ++ apps/web/src/assets/i18n/de.json | 35 ++ apps/web/src/assets/i18n/el.json | 35 ++ apps/web/src/assets/i18n/en.json | 35 ++ apps/web/src/assets/i18n/es.json | 35 ++ apps/web/src/assets/i18n/fr.json | 35 ++ apps/web/src/assets/i18n/it.json | 35 ++ apps/web/src/assets/i18n/ja.json | 35 ++ apps/web/src/assets/i18n/ko.json | 35 ++ apps/web/src/assets/i18n/nl.json | 35 ++ apps/web/src/assets/i18n/pl.json | 35 ++ apps/web/src/assets/i18n/pt.json | 35 ++ apps/web/src/assets/i18n/ru.json | 35 ++ apps/web/src/assets/i18n/tr.json | 35 ++ apps/web/src/assets/i18n/zh.json | 35 ++ apps/web/src/assets/i18n/zhtw.json | 35 ++ apps/web/src/styles.scss | 14 + docs/architecture/workspace-shell.md | 22 +- libs/portal/shared/util/src/index.ts | 1 + .../src/lib/keyboard-shortcut-definitions.ts | 202 +++++++++++ .../util/src/lib/keyboard-shortcuts.spec.ts | 95 +++++ .../shared/util/src/lib/keyboard-shortcuts.ts | 126 +++++++ ...e-keyboard-shortcuts-dialog.component.html | 105 ++++++ ...e-keyboard-shortcuts-dialog.component.scss | 342 ++++++++++++++++++ ...eyboard-shortcuts-dialog.component.spec.ts | 138 +++++++ ...ace-keyboard-shortcuts-dialog.component.ts | 36 ++ ...rkspace-keyboard-shortcuts.service.spec.ts | 96 +++++ .../workspace-keyboard-shortcuts.service.ts | 99 +++++ .../workspace-shell-header.component.html | 9 + .../workspace-shell-header.component.spec.ts | 12 + .../workspace-shell-header.component.ts | 5 + .../workspace-shell.component.html | 1 + .../workspace-shell.component.spec.ts | 62 ++++ .../workspace-shell.component.ts | 3 + 38 files changed, 2036 insertions(+), 1 deletion(-) create mode 100644 libs/portal/shared/util/src/lib/keyboard-shortcut-definitions.ts create mode 100644 libs/portal/shared/util/src/lib/keyboard-shortcuts.spec.ts create mode 100644 libs/portal/shared/util/src/lib/keyboard-shortcuts.ts create mode 100644 libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.html create mode 100644 libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.scss create mode 100644 libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.spec.ts create mode 100644 libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.ts create mode 100644 libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service.spec.ts create mode 100644 libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service.ts diff --git a/README.md b/README.md index 85497b279..d56cbbc45 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,27 @@ The application is a cross-platform, open-source project built with Electron and - Light and Dark themes - Docker image available for self-hosting the PWA and web backend together +## Keyboard shortcuts + +Press `?` or `Shift+/` in the workspace to open the in-app shortcuts list. + +| Area | Shortcut | Action | +| --- | --- | --- | +| Global | `Ctrl/Cmd+K` | Open command palette | +| Global | `Ctrl/Cmd+F` | Open global search in the desktop app | +| Global | `Ctrl/Cmd+R` | Open recently viewed in the desktop app | +| Global | `Enter` in workspace search | Submit the current search | +| Navigation | `Ctrl/Cmd+B` | Toggle the live sidebar | +| Navigation | `0-9` | Select an M3U channel by number | +| Playback | `Space` / `K` | Play or pause embedded MPV playback in the desktop app | +| Playback | `F` | Toggle embedded MPV fullscreen in the desktop app | +| Playback | `ArrowLeft` / `ArrowRight` | Seek embedded MPV playback by 5 seconds in the desktop app | +| Playback | `ArrowUp` / `ArrowDown` | Adjust volume by 5% | +| Playback | `M` | Mute audio | +| Dialogs and lists | `ArrowUp` / `ArrowDown` | Move command palette selection | +| Dialogs and lists | `Enter` | Run the selected command or open a focused item | +| Dialogs and lists | `Escape` | Close dialogs and dismiss overlays | + ## Screenshots: | Dashboard with recently watched content | Live channels with inline player and EPG | diff --git a/apps/web-e2e/src/basic.e2e.ts b/apps/web-e2e/src/basic.e2e.ts index 6a6262ac8..3f76af819 100644 --- a/apps/web-e2e/src/basic.e2e.ts +++ b/apps/web-e2e/src/basic.e2e.ts @@ -29,3 +29,21 @@ test('basic test', async ({ page }) => { await expect(page.getByText('1. Channel 1')).toBeVisible(); await expect(page.getByText('4. HappyKids TV')).toBeVisible(); }); + +test('keyboard shortcuts help opens from question mark', async ({ page }) => { + await page.goto('/'); + + await page.getByRole('button', { name: 'Open keyboard shortcuts' }).focus(); + await page.keyboard.press('?'); + + const dialog = page.getByRole('dialog'); + await expect( + dialog.getByRole('heading', { name: 'Keyboard shortcuts' }) + ).toBeVisible(); + await expect(dialog.getByText('Open command palette')).toBeVisible(); + await expect(dialog.getByText('Toggle sidebar')).toBeVisible(); + await expect(dialog.getByText('Mute audio')).toBeVisible(); + + await page.keyboard.press('Escape'); + await expect(dialog).toBeHidden(); +}); diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 8a658b05c..593509aa2 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "هذا القسم", "GROUP_RECENT": "المستخدمة مؤخرًا" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "المفضلة العامة", "SUBTITLE": "البث المباشر من جميع قوائم التشغيل", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index 3faa6a9ca..3b4ec08d3 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "هاد القسم", "GROUP_RECENT": "مستعملة مؤخراً" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "المفضلة العامة", "SUBTITLE": "البث المباشر من جميع قوائم التشغيل", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index a5d4b725e..1ecdeeb78 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Гэты раздзел", "GROUP_RECENT": "Нядаўна выкарыстаныя" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Усе фаварыты", "SUBTITLE": "Прамы эфір з усіх плэйлістоў", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index d6f72b073..8388a0860 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Dieser Abschnitt", "GROUP_RECENT": "Kürzlich verwendet" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Globale Favoriten", "SUBTITLE": "Live-TV aus allen Playlists", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 4e0769554..1caaf6877 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Αυτή η ενότητα", "GROUP_RECENT": "Πρόσφατα χρησιμοποιημένες" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Βασικά Αγαπημένα", "SUBTITLE": "Ζωντανή TV από όλες τις λίστες αναπαραγωγής", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index ee8a4a470..c53c8fd73 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "This section", "GROUP_RECENT": "Recently used" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Global Favorites", "SUBTITLE": "Live TV from all playlists", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 29b095996..7cf593332 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Esta sección", "GROUP_RECENT": "Usados recientemente" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Favoritos globales", "SUBTITLE": "TV en vivo de todas las listas", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 42d3ad995..25b16a654 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Cette section", "GROUP_RECENT": "Récemment utilisées" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Favoris globaux", "SUBTITLE": "TV en direct de toutes les listes de lecture", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 3d9b4fc9d..14c46c88b 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Questa sezione", "GROUP_RECENT": "Usati di recente" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Preferiti globali", "SUBTITLE": "TV in diretta da tutte le playlist", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index d8c77a915..d35388e32 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "このセクション", "GROUP_RECENT": "最近使用" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "グローバルお気に入り", "SUBTITLE": "すべてのプレイリストのライブTV", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 635855e55..f3d3fa86a 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "이 섹션", "GROUP_RECENT": "최근 사용" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "전체 즐겨찾기", "SUBTITLE": "모든 재생목록의 라이브 TV", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 40c46f5d4..8fbce2a4b 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Deze sectie", "GROUP_RECENT": "Recent gebruikt" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Algemene favorieten", "SUBTITLE": "Live tv uit alle afspeellijsten", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 1726294b4..bb45aa790 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Ta sekcja", "GROUP_RECENT": "Ostatnio używane" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Globalne ulubione", "SUBTITLE": "Telewizja na żywo ze wszystkich list odtwarzania", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 79d31dbc2..a5961a753 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Esta seção", "GROUP_RECENT": "Usados recentemente" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Favoritos globais", "SUBTITLE": "TV ao vivo de todas as playlists", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 80dcdaf6c..5009b61cf 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Этот раздел", "GROUP_RECENT": "Недавно использованные" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Глобальные избранные", "SUBTITLE": "Live TV из всех плейлистов", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 6911a1c88..d86ed8793 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "Bu bölüm", "GROUP_RECENT": "Son kullanılanlar" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "Tüm Favoriler", "SUBTITLE": "Tüm oynatma listelerinden canlı TV", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 7016ca2eb..33b6b5abe 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "本章节", "GROUP_RECENT": "最近使用" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "全局收藏", "SUBTITLE": "来自所有播放列表的直播电视", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 4f362b502..8da2bf49d 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -1019,6 +1019,41 @@ "GROUP_SECTION": "此區段", "GROUP_RECENT": "最近使用" }, + "SHORTCUTS": { + "TITLE": "Keyboard shortcuts", + "SUBTITLE": "Workspace, navigation, playback, and dialog shortcuts available in IPTVnator.", + "OPEN_TOOLTIP": "Keyboard shortcuts (?)", + "OPEN_ARIA": "Open keyboard shortcuts", + "PLATFORM": { + "MAC": "macOS layout", + "OTHER": "Windows/Linux layout" + }, + "GROUPS": { + "GLOBAL": "Global", + "NAVIGATION": "Navigation", + "PLAYBACK": "Playback", + "DIALOGS": "Dialogs and lists" + }, + "ITEMS": { + "OPEN_KEYBOARD_SHORTCUTS": "Open this shortcuts list", + "OPEN_COMMAND_PALETTE": "Open command palette", + "OPEN_GLOBAL_SEARCH": "Open global search", + "OPEN_RECENTLY_VIEWED": "Open recently viewed", + "SUBMIT_SHELL_SEARCH": "Submit workspace search", + "TOGGLE_SIDEBAR": "Toggle sidebar", + "M3U_CHANNEL_NUMBER": "Select M3U channel by number", + "PLAY_PAUSE": "Play or pause embedded MPV playback", + "TOGGLE_FULLSCREEN": "Toggle embedded MPV fullscreen", + "SEEK": "Seek embedded MPV playback by 5 seconds", + "ADJUST_VOLUME": "Adjust volume by 5%", + "MUTE_AUDIO": "Mute audio", + "CLOSE_PLAYER_POPOVERS": "Close embedded MPV popovers", + "COMMAND_PALETTE_NAVIGATION": "Move command palette selection", + "COMMAND_PALETTE_RUN": "Run selected command palette item", + "CLOSE_DIALOGS": "Close dialog or dismiss overlay", + "DOWNLOADS_OPEN_ITEM": "Open focused download item in the library" + } + }, "GLOBAL_FAVORITES": { "TITLE": "全域我的最愛", "SUBTITLE": "來自所有播放清單的直播電視", diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index f1ec3dcfb..b8d185552 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -200,6 +200,20 @@ textarea, overflow: visible; } +.workspace-shortcuts-overlay, +.workspace-shortcuts-overlay .mat-mdc-dialog-surface, +.workspace-shortcuts-overlay .mat-mdc-dialog-container { + app-region: no-drag; +} + +.workspace-shortcuts-overlay .mat-mdc-dialog-surface { + background: transparent; + box-shadow: none; + border-radius: 14px; + padding: 0; + overflow: visible; +} + .mat-mdc-snack-bar-container.settings-snackbar { margin-bottom: calc(76px + env(safe-area-inset-bottom, 0px)); } diff --git a/docs/architecture/workspace-shell.md b/docs/architecture/workspace-shell.md index b3e37bd75..67d9fe3a8 100644 --- a/docs/architecture/workspace-shell.md +++ b/docs/architecture/workspace-shell.md @@ -150,6 +150,24 @@ Command palette behavior is shell-owned but view-extensible: value is disabled. The new player setting applies to the next playback session; an existing stream is not re-mounted. +Keyboard shortcut help is shell-owned: + +1. `WorkspaceKeyboardShortcutsService` is provided by `WorkspaceShellComponent`. + It owns the workspace-scoped `document:keydown` listener for `?` / + `Shift+/`. +2. The listener ignores events from inputs, textareas, selects, and + content-editable elements via `isTypingInInput(...)`. +3. `libs/portal/shared/util/src/lib/keyboard-shortcut-definitions.ts` is the + metadata registry for shortcuts shown in the help dialog and documented in + README. `keyboard-shortcuts.ts` owns the display transformation and help + trigger detection. + Shortcuts that only work through the Electron bridge, such as embedded MPV + controls, must set `electronOnly: true` so the PWA dialog does not advertise + unavailable commands. +4. New custom shortcuts should be added to that registry when the handler is + added. Do not include native browser/editor behavior such as `Tab` or + platform text editing shortcuts. + ## Maintenance Guidance Use this document as the source of truth when changing workspace shell behavior. @@ -160,5 +178,7 @@ Use this document as the source of truth when changing workspace shell behavior. not duplicated inside the shell. 3. If a provider route changes how playlist/session bootstrap works, update the route-session provider and shell-facing route contract together. -4. Historical migration notes, cleanup lists, and one-off refactor steps should +4. When adding a non-native keyboard shortcut, update the shared shortcuts + registry, the help dialog tests, README, and the closest behavior test. +5. Historical migration notes, cleanup lists, and one-off refactor steps should stay out of this file; track them in issues or PR notes instead. diff --git a/libs/portal/shared/util/src/index.ts b/libs/portal/shared/util/src/index.ts index 6ce6a72c9..425e617b2 100644 --- a/libs/portal/shared/util/src/index.ts +++ b/libs/portal/shared/util/src/index.ts @@ -17,6 +17,7 @@ export * from './lib/live-epg-panel-state'; export * from './lib/live-sidebar-state'; export * from './lib/live-layout-sidebar-state.service'; export * from './lib/keyboard'; +export * from './lib/keyboard-shortcuts'; export * from './lib/remote-channel-navigation'; export * from './lib/workspace-header-context.service'; export * from './lib/workspace-view-command.types'; diff --git a/libs/portal/shared/util/src/lib/keyboard-shortcut-definitions.ts b/libs/portal/shared/util/src/lib/keyboard-shortcut-definitions.ts new file mode 100644 index 000000000..306e95abe --- /dev/null +++ b/libs/portal/shared/util/src/lib/keyboard-shortcut-definitions.ts @@ -0,0 +1,202 @@ +export type KeyboardShortcutGroupId = + | 'global' + | 'navigation' + | 'playback' + | 'dialogs'; + +export interface PlatformShortcutChord { + mac: readonly string[]; + other: readonly string[]; +} + +export type KeyboardShortcutChord = + | string + | readonly string[] + | PlatformShortcutChord; + +export interface KeyboardShortcutDefinition { + id: string; + group: KeyboardShortcutGroupId; + labelKey: string; + icon: string; + keys: readonly KeyboardShortcutChord[]; + order: number; + electronOnly?: boolean; +} + +export const KEYBOARD_SHORTCUT_GROUPS: readonly { + id: KeyboardShortcutGroupId; + labelKey: string; + icon: string; +}[] = [ + { + id: 'global', + labelKey: 'WORKSPACE.SHORTCUTS.GROUPS.GLOBAL', + icon: 'keyboard', + }, + { + id: 'navigation', + labelKey: 'WORKSPACE.SHORTCUTS.GROUPS.NAVIGATION', + icon: 'explore', + }, + { + id: 'playback', + labelKey: 'WORKSPACE.SHORTCUTS.GROUPS.PLAYBACK', + icon: 'play_circle', + }, + { + id: 'dialogs', + labelKey: 'WORKSPACE.SHORTCUTS.GROUPS.DIALOGS', + icon: 'web_asset', + }, +]; + +const commandChord = (key: string): PlatformShortcutChord => ({ + mac: ['Cmd', key], + other: ['Ctrl', key], +}); + +export const APP_KEYBOARD_SHORTCUTS: readonly KeyboardShortcutDefinition[] = [ + { + id: 'open-keyboard-shortcuts', + group: 'global', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.OPEN_KEYBOARD_SHORTCUTS', + icon: 'help_outline', + keys: ['?'], + order: 0, + }, + { + id: 'open-command-palette', + group: 'global', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.OPEN_COMMAND_PALETTE', + icon: 'terminal', + keys: [commandChord('K')], + order: 10, + }, + { + id: 'open-global-search', + group: 'global', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.OPEN_GLOBAL_SEARCH', + icon: 'search', + keys: [commandChord('F')], + order: 20, + electronOnly: true, + }, + { + id: 'open-recently-viewed', + group: 'global', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.OPEN_RECENTLY_VIEWED', + icon: 'history', + keys: [commandChord('R')], + order: 30, + electronOnly: true, + }, + { + id: 'submit-shell-search', + group: 'global', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.SUBMIT_SHELL_SEARCH', + icon: 'keyboard_return', + keys: ['Enter'], + order: 40, + }, + { + id: 'toggle-sidebar', + group: 'navigation', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.TOGGLE_SIDEBAR', + icon: 'view_sidebar', + keys: [commandChord('B')], + order: 10, + }, + { + id: 'm3u-channel-number', + group: 'navigation', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.M3U_CHANNEL_NUMBER', + icon: 'dialpad', + keys: ['0-9'], + order: 20, + }, + { + id: 'embedded-mpv-play-pause', + group: 'playback', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.PLAY_PAUSE', + icon: 'play_arrow', + keys: ['Space', 'K'], + order: 10, + electronOnly: true, + }, + { + id: 'embedded-mpv-fullscreen', + group: 'playback', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.TOGGLE_FULLSCREEN', + icon: 'fullscreen', + keys: ['F'], + order: 20, + electronOnly: true, + }, + { + id: 'embedded-mpv-seek', + group: 'playback', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.SEEK', + icon: 'swap_horiz', + keys: ['ArrowLeft', 'ArrowRight'], + order: 30, + electronOnly: true, + }, + { + id: 'adjust-volume', + group: 'playback', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.ADJUST_VOLUME', + icon: 'volume_up', + keys: ['ArrowUp', 'ArrowDown'], + order: 40, + }, + { + id: 'mute-audio', + group: 'playback', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.MUTE_AUDIO', + icon: 'volume_off', + keys: ['M'], + order: 50, + }, + { + id: 'close-player-popovers', + group: 'playback', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.CLOSE_PLAYER_POPOVERS', + icon: 'close', + keys: ['Escape'], + order: 60, + electronOnly: true, + }, + { + id: 'command-palette-navigation', + group: 'dialogs', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.COMMAND_PALETTE_NAVIGATION', + icon: 'unfold_more', + keys: ['ArrowUp', 'ArrowDown'], + order: 10, + }, + { + id: 'command-palette-run', + group: 'dialogs', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.COMMAND_PALETTE_RUN', + icon: 'subdirectory_arrow_right', + keys: ['Enter'], + order: 20, + }, + { + id: 'close-dialogs', + group: 'dialogs', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.CLOSE_DIALOGS', + icon: 'close', + keys: ['Escape'], + order: 30, + }, + { + id: 'downloads-open-item', + group: 'dialogs', + labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.DOWNLOADS_OPEN_ITEM', + icon: 'download', + keys: ['Enter', 'Space'], + order: 40, + }, +]; diff --git a/libs/portal/shared/util/src/lib/keyboard-shortcuts.spec.ts b/libs/portal/shared/util/src/lib/keyboard-shortcuts.spec.ts new file mode 100644 index 000000000..4852806bd --- /dev/null +++ b/libs/portal/shared/util/src/lib/keyboard-shortcuts.spec.ts @@ -0,0 +1,95 @@ +import { + APP_KEYBOARD_SHORTCUTS, + getKeyboardShortcutGroups, + isKeyboardShortcutHelpTrigger, +} from './keyboard-shortcuts'; + +describe('keyboard shortcuts registry', () => { + it('keeps shortcut ids unique', () => { + const ids = APP_KEYBOARD_SHORTCUTS.map((shortcut) => shortcut.id); + + expect(new Set(ids).size).toBe(ids.length); + }); + + it('omits Electron-only shortcuts outside Electron', () => { + const groups = getKeyboardShortcutGroups({ + isMac: false, + isElectron: false, + }); + const ids = groups.flatMap((group) => + group.items.map((item) => item.id) + ); + + expect(ids).not.toContain('open-global-search'); + expect(ids).not.toContain('open-recently-viewed'); + expect(ids).not.toContain('embedded-mpv-play-pause'); + expect(ids).not.toContain('embedded-mpv-fullscreen'); + expect(ids).not.toContain('embedded-mpv-seek'); + expect(ids).not.toContain('close-player-popovers'); + expect(ids).toContain('open-command-palette'); + expect(ids).toContain('adjust-volume'); + expect(ids).toContain('mute-audio'); + }); + + it('uses platform-specific modifier labels', () => { + const macGroups = getKeyboardShortcutGroups({ + isMac: true, + isElectron: true, + }); + const linuxGroups = getKeyboardShortcutGroups({ + isMac: false, + isElectron: true, + }); + + expect(findChordLabels(macGroups, 'open-command-palette')).toEqual([ + ['Cmd', 'K'], + ]); + expect(findChordLabels(linuxGroups, 'open-command-palette')).toEqual([ + ['Ctrl', 'K'], + ]); + }); + + it('normalizes display labels for compact keycaps', () => { + const groups = getKeyboardShortcutGroups({ + isMac: false, + isElectron: true, + }); + + expect(findChordLabels(groups, 'embedded-mpv-seek')).toEqual([ + ['←'], + ['→'], + ]); + expect(findChordLabels(groups, 'close-dialogs')).toEqual([['Esc']]); + }); + + it('detects the shortcuts help trigger', () => { + expect( + isKeyboardShortcutHelpTrigger( + new KeyboardEvent('keydown', { key: '?' }) + ) + ).toBe(true); + expect( + isKeyboardShortcutHelpTrigger( + new KeyboardEvent('keydown', { key: '/', shiftKey: true }) + ) + ).toBe(true); + expect( + isKeyboardShortcutHelpTrigger( + new KeyboardEvent('keydown', { key: '/', ctrlKey: true }) + ) + ).toBe(false); + }); +}); + +function findChordLabels( + groups: ReturnType, + id: string +): readonly (readonly string[])[] { + const item = groups + .flatMap((group) => group.items) + .find((shortcut) => shortcut.id === id); + + return ( + item?.chords.map((chord) => chord.keys.map((key) => key.label)) ?? [] + ); +} diff --git a/libs/portal/shared/util/src/lib/keyboard-shortcuts.ts b/libs/portal/shared/util/src/lib/keyboard-shortcuts.ts new file mode 100644 index 000000000..7a2ebbd8f --- /dev/null +++ b/libs/portal/shared/util/src/lib/keyboard-shortcuts.ts @@ -0,0 +1,126 @@ +import { + APP_KEYBOARD_SHORTCUTS, + KEYBOARD_SHORTCUT_GROUPS, +} from './keyboard-shortcut-definitions'; +import type { + KeyboardShortcutChord, + KeyboardShortcutGroupId, +} from './keyboard-shortcut-definitions'; + +export { APP_KEYBOARD_SHORTCUTS }; +export type { KeyboardShortcutGroupId }; + +export interface KeyboardShortcutDisplayItem { + id: string; + labelKey: string; + icon: string; + chords: readonly KeyboardShortcutDisplayChord[]; +} + +export interface KeyboardShortcutDisplayChord { + id: string; + ariaLabel: string; + keys: readonly KeyboardShortcutDisplayKey[]; +} + +export interface KeyboardShortcutDisplayKey { + id: string; + label: string; + ariaLabel: string; + isModifier: boolean; +} + +export interface KeyboardShortcutDisplayGroup { + id: KeyboardShortcutGroupId; + labelKey: string; + icon: string; + items: readonly KeyboardShortcutDisplayItem[]; +} + +const KEY_LABELS = new Map([ + ['ArrowLeft', '←'], + ['ArrowRight', '→'], + ['ArrowUp', '↑'], + ['ArrowDown', '↓'], + ['Escape', 'Esc'], +]); + +const KEY_ARIA_LABELS = new Map([ + ['Cmd', 'Command'], + ['Ctrl', 'Control'], + ['ArrowLeft', 'Left arrow'], + ['ArrowRight', 'Right arrow'], + ['ArrowUp', 'Up arrow'], + ['ArrowDown', 'Down arrow'], + ['Escape', 'Escape'], +]); + +const MODIFIER_KEYS = new Set(['Cmd', 'Ctrl', 'Alt', 'Shift']); + +export function getKeyboardShortcutGroups(options: { + isMac: boolean; + isElectron: boolean; +}): readonly KeyboardShortcutDisplayGroup[] { + return KEYBOARD_SHORTCUT_GROUPS.map((group) => ({ + ...group, + items: APP_KEYBOARD_SHORTCUTS.filter( + (shortcut) => + shortcut.group === group.id && + (!shortcut.electronOnly || options.isElectron) + ) + .sort((first, second) => first.order - second.order) + .map((shortcut) => ({ + id: shortcut.id, + labelKey: shortcut.labelKey, + icon: shortcut.icon, + chords: shortcut.keys.map((key) => + resolveShortcutChord(key, options.isMac) + ), + })), + })).filter((group) => group.items.length > 0); +} + +export function isKeyboardShortcutHelpTrigger(event: KeyboardEvent): boolean { + if (event.ctrlKey || event.metaKey || event.altKey) { + return false; + } + + return event.key === '?' || (event.key === '/' && event.shiftKey); +} + +function resolveShortcutChord( + key: KeyboardShortcutChord, + isMac: boolean +): KeyboardShortcutDisplayChord { + const keys = resolveShortcutKeys(key, isMac); + + return { + id: keys.join('+'), + ariaLabel: keys + .map( + (shortcutKey) => KEY_ARIA_LABELS.get(shortcutKey) ?? shortcutKey + ) + .join(' + '), + keys: keys.map((shortcutKey) => ({ + id: shortcutKey.toLowerCase().replace(/[^a-z0-9]+/g, '-'), + label: KEY_LABELS.get(shortcutKey) ?? shortcutKey, + ariaLabel: KEY_ARIA_LABELS.get(shortcutKey) ?? shortcutKey, + isModifier: MODIFIER_KEYS.has(shortcutKey), + })), + }; +} + +function resolveShortcutKeys( + key: KeyboardShortcutChord, + isMac: boolean +): readonly string[] { + if (typeof key === 'string') { + return [key]; + } + + if ('mac' in key) { + return isMac ? key.mac : key.other; + } + + return key; +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.html b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.html new file mode 100644 index 000000000..bb6925e26 --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.html @@ -0,0 +1,105 @@ +
+
+
+ +
+

+ {{ 'WORKSPACE.SHORTCUTS.TITLE' | translate }} +

+

{{ 'WORKSPACE.SHORTCUTS.SUBTITLE' | translate }}

+
+
+
+ + + {{ data.platformLabelKey | translate }} + + +
+
+ + +
+ @for (group of groups; track group.id) { +
+
+ +

{{ group.labelKey | translate }}

+
+ +
+ @for (item of group.items; track item.id) { +
+ + + {{ item.labelKey | translate }} + + + @for ( + chord of item.chords; + track chord.id + ) { + + @for ( + key of chord.keys; + track key.id + ) { + + {{ key.label }} + + @if (!$last) { + + } + } + + } + +
+ } +
+
+ } +
+
+ + + + +
diff --git a/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.scss b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.scss new file mode 100644 index 000000000..84943135c --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.scss @@ -0,0 +1,342 @@ +:host { + display: block; + app-region: no-drag; +} + +.shortcuts-dialog { + --shortcut-surface: var(--app-widget-bg, #1e2330); + --shortcut-surface-raised: var(--app-widget-header-bg, #232840); + --shortcut-text: var(--app-heading-color, #d8dce8); + --shortcut-muted: var(--app-body-color, #8b93a8); + --shortcut-border: var(--app-separator, rgba(255, 255, 255, 0.14)); + --shortcut-accent: var(--app-selection-color, #78adff); + --shortcut-panel-bg: color-mix( + in srgb, + var(--shortcut-surface) 86%, + var(--shortcut-surface-raised) 14% + ); + --shortcut-panel-hover: color-mix( + in srgb, + var(--shortcut-accent) 10%, + var(--shortcut-surface-raised) 90% + ); + --shortcut-key-bg: color-mix( + in srgb, + var(--shortcut-surface-raised) 88%, + white 12% + ); + --shortcut-key-shadow: color-mix(in srgb, black 24%, transparent); + + color: var(--shortcut-text); + background: + linear-gradient( + 145deg, + color-mix( + in srgb, + var(--shortcut-accent) 8%, + var(--shortcut-surface) 92% + ), + var(--shortcut-surface) 42% + ), + var(--shortcut-surface); + border: 1px solid var(--shortcut-border); + border-radius: 16px; + box-shadow: 0 22px 70px color-mix(in srgb, black 34%, transparent); + overflow: hidden; +} + +.shortcuts-dialog__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + padding: 20px 22px 16px; + border-bottom: 1px solid var(--shortcut-border); +} + +.shortcuts-dialog__title-block { + display: flex; + align-items: center; + min-width: 0; + gap: 14px; +} + +.shortcuts-dialog__hero-icon { + display: inline-grid; + flex: 0 0 auto; + place-items: center; + width: 46px; + height: 46px; + color: var(--shortcut-accent); + background: color-mix(in srgb, var(--shortcut-accent) 14%, transparent); + border: 1px solid + color-mix(in srgb, var(--shortcut-accent) 28%, transparent); + border-radius: 8px; + + mat-icon { + width: 24px; + height: 24px; + font-size: 24px; + } +} + +.shortcuts-dialog__header h2 { + margin: 0; + padding: 0; + font-size: 1.08rem; + font-weight: 700; + line-height: 1.25; +} + +.shortcuts-dialog__header p { + margin: 5px 0 0; + color: var(--shortcut-muted); + font-size: 0.86rem; + line-height: 1.4; +} + +.shortcuts-dialog__header-actions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 8px; +} + +.shortcuts-dialog__platform { + display: inline-flex; + align-items: center; + gap: 7px; + min-height: 32px; + padding: 0 10px; + color: var(--shortcut-muted); + background: color-mix( + in srgb, + var(--shortcut-surface-raised) 82%, + var(--shortcut-surface) 18% + ); + border: 1px solid var(--shortcut-border); + border-radius: 8px; + font-size: 0.76rem; + font-weight: 650; + line-height: 1; + white-space: nowrap; + + mat-icon { + width: 17px; + height: 17px; + font-size: 17px; + } +} + +.shortcuts-dialog__close { + flex: 0 0 auto; +} + +.shortcuts-dialog__content { + max-height: min(68vh, 670px); + padding: 12px; +} + +.shortcuts-dialog__groups { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.shortcuts-dialog__group { + min-width: 0; + background: var(--shortcut-panel-bg); + border: 1px solid var(--shortcut-border); + border-radius: 8px; + overflow: hidden; +} + +.shortcuts-dialog__group-header { + display: flex; + align-items: center; + gap: 9px; + min-height: 38px; + padding: 8px 12px; + border-bottom: 1px solid + color-mix(in srgb, var(--shortcut-border) 72%, transparent); +} + +.shortcuts-dialog__group-icon, +.shortcuts-dialog__action-icon { + display: inline-grid; + flex: 0 0 auto; + place-items: center; + border-radius: 7px; +} + +.shortcuts-dialog__group-icon { + width: 28px; + height: 28px; + color: var(--shortcut-accent); + background: color-mix(in srgb, var(--shortcut-accent) 12%, transparent); + + mat-icon { + width: 18px; + height: 18px; + font-size: 18px; + } +} + +.shortcuts-dialog__group h3 { + margin: 0; + color: var(--shortcut-text); + font-size: 0.74rem; + font-weight: 750; + line-height: 1.2; + text-transform: uppercase; +} + +.shortcuts-dialog__rows { + display: grid; + gap: 1px; +} + +.shortcuts-dialog__row { + display: grid; + grid-template-columns: 28px minmax(0, 1fr) max-content; + gap: 10px; + align-items: center; + min-height: 42px; + padding: 7px 10px; + font-size: 0.88rem; + line-height: 1.35; + transition: + background-color 140ms ease, + transform 140ms ease; + + &:hover { + background: var(--shortcut-panel-hover); + } +} + +.shortcuts-dialog__action-icon { + width: 28px; + height: 28px; + color: var(--shortcut-muted); + background: color-mix( + in srgb, + var(--shortcut-surface-raised) 78%, + var(--shortcut-surface) 22% + ); + + mat-icon { + width: 17px; + height: 17px; + font-size: 17px; + } +} + +.shortcuts-dialog__label { + min-width: 0; + color: var(--shortcut-text); + font-size: 0.85rem; +} + +.shortcuts-dialog__chords { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 6px 8px; + min-width: 0; +} + +.shortcuts-dialog__chord { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.shortcuts-dialog__joiner { + color: var(--shortcut-muted); + font-size: 0.68rem; + font-weight: 700; +} + +.shortcuts-dialog__chord kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28px; + min-height: 27px; + padding: 2px 8px 3px; + color: var(--shortcut-text); + background: var(--shortcut-key-bg); + border: 1px solid var(--shortcut-border); + border-radius: 6px; + box-shadow: + inset 0 -2px 0 var(--shortcut-key-shadow), + 0 1px 0 color-mix(in srgb, white 28%, transparent); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.72rem; + font-weight: 750; + line-height: 1; + white-space: nowrap; +} + +.shortcuts-dialog__chord kbd.shortcuts-dialog__keycap--modifier { + min-width: 38px; + color: var(--shortcut-accent); + border-color: color-mix( + in srgb, + var(--shortcut-accent) 34%, + var(--shortcut-border) + ); +} + +.shortcuts-dialog__actions { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 9px 14px 14px; + border-top: 1px solid var(--shortcut-border); +} + +@media (prefers-reduced-motion: no-preference) { + .shortcuts-dialog { + animation: shortcuts-dialog-enter 160ms ease-out; + } +} + +@media (max-width: 640px) { + .shortcuts-dialog__header { + align-items: flex-start; + flex-direction: column; + padding: 16px 16px 12px; + } + + .shortcuts-dialog__header-actions { + justify-content: space-between; + width: 100%; + } + + .shortcuts-dialog__groups { + grid-template-columns: 1fr; + } + + .shortcuts-dialog__row { + grid-template-columns: 28px minmax(0, 1fr); + gap: 8px 10px; + } + + .shortcuts-dialog__chords { + grid-column: 2; + justify-content: flex-start; + } +} + +@keyframes shortcuts-dialog-enter { + from { + opacity: 0; + transform: translateY(8px) scale(0.985); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.spec.ts new file mode 100644 index 000000000..da2a93f18 --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.spec.ts @@ -0,0 +1,138 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { WorkspaceKeyboardShortcutsDialogComponent } from './workspace-keyboard-shortcuts-dialog.component'; + +describe('WorkspaceKeyboardShortcutsDialogComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WorkspaceKeyboardShortcutsDialogComponent], + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { + groups: [ + { + id: 'global', + labelKey: 'WORKSPACE.SHORTCUTS.GROUPS.GLOBAL', + icon: 'keyboard', + items: [ + { + id: 'open-command-palette', + labelKey: + 'WORKSPACE.SHORTCUTS.ITEMS.OPEN_COMMAND_PALETTE', + icon: 'terminal', + chords: [ + { + id: 'Cmd+K', + ariaLabel: 'Command + K', + keys: [ + { + id: 'cmd', + label: 'Cmd', + ariaLabel: 'Command', + isModifier: true, + }, + { + id: 'k', + label: 'K', + ariaLabel: 'K', + isModifier: false, + }, + ], + }, + ], + }, + { + id: 'play-pause', + labelKey: + 'WORKSPACE.SHORTCUTS.ITEMS.PLAY_PAUSE', + icon: 'play_arrow', + chords: [ + { + id: 'Space', + ariaLabel: 'Space', + keys: [ + { + id: 'space', + label: 'Space', + ariaLabel: 'Space', + isModifier: false, + }, + ], + }, + { + id: 'K', + ariaLabel: 'K', + keys: [ + { + id: 'k', + label: 'K', + ariaLabel: 'K', + isModifier: false, + }, + ], + }, + ], + }, + ], + }, + ], + platformIcon: 'laptop_mac', + platformLabelKey: 'WORKSPACE.SHORTCUTS.PLATFORM.MAC', + }, + }, + { + provide: MatDialogRef, + useValue: { close: jest.fn() }, + }, + { + provide: TranslateService, + useValue: { + instant: (key: string) => key, + get: (key: string) => of(key), + stream: (key: string) => of(key), + onLangChange: of(null), + onTranslationChange: of(null), + onDefaultLangChange: of(null), + currentLang: 'en', + defaultLang: 'en', + }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent( + WorkspaceKeyboardShortcutsDialogComponent + ); + fixture.detectChanges(); + }); + + it('renders the dialog title and groups', () => { + const text = fixture.nativeElement.textContent; + + expect(text).toContain('WORKSPACE.SHORTCUTS.TITLE'); + expect(text).toContain('WORKSPACE.SHORTCUTS.GROUPS.GLOBAL'); + }); + + it('renders shortcut labels and multiple keys', () => { + const text = fixture.nativeElement.textContent; + + expect(text).toContain( + 'WORKSPACE.SHORTCUTS.ITEMS.OPEN_COMMAND_PALETTE' + ); + expect(text).toContain('WORKSPACE.SHORTCUTS.ITEMS.PLAY_PAUSE'); + expect(text).toContain('Cmd'); + expect(text).toContain('Space'); + expect(text).toContain('K'); + }); + + it('renders the detected platform label', () => { + expect(fixture.nativeElement.textContent).toContain( + 'WORKSPACE.SHORTCUTS.PLATFORM.MAC' + ); + }); +}); diff --git a/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.ts b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.ts new file mode 100644 index 000000000..1496f4c33 --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.ts @@ -0,0 +1,36 @@ +import { Component, inject } from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { TranslatePipe } from '@ngx-translate/core'; +import { KeyboardShortcutDisplayGroup } from '@iptvnator/portal/shared/util'; + +export interface WorkspaceKeyboardShortcutsDialogData { + groups: readonly KeyboardShortcutDisplayGroup[]; + platformIcon: string; + platformLabelKey: string; +} + +@Component({ + selector: 'app-workspace-keyboard-shortcuts-dialog', + imports: [MatButtonModule, MatDialogModule, MatIconModule, TranslatePipe], + templateUrl: './workspace-keyboard-shortcuts-dialog.component.html', + styleUrl: './workspace-keyboard-shortcuts-dialog.component.scss', +}) +export class WorkspaceKeyboardShortcutsDialogComponent { + private readonly dialogRef = inject( + MatDialogRef + ); + readonly data = + inject(MAT_DIALOG_DATA); + + readonly groups = this.data.groups; + + close(): void { + this.dialogRef.close(); + } +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service.spec.ts new file mode 100644 index 000000000..5b1fdf24f --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service.spec.ts @@ -0,0 +1,96 @@ +import { TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; +import { WorkspaceKeyboardShortcutsService } from './workspace-keyboard-shortcuts.service'; + +describe('WorkspaceKeyboardShortcutsService', () => { + let afterClosed$: Subject; + let dialog: { open: jest.Mock }; + let service: WorkspaceKeyboardShortcutsService; + + beforeEach(() => { + afterClosed$ = new Subject(); + dialog = { + open: jest.fn().mockReturnValue({ + afterClosed: () => afterClosed$.asObservable(), + }), + }; + + TestBed.configureTestingModule({ + providers: [ + WorkspaceKeyboardShortcutsService, + { provide: MatDialog, useValue: dialog }, + ], + }); + + service = TestBed.inject(WorkspaceKeyboardShortcutsService); + }); + + afterEach(() => { + Object.defineProperty(navigator, 'userAgentData', { + configurable: true, + value: undefined, + }); + TestBed.resetTestingModule(); + }); + + it('opens the shortcuts dialog when question mark is pressed', () => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: '?' })); + + expect(dialog.open).toHaveBeenCalledTimes(1); + }); + + it('opens the shortcuts dialog for Shift+/', () => { + document.dispatchEvent( + new KeyboardEvent('keydown', { key: '/', shiftKey: true }) + ); + + expect(dialog.open).toHaveBeenCalledTimes(1); + }); + + it('does not open while typing in an input', () => { + const input = document.createElement('input'); + document.body.appendChild(input); + + input.dispatchEvent( + new KeyboardEvent('keydown', { + key: '?', + bubbles: true, + }) + ); + + expect(dialog.open).not.toHaveBeenCalled(); + input.remove(); + }); + + it('does not open duplicate dialogs while one is active', () => { + service.openShortcutsDialog(); + service.openShortcutsDialog(); + + expect(dialog.open).toHaveBeenCalledTimes(1); + + afterClosed$.next(); + service.openShortcutsDialog(); + + expect(dialog.open).toHaveBeenCalledTimes(2); + }); + + it('uses userAgentData platform when resolving shortcut modifier labels', () => { + Object.defineProperty(navigator, 'userAgentData', { + configurable: true, + value: { platform: 'Windows' }, + }); + + service.openShortcutsDialog(); + + const dialogData = dialog.open.mock.calls[0][1].data; + const commandPaletteShortcut = dialogData.groups + .flatMap((group) => group.items) + .find((item) => item.id === 'open-command-palette'); + + expect(dialogData.platformLabelKey).toBe( + 'WORKSPACE.SHORTCUTS.PLATFORM.OTHER' + ); + expect(commandPaletteShortcut?.chords[0].keys[0].label).toBe('Ctrl'); + }); +}); diff --git a/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service.ts b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service.ts new file mode 100644 index 000000000..2b6f38daa --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service.ts @@ -0,0 +1,99 @@ +import { DestroyRef, Injectable, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { + getKeyboardShortcutGroups, + isKeyboardShortcutHelpTrigger, + isTypingInInput, +} from '@iptvnator/portal/shared/util'; +import { + WorkspaceKeyboardShortcutsDialogComponent, + WorkspaceKeyboardShortcutsDialogData, +} from './workspace-keyboard-shortcuts-dialog.component'; + +@Injectable() +export class WorkspaceKeyboardShortcutsService { + private readonly dialog = inject(MatDialog); + private readonly destroyRef = inject(DestroyRef); + private readonly onDocumentKeydown = (event: KeyboardEvent): void => + this.handleKeydown(event); + + private dialogRef: MatDialogRef< + WorkspaceKeyboardShortcutsDialogComponent, + unknown + > | null = null; + + constructor() { + if (typeof document !== 'undefined') { + document.addEventListener('keydown', this.onDocumentKeydown); + this.destroyRef.onDestroy(() => { + document.removeEventListener('keydown', this.onDocumentKeydown); + }); + } + } + + openShortcutsDialog(): void { + if (this.dialogRef) { + return; + } + + const platform = this.getShortcutPlatform(); + const dialogRef = this.dialog.open< + WorkspaceKeyboardShortcutsDialogComponent, + WorkspaceKeyboardShortcutsDialogData + >(WorkspaceKeyboardShortcutsDialogComponent, { + width: 'min(960px, 94vw)', + maxWidth: '94vw', + panelClass: 'workspace-shortcuts-overlay', + autoFocus: false, + data: { + groups: getKeyboardShortcutGroups({ + isMac: platform === 'mac', + isElectron: this.isElectron(), + }), + platformIcon: + platform === 'mac' ? 'laptop_mac' : 'desktop_windows', + platformLabelKey: + platform === 'mac' + ? 'WORKSPACE.SHORTCUTS.PLATFORM.MAC' + : 'WORKSPACE.SHORTCUTS.PLATFORM.OTHER', + }, + }); + + this.dialogRef = dialogRef; + dialogRef + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.dialogRef = null; + }); + } + + private handleKeydown(event: KeyboardEvent): void { + if (isTypingInInput(event) || !isKeyboardShortcutHelpTrigger(event)) { + return; + } + + event.preventDefault(); + this.openShortcutsDialog(); + } + + private getShortcutPlatform(): 'mac' | 'other' { + if (typeof navigator === 'undefined') { + return 'other'; + } + + const navigatorWithUserAgentData = navigator as Navigator & { + userAgentData?: { platform?: string }; + }; + const platform = + navigatorWithUserAgentData.userAgentData?.platform || + navigator.userAgent; + + return /Mac|iPhone|iPad|iPod/i.test(platform) ? 'mac' : 'other'; + } + + private isElectron(): boolean { + return typeof window !== 'undefined' && !!window.electron; + } +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html index 01afaf1cd..ff09cfe6e 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html @@ -54,6 +54,15 @@ }
+