diff --git a/front/src/App.vue b/front/src/App.vue
index be355c079..28baa7a21 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -9,8 +9,7 @@ import SetInstanceModal from '~/components/SetInstanceModal.vue'
import ShortcutsModal from '~/components/ShortcutsModal.vue'
import FilterModal from '~/components/moderation/FilterModal.vue'
import ReportModal from '~/components/moderation/ReportModal.vue'
-import { useIntervalFn, useWindowSize } from '@vueuse/core'
-import GlobalEvents from '~/components/utils/global-events.vue'
+import { useIntervalFn, useToggle, useWindowSize } from '@vueuse/core'
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
import store from '~/store'
@@ -23,6 +22,7 @@ import {
} from '~/types'
import useWebSocketHandler from '~/composables/useWebSocketHandler'
import { getClientOnlyRadio } from '~/radios'
+import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
// Tracks
const currentTrack = computed(() => store.getters['queue/currentTrack'])
@@ -102,14 +102,18 @@ useWebSocketHandler('Listen', (event) => {
})
// Time ago
+// TODO (wvffle): Migrate to useTimeAgo
useIntervalFn(() => {
// used to redraw ago dates every minute
store.commit('ui/computeLastDate')
}, 1000 * 60)
+// Shortcuts
+const [showShortcutsModal, toggleShortcutsModal] = useToggle(false)
+onKeyboardShortcut('h', () => toggleShortcutsModal())
+
const { width } = useWindowSize()
const player = ref()
-const showShortcutsModal = ref(false)
const showSetInstanceModal = ref(false)
@@ -131,7 +135,7 @@ const showSetInstanceModal = ref(false)
@@ -168,7 +172,6 @@ const showSetInstanceModal = ref(false)
:show="showShortcutsModal"
@update:show="showShortcutsModal = $event"
/>
-
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 23fe2c269..a5ebb1771 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -156,7 +156,7 @@
:title="labels.play"
:aria-label="labels.play"
class="circular button control"
- @click.prevent.stop="resumePlayback"
+ @click.prevent.stop="playback = true"
>
@@ -165,7 +165,7 @@
:title="labels.pause"
:aria-label="labels.pause"
class="circular button control"
- @click.prevent.stop="pausePlayback"
+ @click.prevent.stop="playback = false"
>
@@ -234,7 +234,7 @@
@@ -316,30 +316,11 @@
-
diff --git a/front/src/composables/onKeyboardShortcut.ts b/front/src/composables/onKeyboardShortcut.ts
new file mode 100644
index 000000000..49667562c
--- /dev/null
+++ b/front/src/composables/onKeyboardShortcut.ts
@@ -0,0 +1,72 @@
+import { DefaultMagicKeysAliasMap, tryOnScopeDispose, useActiveElement, useEventListener } from '@vueuse/core'
+import { computed, reactive } from 'vue'
+import { isEqual, isMatch } from 'lodash-es'
+
+type KeyFilter = string | string[]
+
+interface Entry {
+ handler: () => unknown,
+ prevent: boolean,
+ __location?: string
+}
+
+const combinations = reactive(new Map())
+const activeElement = useActiveElement()
+const bodyIsActive = computed(() => activeElement.value === document.body)
+
+const current = new Set()
+useEventListener(window, 'keydown', (event) => {
+ if (!bodyIsActive.value) return
+ current.add(event.key.toLowerCase())
+
+ const currentArray = [...current]
+ for (const [requiredKeys, { handler, prevent }] of combinations.entries()) {
+ if (isEqual(currentArray, requiredKeys)) {
+ if (prevent) event.preventDefault()
+ handler()
+ }
+ }
+})
+
+useEventListener(window, 'keyup', (event) => {
+ current.delete(event.key.toLowerCase())
+})
+
+export default (key: KeyFilter, handler: () => unknown, prevent = false) => {
+ const combination = (Array.isArray(key) ? key : [key as string]).map(key => {
+ return DefaultMagicKeysAliasMap[key] ?? key
+ })
+
+ const entry: Entry = { prevent, handler }
+
+ if (import.meta.env.DEV) {
+ entry.__location = new Error().stack?.split('\n', 2).pop()
+ // TODO: Get correct line number somehow?
+ // Currently $3 is a line number that should work in .ts files,
+ // though in .vue files we need to get the line of the script plus
+ // the position of