Skip to content

Commit

Permalink
Overhaul slider range inputs
Browse files Browse the repository at this point in the history
- Use vue-3-slider-component for all slider range inputs
- Now you can drag-and-drop the dot to seek in the seek bar and volume control bar
  • Loading branch information
tranxuanthang committed Sep 20, 2024
1 parent 852ad12 commit f9c7215
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 58 deletions.
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"path-browserify": "^1.0.1",
"semver": "^7.6.0",
"vue": "^3.2.37",
"vue-3-slider-component": "^1.0.1",
"vue-codemirror": "^6.1.1",
"vue-toastification": "^2.0.0-rc.5"
},
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ impl Player {

self.duration = sound_data.duration().as_secs_f64();
self.sound_handle = Some(self.manager.play(sound_data)?);
self.sound_handle.as_mut().unwrap().set_volume(self.volume, Tween::default());
}

Ok(())
Expand Down
7 changes: 2 additions & 5 deletions src/components/NowPlaying.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<div class="w-full flex gap-1 justify-center items-center">
<div class="flex-none w-12 text-xs text-brave-30">{{ humanDuration(progress) }}</div>
<Seek class="grow" :duration="duration" :progress="progress" @seek="seek" />
<div class="flex-none w-12 text-xs text-brave-30">{{ humanDuration(duration) }}</div>
<div class="flex-none text-right w-12 text-xs text-brave-30">{{ humanDuration(duration) }}</div>
</div>

