mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
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
This commit is contained in:
1 parent
5d31c1d49a
commit
b2909a1186
38 files changed
+2036
-1
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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": "البث المباشر من جميع قوائم التشغيل",
|
||||
|
||||
@@ -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": "البث المباشر من جميع قوائم التشغيل",
|
||||
|
||||
@@ -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": "Прамы эфір з усіх плэйлістоў",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 από όλες τις λίστες αναπαραγωγής",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 из всех плейлистов",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "来自所有播放列表的直播电视",
|
||||
|
||||
@@ -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": "來自所有播放清單的直播電視",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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<typeof getKeyboardShortcutGroups>,
|
||||
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)) ?? []
|
||||
);
|
||||
}
|
||||
@@ -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<string, string>([
|
||||
['ArrowLeft', '←'],
|
||||
['ArrowRight', '→'],
|
||||
['ArrowUp', '↑'],
|
||||
['ArrowDown', '↓'],
|
||||
['Escape', 'Esc'],
|
||||
]);
|
||||
|
||||
const KEY_ARIA_LABELS = new Map<string, string>([
|
||||
['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;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<section class="shortcuts-dialog">
|
||||
<header class="shortcuts-dialog__header">
|
||||
<div class="shortcuts-dialog__title-block">
|
||||
<span class="shortcuts-dialog__hero-icon" aria-hidden="true">
|
||||
<mat-icon>keyboard</mat-icon>
|
||||
</span>
|
||||
<div>
|
||||
<h2 mat-dialog-title>
|
||||
{{ 'WORKSPACE.SHORTCUTS.TITLE' | translate }}
|
||||
</h2>
|
||||
<p>{{ 'WORKSPACE.SHORTCUTS.SUBTITLE' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcuts-dialog__header-actions">
|
||||
<span class="shortcuts-dialog__platform">
|
||||
<mat-icon aria-hidden="true">{{ data.platformIcon }}</mat-icon>
|
||||
{{ data.platformLabelKey | translate }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="shortcuts-dialog__close"
|
||||
[attr.aria-label]="'CLOSE' | translate"
|
||||
(click)="close()"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<mat-dialog-content class="shortcuts-dialog__content">
|
||||
<div class="shortcuts-dialog__groups">
|
||||
@for (group of groups; track group.id) {
|
||||
<section class="shortcuts-dialog__group">
|
||||
<header class="shortcuts-dialog__group-header">
|
||||
<span
|
||||
class="shortcuts-dialog__group-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<mat-icon>{{ group.icon }}</mat-icon>
|
||||
</span>
|
||||
<h3>{{ group.labelKey | translate }}</h3>
|
||||
</header>
|
||||
|
||||
<div class="shortcuts-dialog__rows">
|
||||
@for (item of group.items; track item.id) {
|
||||
<article class="shortcuts-dialog__row">
|
||||
<span
|
||||
class="shortcuts-dialog__action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<mat-icon>{{ item.icon }}</mat-icon>
|
||||
</span>
|
||||
<span class="shortcuts-dialog__label">
|
||||
{{ item.labelKey | translate }}
|
||||
</span>
|
||||
<span class="shortcuts-dialog__chords">
|
||||
@for (
|
||||
chord of item.chords;
|
||||
track chord.id
|
||||
) {
|
||||
<span
|
||||
class="shortcuts-dialog__chord"
|
||||
[attr.aria-label]="chord.ariaLabel"
|
||||
>
|
||||
@for (
|
||||
key of chord.keys;
|
||||
track key.id
|
||||
) {
|
||||
<kbd
|
||||
[attr.aria-label]="
|
||||
key.ariaLabel
|
||||
"
|
||||
[class.shortcuts-dialog__keycap--modifier]="
|
||||
key.isModifier
|
||||
"
|
||||
>
|
||||
{{ key.label }}
|
||||
</kbd>
|
||||
@if (!$last) {
|
||||
<span
|
||||
class="shortcuts-dialog__joiner"
|
||||
aria-hidden="true"
|
||||
>
|
||||
+
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end" class="shortcuts-dialog__actions">
|
||||
<button type="button" mat-button (click)="close()">
|
||||
{{ 'CLOSE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</section>
|
||||
+342
@@ -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);
|
||||
}
|
||||
}
|
||||
+138
@@ -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<WorkspaceKeyboardShortcutsDialogComponent>;
|
||||
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
+36
@@ -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<WorkspaceKeyboardShortcutsDialogComponent>
|
||||
);
|
||||
readonly data =
|
||||
inject<WorkspaceKeyboardShortcutsDialogData>(MAT_DIALOG_DATA);
|
||||
|
||||
readonly groups = this.data.groups;
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
+96
@@ -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<void>;
|
||||
let dialog: { open: jest.Mock };
|
||||
let service: WorkspaceKeyboardShortcutsService;
|
||||
|
||||
beforeEach(() => {
|
||||
afterClosed$ = new Subject<void>();
|
||||
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');
|
||||
});
|
||||
});
|
||||
+99
@@ -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;
|
||||
}
|
||||
}
|
||||
+9
@@ -54,6 +54,15 @@
|
||||
}
|
||||
|
||||
<div class="header-actions header-actions--leading">
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
(click)="onShortcutsRequested()"
|
||||
[matTooltip]="'WORKSPACE.SHORTCUTS.OPEN_TOOLTIP' | translate"
|
||||
[attr.aria-label]="'WORKSPACE.SHORTCUTS.OPEN_ARIA' | translate"
|
||||
>
|
||||
<mat-icon>keyboard</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
|
||||
+12
@@ -105,6 +105,18 @@ describe('WorkspaceShellHeaderComponent', () => {
|
||||
expect(requested).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('emits keyboard shortcuts requests from the help button', () => {
|
||||
const requested = jest.fn();
|
||||
component.shortcutsRequested.subscribe(requested);
|
||||
|
||||
const button: HTMLButtonElement = fixture.nativeElement.querySelector(
|
||||
'button[aria-label="WORKSPACE.SHORTCUTS.OPEN_ARIA"]'
|
||||
);
|
||||
button.click();
|
||||
|
||||
expect(requested).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not render the removed global favorites shortcut', () => {
|
||||
const button: HTMLButtonElement | null =
|
||||
fixture.nativeElement.querySelector(
|
||||
|
||||
+5
@@ -52,6 +52,7 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly searchChanged = output<string>();
|
||||
readonly searchSubmitted = output<string>();
|
||||
readonly commandPaletteRequested = output<void>();
|
||||
readonly shortcutsRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly headerBulkActionRequested = output<void>();
|
||||
@@ -86,6 +87,10 @@ export class WorkspaceShellHeaderComponent {
|
||||
this.commandPaletteRequested.emit();
|
||||
}
|
||||
|
||||
onShortcutsRequested(): void {
|
||||
this.shortcutsRequested.emit();
|
||||
}
|
||||
|
||||
onHeaderShortcutRequested(): void {
|
||||
this.headerShortcutRequested.emit();
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
(searchChanged)="facade.onSearchInput($event)"
|
||||
(searchSubmitted)="facade.onSearchEnter($event)"
|
||||
(commandPaletteRequested)="facade.openCommandPalette()"
|
||||
(shortcutsRequested)="keyboardShortcuts.openShortcutsDialog()"
|
||||
(addPlaylistRequested)="facade.openAddPlaylistDialog()"
|
||||
(headerShortcutRequested)="facade.runHeaderShortcut()"
|
||||
(headerBulkActionRequested)="facade.runHeaderBulkAction()"
|
||||
|
||||
+62
@@ -7,6 +7,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterOutlet, provideRouter } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import {
|
||||
WorkspacePortalContext,
|
||||
WorkspaceShellContextPanel,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
WorkspaceHeaderBulkAction,
|
||||
WorkspaceShellFacade,
|
||||
} from './services/workspace-shell.facade';
|
||||
import { WorkspaceKeyboardShortcutsService } from '../workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-shell-rail',
|
||||
@@ -61,6 +63,7 @@ class MockWorkspaceShellHeaderComponent {
|
||||
readonly searchChanged = output<string>();
|
||||
readonly searchSubmitted = output<string>();
|
||||
readonly commandPaletteRequested = output<void>();
|
||||
readonly shortcutsRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
@@ -117,6 +120,10 @@ class MockPlaylistDropZoneDirective {
|
||||
})
|
||||
class MockWorkspaceShellImportOverlayComponent {}
|
||||
|
||||
class MockWorkspaceKeyboardShortcutsService {
|
||||
openShortcutsDialog = jest.fn();
|
||||
}
|
||||
|
||||
class MockWorkspaceShellFacade {
|
||||
readonly brandLink = signal('/workspace/dashboard');
|
||||
readonly brandTooltipKey = signal('WORKSPACE.SHELL.RAIL_DASHBOARD');
|
||||
@@ -215,6 +222,10 @@ describe('WorkspaceShellComponent', () => {
|
||||
provide: WorkspaceShellFacade,
|
||||
useValue: facade,
|
||||
},
|
||||
{
|
||||
provide: WorkspaceKeyboardShortcutsService,
|
||||
useClass: MockWorkspaceKeyboardShortcutsService,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -264,6 +275,10 @@ describe('WorkspaceShellComponent', () => {
|
||||
provide: WorkspaceShellFacade,
|
||||
useValue: facade,
|
||||
},
|
||||
{
|
||||
provide: WorkspaceKeyboardShortcutsService,
|
||||
useClass: MockWorkspaceKeyboardShortcutsService,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -287,4 +302,51 @@ describe('WorkspaceShellComponent', () => {
|
||||
)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('opens keyboard shortcuts when the header requests them', async () => {
|
||||
const facade = new MockWorkspaceShellFacade();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WorkspaceShellComponent],
|
||||
providers: [provideRouter([])],
|
||||
})
|
||||
.overrideComponent(WorkspaceShellComponent, {
|
||||
set: {
|
||||
imports: [
|
||||
RouterOutlet,
|
||||
MockExternalPlaybackDockComponent,
|
||||
MockPlaylistDropOverlayComponent,
|
||||
MockPlaylistDropZoneDirective,
|
||||
MockWorkspaceShellContextSidebarComponent,
|
||||
MockWorkspaceShellHeaderComponent,
|
||||
MockWorkspaceShellImportOverlayComponent,
|
||||
MockWorkspaceShellRailComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: WorkspaceShellFacade,
|
||||
useValue: facade,
|
||||
},
|
||||
{
|
||||
provide: WorkspaceKeyboardShortcutsService,
|
||||
useClass: MockWorkspaceKeyboardShortcutsService,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(WorkspaceShellComponent);
|
||||
fixture.detectChanges();
|
||||
const shortcutsService = fixture.debugElement.injector.get(
|
||||
WorkspaceKeyboardShortcutsService
|
||||
) as unknown as MockWorkspaceKeyboardShortcutsService;
|
||||
const header = fixture.debugElement.query(
|
||||
By.directive(MockWorkspaceShellHeaderComponent)
|
||||
).componentInstance as MockWorkspaceShellHeaderComponent;
|
||||
|
||||
header.shortcutsRequested.emit();
|
||||
|
||||
expect(shortcutsService.openShortcutsDialog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { WorkspaceShellRailComponent } from './components/workspace-shell-rail/w
|
||||
import { WorkspaceShellFacade } from './services/workspace-shell.facade';
|
||||
import { WorkspaceShellXtreamImportService } from './services/workspace-shell-xtream-import.service';
|
||||
import { WorkspaceShellCommandPaletteService } from './services/workspace-shell-command-palette.service';
|
||||
import { WorkspaceKeyboardShortcutsService } from '../workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-shell',
|
||||
@@ -31,8 +32,10 @@ import { WorkspaceShellCommandPaletteService } from './services/workspace-shell-
|
||||
WorkspaceShellFacade,
|
||||
WorkspaceShellXtreamImportService,
|
||||
WorkspaceShellCommandPaletteService,
|
||||
WorkspaceKeyboardShortcutsService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceShellComponent {
|
||||
readonly facade = inject(WorkspaceShellFacade);
|
||||
readonly keyboardShortcuts = inject(WorkspaceKeyboardShortcutsService);
|
||||
}
|
||||
Reference in new issue
Block a user