parent
db7c0444b4
commit
b3022c26b6
|
@ -9,8 +9,7 @@ import SetInstanceModal from '~/components/SetInstanceModal.vue'
|
||||||
import ShortcutsModal from '~/components/ShortcutsModal.vue'
|
import ShortcutsModal from '~/components/ShortcutsModal.vue'
|
||||||
import FilterModal from '~/components/moderation/FilterModal.vue'
|
import FilterModal from '~/components/moderation/FilterModal.vue'
|
||||||
import ReportModal from '~/components/moderation/ReportModal.vue'
|
import ReportModal from '~/components/moderation/ReportModal.vue'
|
||||||
import { useIntervalFn, useWindowSize } from '@vueuse/core'
|
import { useIntervalFn, useToggle, useWindowSize } from '@vueuse/core'
|
||||||
import GlobalEvents from '~/components/utils/global-events.vue'
|
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
|
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
|
||||||
import store from '~/store'
|
import store from '~/store'
|
||||||
|
@ -23,6 +22,7 @@ import {
|
||||||
} from '~/types'
|
} from '~/types'
|
||||||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||||
import { getClientOnlyRadio } from '~/radios'
|
import { getClientOnlyRadio } from '~/radios'
|
||||||
|
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||||
|
|
||||||
// Tracks
|
// Tracks
|
||||||
const currentTrack = computed(() => store.getters['queue/currentTrack'])
|
const currentTrack = computed(() => store.getters['queue/currentTrack'])
|
||||||
|
@ -102,14 +102,18 @@ useWebSocketHandler('Listen', (event) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Time ago
|
// Time ago
|
||||||
|
// TODO (wvffle): Migrate to useTimeAgo
|
||||||
useIntervalFn(() => {
|
useIntervalFn(() => {
|
||||||
// used to redraw ago dates every minute
|
// used to redraw ago dates every minute
|
||||||
store.commit('ui/computeLastDate')
|
store.commit('ui/computeLastDate')
|
||||||
}, 1000 * 60)
|
}, 1000 * 60)
|
||||||
|
|
||||||
|
// Shortcuts
|
||||||
|
const [showShortcutsModal, toggleShortcutsModal] = useToggle(false)
|
||||||
|
onKeyboardShortcut('h', () => toggleShortcutsModal())
|
||||||
|
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
const player = ref()
|
const player = ref()
|
||||||
const showShortcutsModal = ref(false)
|
|
||||||
const showSetInstanceModal = ref(false)
|
const showSetInstanceModal = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -131,7 +135,7 @@ const showSetInstanceModal = ref(false)
|
||||||
<sidebar
|
<sidebar
|
||||||
:width="width"
|
:width="width"
|
||||||
@show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal"
|
@show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal"
|
||||||
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
|
@show:shortcuts-modal="toggleShortcutsModal"
|
||||||
/>
|
/>
|
||||||
<set-instance-modal
|
<set-instance-modal
|
||||||
:show="showSetInstanceModal"
|
:show="showSetInstanceModal"
|
||||||
|
@ -146,9 +150,9 @@ const showSetInstanceModal = ref(false)
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<router-view
|
<router-view
|
||||||
|
v-slot="{ Component }"
|
||||||
role="main"
|
role="main"
|
||||||
:class="{hidden: store.state.ui.queueFocused}"
|
:class="{hidden: store.state.ui.queueFocused}"
|
||||||
v-slot="{ Component }"
|
|
||||||
>
|
>
|
||||||
<Suspense v-if="Component">
|
<Suspense v-if="Component">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
|
@ -168,7 +172,6 @@ const showSetInstanceModal = ref(false)
|
||||||
:show="showShortcutsModal"
|
:show="showShortcutsModal"
|
||||||
@update:show="showShortcutsModal = $event"
|
@update:show="showShortcutsModal = $event"
|
||||||
/>
|
/>
|
||||||
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
:title="labels.play"
|
:title="labels.play"
|
||||||
:aria-label="labels.play"
|
:aria-label="labels.play"
|
||||||
class="circular button control"
|
class="circular button control"
|
||||||
@click.prevent.stop="resumePlayback"
|
@click.prevent.stop="playback = true"
|
||||||
>
|
>
|
||||||
<i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']" />
|
<i :class="['ui', 'big', 'play', {'disabled': !currentTrack}, 'icon']" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -165,7 +165,7 @@
|
||||||
:title="labels.pause"
|
:title="labels.pause"
|
||||||
:aria-label="labels.pause"
|
:aria-label="labels.pause"
|
||||||
class="circular button control"
|
class="circular button control"
|
||||||
@click.prevent.stop="pausePlayback"
|
@click.prevent.stop="playback = false"
|
||||||
>
|
>
|
||||||
<i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
<i :class="['ui', 'big', 'pause', {'disabled': !currentTrack}, 'icon']" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -234,7 +234,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="circular control button"
|
class="circular control button"
|
||||||
:disabled="queue.tracks.length === 0"
|
:disabled="queueIsEmpty"
|
||||||
:title="labels.shuffle"
|
:title="labels.shuffle"
|
||||||
:aria-label="labels.shuffle"
|
:aria-label="labels.shuffle"
|
||||||
@click.prevent.stop="shuffle()"
|
@click.prevent.stop="shuffle()"
|
||||||
|
@ -245,7 +245,7 @@
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
v-else
|
v-else
|
||||||
:class="['ui', 'random', {'disabled': queue.tracks.length === 0}, 'icon']"
|
:class="['ui', 'random', {'disabled': queueIsEmpty}, 'icon']"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -316,30 +316,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GlobalEvents
|
|
||||||
@keydown.p.prevent.exact="togglePlayback"
|
|
||||||
@keydown.esc.prevent.exact="$store.commit('ui/queueFocused', null)"
|
|
||||||
@keydown.ctrl.shift.left.prevent.exact="previous"
|
|
||||||
@keydown.ctrl.shift.right.prevent.exact="next"
|
|
||||||
@keydown.shift.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
|
|
||||||
@keydown.shift.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
|
|
||||||
@keydown.right.prevent.exact="seek (5)"
|
|
||||||
@keydown.left.prevent.exact="seek (-5)"
|
|
||||||
@keydown.shift.right.prevent.exact="seek (30)"
|
|
||||||
@keydown.shift.left.prevent.exact="seek (-30)"
|
|
||||||
@keydown.m.prevent.exact="toggleMute"
|
|
||||||
@keydown.l.exact="$store.commit('player/toggleLooping')"
|
|
||||||
@keydown.s.exact="shuffle"
|
|
||||||
@keydown.f.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
|
|
||||||
@keydown.q.exact="clean"
|
|
||||||
@keydown.e.exact="toggleMobilePlayer"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex'
|
import { useStore, mapState, mapGetters, mapActions } from 'vuex'
|
||||||
import GlobalEvents from '~/components/utils/global-events.vue'
|
|
||||||
import { toLinearVolumeScale } from '~/audio/volume.js'
|
import { toLinearVolumeScale } from '~/audio/volume.js'
|
||||||
import { Howl, Howler } from 'howler'
|
import { Howl, Howler } from 'howler'
|
||||||
import { throttle, reverse } from 'lodash-es'
|
import { throttle, reverse } from 'lodash-es'
|
||||||
|
@ -348,13 +329,122 @@ import VolumeControl from './VolumeControl.vue'
|
||||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||||
import updateQueryString from '~/composables/updateQueryString'
|
import updateQueryString from '~/composables/updateQueryString'
|
||||||
|
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||||
|
import { useThrottleFn, useTimeoutFn, useToggle } from '@vueuse/core'
|
||||||
|
import { computed, watch, defineEmits } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
VolumeControl,
|
VolumeControl,
|
||||||
TrackFavoriteIcon,
|
TrackFavoriteIcon,
|
||||||
TrackPlaylistIcon,
|
TrackPlaylistIcon
|
||||||
GlobalEvents
|
},
|
||||||
|
setup () {
|
||||||
|
const emit = defineEmits(['next', 'previous'])
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
|
||||||
|
const queue = computed(() => store.state.queue)
|
||||||
|
const queueIsEmpty = computed(() => queue.value.tracks.length === 0)
|
||||||
|
const currentTrack = computed(() => store.getters['queue/currentTrack'])
|
||||||
|
const currentTime = computed(() => store.state.player.currentTime)
|
||||||
|
|
||||||
|
const toggleMobilePlayer = () => {
|
||||||
|
store.commit('ui/queueFocused', ['queue', 'player'].indexOf(store.state.ui.queueFocused) > -1 ? null : 'player')
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffledMessage = $pgettext('Content/Queue/Message', 'Queue shuffled!')
|
||||||
|
const shuffle = useThrottleFn(() => {
|
||||||
|
if (queueIsEmpty.value) return
|
||||||
|
useTimeoutFn(async () => {
|
||||||
|
await store.dispatch('queue/shuffle')
|
||||||
|
store.commit('ui/addMessage', {
|
||||||
|
content: shuffledMessage,
|
||||||
|
date: new Date()
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
}, 101, false)
|
||||||
|
|
||||||
|
const seek = (step) => {
|
||||||
|
if (step > 0) {
|
||||||
|
// seek right
|
||||||
|
if (currentTime.value + step < this.duration) {
|
||||||
|
store.dispatch('player/updateProgress', (currentTime.value + step))
|
||||||
|
} else {
|
||||||
|
this.next() // parenthesis where missing here
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// seek left
|
||||||
|
const position = Math.max(currentTime.value + step, 0)
|
||||||
|
store.dispatch('player/updateProgress', position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = async () => {
|
||||||
|
await store.dispatch('queue/next')
|
||||||
|
emit('next')
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = async () => {
|
||||||
|
await store.dispatch('queue/previous')
|
||||||
|
emit('previous')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playback
|
||||||
|
const playing = computed(() => store.state.player.playing)
|
||||||
|
const [playback, togglePlayback] = useToggle()
|
||||||
|
watch(playback, isPlaying => store.dispatch(
|
||||||
|
isPlaying
|
||||||
|
? 'player/resumePlayback'
|
||||||
|
: 'player/pausePlayback'
|
||||||
|
))
|
||||||
|
|
||||||
|
// Add controls for notification drawer
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.setActionHandler('play', () => (playback.value = true))
|
||||||
|
navigator.mediaSession.setActionHandler('pause', () => (playback.value = false))
|
||||||
|
navigator.mediaSession.setActionHandler('seekforward', () => seek(5))
|
||||||
|
navigator.mediaSession.setActionHandler('seekbackward', () => seek(-5))
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', next)
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key binds
|
||||||
|
onKeyboardShortcut('e', toggleMobilePlayer)
|
||||||
|
onKeyboardShortcut('p', togglePlayback)
|
||||||
|
onKeyboardShortcut('s', shuffle)
|
||||||
|
onKeyboardShortcut('q', () => store.dispatch('queue/clean'))
|
||||||
|
onKeyboardShortcut('m', () => store.dispatch('player/toggleMute'))
|
||||||
|
onKeyboardShortcut('l', () => store.commit('player/toggleLooping'))
|
||||||
|
onKeyboardShortcut('f', () => store.dispatch('favorites/toggle', currentTrack.value?.id))
|
||||||
|
onKeyboardShortcut('escape', () => store.commit('ui/queueFocused', null))
|
||||||
|
|
||||||
|
onKeyboardShortcut(['shift', 'up'], () => store.commit('player/incrementVolume', 0.1), true)
|
||||||
|
onKeyboardShortcut(['shift', 'down'], () => store.commit('player/incrementVolume', -0.1), true)
|
||||||
|
|
||||||
|
onKeyboardShortcut('right', () => seek(5), true)
|
||||||
|
onKeyboardShortcut(['shift', 'right'], () => seek(30), true)
|
||||||
|
onKeyboardShortcut('left', () => seek(-5), true)
|
||||||
|
onKeyboardShortcut(['shift', 'left'], () => seek(-30), true)
|
||||||
|
|
||||||
|
onKeyboardShortcut(['ctrl', 'shift', 'left'], previous, true)
|
||||||
|
onKeyboardShortcut(['ctrl', 'shift', 'right'], next, true)
|
||||||
|
|
||||||
|
return {
|
||||||
|
queue,
|
||||||
|
queueIsEmpty,
|
||||||
|
currentTrack,
|
||||||
|
|
||||||
|
toggleMobilePlayer,
|
||||||
|
shuffle,
|
||||||
|
|
||||||
|
next,
|
||||||
|
previous,
|
||||||
|
playback,
|
||||||
|
playing
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -379,18 +469,14 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
currentIndex: state => state.queue.currentIndex,
|
currentIndex: state => state.queue.currentIndex,
|
||||||
playing: state => state.player.playing,
|
|
||||||
isLoadingAudio: state => state.player.isLoadingAudio,
|
isLoadingAudio: state => state.player.isLoadingAudio,
|
||||||
volume: state => state.player.volume,
|
volume: state => state.player.volume,
|
||||||
looping: state => state.player.looping,
|
looping: state => state.player.looping,
|
||||||
duration: state => state.player.duration,
|
duration: state => state.player.duration,
|
||||||
bufferProgress: state => state.player.bufferProgress,
|
bufferProgress: state => state.player.bufferProgress,
|
||||||
errored: state => state.player.errored,
|
errored: state => state.player.errored
|
||||||
currentTime: state => state.player.currentTime,
|
|
||||||
queue: state => state.queue
|
|
||||||
}),
|
}),
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: 'queue/currentTrack',
|
|
||||||
hasNext: 'queue/hasNext',
|
hasNext: 'queue/hasNext',
|
||||||
hasPrevious: 'queue/hasPrevious',
|
hasPrevious: 'queue/hasPrevious',
|
||||||
emptyQueue: 'queue/isEmpty',
|
emptyQueue: 'queue/isEmpty',
|
||||||
|
@ -512,31 +598,17 @@ export default {
|
||||||
this.getSound(this.currentTrack)
|
this.getSound(this.currentTrack)
|
||||||
this.updateMetadata()
|
this.updateMetadata()
|
||||||
}
|
}
|
||||||
// Add controls for notification drawer
|
|
||||||
if ('mediaSession' in navigator) {
|
|
||||||
navigator.mediaSession.setActionHandler('play', this.resumePlayback)
|
|
||||||
navigator.mediaSession.setActionHandler('pause', this.pausePlayback)
|
|
||||||
navigator.mediaSession.setActionHandler('seekforward', this.seekForward)
|
|
||||||
navigator.mediaSession.setActionHandler('seekbackward', this.seekBackward)
|
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', this.next)
|
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', this.previous)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeUnmount () {
|
||||||
this.dummyAudio.unload()
|
this.dummyAudio.unload()
|
||||||
this.observeProgress(false)
|
this.observeProgress(false)
|
||||||
},
|
},
|
||||||
destroyed () {
|
unmounted () {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
resumePlayback: 'player/resumePlayback',
|
|
||||||
pausePlayback: 'player/pausePlayback',
|
|
||||||
togglePlayback: 'player/togglePlayback',
|
|
||||||
mute: 'player/mute',
|
mute: 'player/mute',
|
||||||
unmute: 'player/unmute',
|
unmute: 'player/unmute'
|
||||||
clean: 'queue/clean',
|
|
||||||
toggleMute: 'player/toggleMute'
|
|
||||||
}),
|
}),
|
||||||
async getTrackData (trackData) {
|
async getTrackData (trackData) {
|
||||||
// use previously fetched trackData
|
// use previously fetched trackData
|
||||||
|
@ -549,36 +621,6 @@ export default {
|
||||||
() => null
|
() => null
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
shuffle () {
|
|
||||||
const disabled = this.queue.tracks.length === 0
|
|
||||||
if (this.isShuffling || disabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const self = this
|
|
||||||
const msg = this.$pgettext('Content/Queue/Message', 'Queue shuffled!')
|
|
||||||
this.isShuffling = true
|
|
||||||
setTimeout(() => {
|
|
||||||
self.$store.dispatch('queue/shuffle', () => {
|
|
||||||
self.isShuffling = false
|
|
||||||
self.$store.commit('ui/addMessage', {
|
|
||||||
content: msg,
|
|
||||||
date: new Date()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, 100)
|
|
||||||
},
|
|
||||||
next () {
|
|
||||||
const self = this
|
|
||||||
this.$store.dispatch('queue/next').then(() => {
|
|
||||||
self.$emit('next')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
previous () {
|
|
||||||
const self = this
|
|
||||||
this.$store.dispatch('queue/previous').then(() => {
|
|
||||||
self.$emit('previous')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleError ({ sound, error }) {
|
handleError ({ sound, error }) {
|
||||||
this.$store.commit('player/isLoadingAudio', false)
|
this.$store.commit('player/isLoadingAudio', false)
|
||||||
this.$store.dispatch('player/trackErrored')
|
this.$store.dispatch('player/trackErrored')
|
||||||
|
@ -737,20 +779,6 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
seek (step) {
|
|
||||||
if (step > 0) {
|
|
||||||
// seek right
|
|
||||||
if (this.currentTime + step < this.duration) {
|
|
||||||
this.$store.dispatch('player/updateProgress', (this.currentTime + step))
|
|
||||||
} else {
|
|
||||||
this.next() // parenthesis where missing here
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// seek left
|
|
||||||
const position = Math.max(this.currentTime + step, 0)
|
|
||||||
this.$store.dispatch('player/updateProgress', position)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
seekForward () {
|
seekForward () {
|
||||||
this.seek(5)
|
this.seek(5)
|
||||||
},
|
},
|
||||||
|
@ -864,13 +892,6 @@ export default {
|
||||||
this.observeProgress(true)
|
this.observeProgress(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleMobilePlayer () {
|
|
||||||
if (['queue', 'player'].indexOf(this.$store.state.ui.queueFocused) > -1) {
|
|
||||||
this.$store.commit('ui/queueFocused', null)
|
|
||||||
} else {
|
|
||||||
this.$store.commit('ui/queueFocused', 'player')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
switchTab () {
|
switchTab () {
|
||||||
if (this.$store.state.ui.queueFocused === 'player') {
|
if (this.$store.state.ui.queueFocused === 'player') {
|
||||||
this.$store.commit('ui/queueFocused', 'queue')
|
this.$store.commit('ui/queueFocused', 'queue')
|
||||||
|
|
|
@ -14,9 +14,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="results" />
|
<div class="results" />
|
||||||
<slot name="after" />
|
<slot name="after" />
|
||||||
<GlobalEvents
|
|
||||||
@keydown.shift.f.prevent.exact="focusSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -24,11 +21,19 @@
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
import router from '~/router'
|
import router from '~/router'
|
||||||
import { trim } from 'lodash-es'
|
import { trim } from 'lodash-es'
|
||||||
import GlobalEvents from '~/components/utils/global-events.vue'
|
import { useFocus } from '@vueuse/core'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
setup () {
|
||||||
GlobalEvents
|
const search = ref()
|
||||||
|
const { focused } = useFocus(search)
|
||||||
|
onKeyboardShortcut(['shift', 'f'], () => (focused.value = true), true)
|
||||||
|
|
||||||
|
return {
|
||||||
|
search
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
|
@ -244,9 +249,6 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
focusSearch () {
|
|
||||||
this.$refs.search.focus()
|
|
||||||
},
|
|
||||||
extractObjId (query) {
|
extractObjId (query) {
|
||||||
query = trim(query)
|
query = trim(query)
|
||||||
query = trim(query, '@')
|
query = trim(query, '@')
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
<script>
|
|
||||||
import $ from 'jquery'
|
|
||||||
|
|
||||||
const modifiersRE = /^[~!&]*/
|
|
||||||
const nonEventNameCharsRE = /\W+/
|
|
||||||
const names = {
|
|
||||||
'!': 'capture',
|
|
||||||
'~': 'once',
|
|
||||||
'&': 'passive'
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractEventOptions (eventDescriptor) {
|
|
||||||
const [modifiers] = eventDescriptor.match(modifiersRE)
|
|
||||||
return modifiers.split('').reduce((options, modifier) => {
|
|
||||||
options[names[modifier]] = true
|
|
||||||
return options
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
mounted () {
|
|
||||||
this._listeners = Object.create(null)
|
|
||||||
Object.keys(this.$listeners).forEach(event => {
|
|
||||||
const handler = this.$listeners[event]
|
|
||||||
const wrapper = function (event) {
|
|
||||||
// we check here the event is not triggered from an input
|
|
||||||
// to avoid collisions
|
|
||||||
if (!$(event.target).is('.field, :input, [contenteditable]')) {
|
|
||||||
handler(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener(
|
|
||||||
event.replace(nonEventNameCharsRE, ''),
|
|
||||||
wrapper,
|
|
||||||
extractEventOptions(event)
|
|
||||||
)
|
|
||||||
this._listeners[event] = handler
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeDestroy () {
|
|
||||||
for (const event in this._listeners) {
|
|
||||||
document.removeEventListener(
|
|
||||||
event.replace(nonEventNameCharsRE, ''),
|
|
||||||
this._listeners[event]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: h => h()
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -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 <script> tag in SFC
|
||||||
|
?.replace(/^(.+?)@.+\/(.+\..+?)(?:\?.+?|):(\d+):.+$/, 'Method $1 in $2')
|
||||||
|
|
||||||
|
// NOTE: Inform about possible combination collision
|
||||||
|
for (const [keys, { __location }] of combinations.entries()) {
|
||||||
|
const collisions = []
|
||||||
|
if (isMatch(keys, combination) || isMatch(combination, keys)) {
|
||||||
|
collisions.push(`${__location}: ${keys.join(' + ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collisions.length) {
|
||||||
|
console.warn([
|
||||||
|
'onKeyboardShortcut detected a possible collision in:',
|
||||||
|
`${entry.__location}: ${combination.join(' + ')}`,
|
||||||
|
...collisions
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combinations.set(combination, entry)
|
||||||
|
|
||||||
|
const stop = () => combinations.delete(combination)
|
||||||
|
tryOnScopeDispose(stop)
|
||||||
|
return stop
|
||||||
|
}
|
Loading…
Reference in New Issue