<div class="flex justify-between w-full">
Expand Down Expand Up @@ -43,6 +43,7 @@ import { Play, Pause, Replay, Rewind_10, FastForward_10 } from 'mdue'
import { usePlayer } from '@/composables/player.js'
import { useGlobalState } from '@/composables/global-state.js'
import VolumeSlider from './now-playing/VolumeSlider.vue'
import { humanDuration } from '@/utils/human-duration'
const { isHotkey } = useGlobalState()
const { playingTrack, status, duration, progress, volume, playTrack, pause, resume, seek, setVolume: setPlayerVolume } = usePlayer()
Expand Down Expand Up @@ -91,10 +92,6 @@ const setVolume = (event) => {
setPlayerVolume(volume)
}
const humanDuration = (seconds) => {
return new Date(seconds * 1000).toISOString().slice(11, 19)
}
const lyricsClicked = (line) => {
seek(line.timestamp)
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/now-playing/LyricsViewer.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<transition name="slide-fade" mode="out-in">
<div v-if="lyrics && duration && progress" class="flex flex-col gap-1 border-b border-brave-90/50 relative z-10">
<div v-if="lyrics && duration && progress" class="flex flex-col gap-1 border-b border-brave-90/50 relative">
<transition name="slide-fade" mode="out-in">
<div v-if="expanded" class="full-viewer absolute bottom-0 left-0 w-full h-[40vh] bg-brave-95 border-t border-brave-90/50 overflow-hidden">
<div class="relative h-full">
Expand Down
2 changes: 1 addition & 1 deletion src/components/now-playing/PlainLyricsViewer.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<transition name="slide-fade" mode="out-in">
<div v-if="lyrics" class="flex flex-col gap-1 border-b border-brave-90/50 relative z-10">
<div v-if="lyrics" class="flex flex-col gap-1 border-b border-brave-90/50 relative">
<transition name="slide-fade" mode="out-in">
<div v-if="expanded" class="full-viewer absolute bottom-0 left-0 w-full h-[40vh] bg-brave-95 border-t border-brave-90/50 overflow-hidden">
<div class="relative h-full rounded text-center text-brave-50 whitespace-pre flex flex-col">
Expand Down
82 changes: 58 additions & 24 deletions src/components/now-playing/Seek.vue
Original file line number Diff line number Diff line change
@@ -1,38 +1,72 @@
<template>
<div class="bg-brave-90 w-full h-2 relative origin-center hover:scale-y-150 transition" @mouseover="onMouseOver" @mousemove="onMouseOver" @mouseleave="onMouseLeave" @click="chooseProgress">
<div class="bg-brave-30 h-full" :style="{ width: progressPercent }"></div>
<div class="bg-brave-60 h-full absolute top-0 left-0 opacity-40" :style="{ width: choosingProgressPercent }"></div>
</div>
<VueSlider
v-model="progressPercent"
:min="0"
:max="1"
:interval="0.001"
:duration="0"
:rail-style="{ backgroundColor: '#ffd9e2' }"
:dot-style="{ transition: 'initial' }"
:tooltip-style="{ zIndex: 200 }"
tooltip="hover"
@change="chooseProgress"
>
<template #dot="{pos, index, value, focus, disabled}">
<div
class="w-full h-full rounded-full bg-brave-30"
/>
</template>

<template #process="{ start, end }">
<div
class="absolute h-full rounded-full bg-brave-30"
:style="'width: ' + end + '%;'"
/>
</template>

<template #tooltip="{pos, index, value, focus, disabled}">
<div v-if="value" class="text-brave-30 text-[0.6rem] font-bold rounded-lg px-1 py-0.5 bg-brave-90">{{ humanDuration(value * props.duration) }}</div>
</template>
</VueSlider>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import VueSlider from "vue-3-slider-component";
import { ref, onMounted, watch } from 'vue'
import { humanDuration } from '@/utils/human-duration'
import _throttle from 'lodash/throttle'
const props = defineProps(['duration', 'progress'])
const emit = defineEmits(['seek'])
const isGracePeriod = ref(false)
const gracePeriodTimeout = ref(null)
const progressPercent = computed(() => {
if (!props.progress || !props.duration) {
return '0%'
}
return `${(props.progress / props.duration) * 100}%`
})
const progressPercent = ref(0)
const choosingProgress = ref(0.0)
// There is a slight delay after seeking before the player can actually start playing from the new position due to kira's StreamingSoundHandle.
// So this is a hack to prevent the seek bar from jumping back to the old position after user seeks.
// Also throttle the seek event to prevent it from being called too frequently.
const chooseProgress = _throttle((value) => {
emit('seek', value * props.duration)
const choosingProgressPercent = computed(() => `${choosingProgress.value * 100}%` )
isGracePeriod.value = true
const onMouseOver = (event) => {
const totalWidth = event.currentTarget.clientWidth
const seekWidth = event.offsetX
choosingProgress.value = seekWidth / totalWidth
}
clearTimeout(gracePeriodTimeout.value)
gracePeriodTimeout.value = setTimeout(() => {
isGracePeriod.value = false
}, 500)
}, 200)
const onMouseLeave = (event) => {
choosingProgress.value = 0.0
}
onMounted(() => {
progressPercent.value = props.progress / props.duration
})
const chooseProgress = () => {
emit('seek', choosingProgress.value * props.duration)
}
watch(() => props.progress, (newProgress) => {
if (isGracePeriod.value) return
progressPercent.value = newProgress / props.duration
})
watch(() => props.duration, (newDuration) => {
progressPercent.value = props.progress / newDuration
})
</script>
68 changes: 42 additions & 26 deletions src/components/now-playing/VolumeSlider.vue
Original file line number Diff line number Diff line change
@@ -1,47 +1,63 @@
<template>
<div class="flex items-center gap-1">
<button v-if="volume > 0" @click="mute" class="button text-brave-30 p-1 m-1 rounded-full"><VolumeMedium /></button>
<button v-else @click="unmute" class="button text-brave-30 p-1 m-1 rounded-full"><VolumeMute /></button>

<div class="bg-brave-90 w-32 h-2 relative origin-center hover:scale-y-150 transition" @mouseover="onMouseOver" @mousemove="onMouseOver" @mouseleave="onMouseLeave" @click="chooseVolume">
<div class="bg-brave-30 h-full" :style="{ width: volumePercent }"></div>
<div class="bg-brave-60 h-full absolute top-0 left-0 opacity-40" :style="{ width: choosingVolumePercent }"></div>
</div>
<div class="flex items-center gap-1 w-40">
<button v-if="volume > 0" @click="mute" class="flex-none button text-brave-30 p-1 m-1 rounded-full"><VolumeMedium /></button>
<button v-else @click="unmute" class="flex-none button text-brave-30 p-1 m-1 rounded-full"><VolumeMute /></button>

<VueSlider
class="grow"
v-model="volume"
:min="0"
:max="1"
:interval="0.01"
:rail-style="{ backgroundColor: '#ffd9e2' }"
tooltip="hover"
@change="chooseVolume"
>
<template #dot="{pos, index, value, focus, disabled}">
<div
class="w-full h-full rounded-full bg-brave-30"
/>
</template>

<template #process="{ start, end }">
<div
class="absolute h-full rounded-full bg-brave-30"
:style="'width: ' + end + '%;'"
/>
</template>

<template #tooltip="{pos, index, value, focus, disabled}">
<div v-if="value && value > 0" class="text-brave-30 text-[0.6rem] font-bold rounded-lg px-1 py-0.5 bg-brave-90">{{ Math.round(value * 100) }}%</div>
</template>
</VueSlider>
</div>
</template>

<script setup>
import { VolumeMedium, VolumeMute } from 'mdue';
import { ref, computed } from 'vue'
import VueSlider from "vue-3-slider-component";
import { ref, watch } from 'vue'
const props = defineProps(['volume'])
const emit = defineEmits(['setVolume'])
const volumePercent = computed(() => `${props.volume * 100}%`)
const volume = ref(props.volume)
const choosingVolume = ref(0.0)
const choosingVolumePercent = computed(() => `${choosingVolume.value * 100}%`)
const onMouseOver = (event) => {
const totalWidth = event.currentTarget.clientWidth
const seekWidth = event.offsetX
choosingVolume.value = seekWidth / totalWidth
}
const onMouseLeave = () => {
choosingVolume.value = 0.0
}
const chooseVolume = () => {
emit('setVolume', choosingVolume.value)
const chooseVolume = (value) => {
emit('setVolume', value)
}
const mute = () => {
volume.value = 0.0
emit('setVolume', 0.0)
}
const unmute = () => {
volume.value = 1.0
emit('setVolume', 1.0)
}
watch(props.volume, (newVolume) => {
volume.value = newVolume
})
</script>
19 changes: 18 additions & 1 deletion src/composables/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,38 @@ export function usePlayer() {
}

const pause = () => {
if (!playingTrack.value) {
return
}

invoke('pause_track')
}

const resume = () => {
if (!playingTrack.value) {
return
}

invoke('resume_track')
}

const seek = (position) => {
if (status.value === 'stopped') {
if (!playingTrack.value) {
return
}

if (status.value === 'stopped' ) {
invoke('play_track', { trackId: playingTrack.value.id })
}

invoke('seek_track', { position })
}

const stop = () => {
if (!playingTrack.value) {
return
}

invoke('stop_track')
}

Expand Down

0 comments on commit f9c7215

Please sign in to comment.