Skip to content

Commit

Permalink
feat: improve segment edit (#126)
Browse files Browse the repository at this point in the history
* feat: improve segment edit

* i18n

* fix: notes

* fix

---------

Co-authored-by: ChrisItisdud <[email protected]>
Co-authored-by: Christina / Itisdud / twyn!b <[email protected]>
  • Loading branch information
3 people authored Oct 10, 2023
1 parent 43ba5e3 commit 7da61a3
Show file tree
Hide file tree
Showing 13 changed files with 345 additions and 63 deletions.
6 changes: 3 additions & 3 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 src/frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ declare module 'vue' {
BaseSelect: typeof import('./components/BaseSelect.vue')['default']
BaseSeparator: typeof import('./components/BaseSeparator.vue')['default']
BaseSkeleton: typeof import('./components/BaseSkeleton.vue')['default']
BaseSlider: typeof import('./components/BaseSlider.vue')['default']
BaseSwitch: typeof import('./components/BaseSwitch.vue')['default']
BaseTextArea: typeof import('./components/BaseTextArea.vue')['default']
ConfirmModal: typeof import('./components/ConfirmModal.vue')['default']
Expand Down
77 changes: 77 additions & 0 deletions src/frontend/components/BaseSlider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
const modelValue = defineModel({ default: 0 })
const track = ref<HTMLDivElement>()
const thumb = ref<HTMLDivElement>()
const isDragging = ref(false)
useEventListener(document, 'mouseup', () => {
isDragging.value = false
})
useEventListener(document, 'mousemove', (e) => {
if (!isDragging.value)
return
if (!track.value || !thumb.value)
return
const rect = track.value.getBoundingClientRect()
const x = e.clientX - rect.left
const maxX = rect.width - thumb.value.clientWidth
const xPos = Math.min(Math.max(0, x), maxX)
modelValue.value = Math.floor(xPos * 100 / maxX)
})
onMounted(() => {
if (!track.value)
return
useEventListener(track.value, 'click', (e) => {
const parentWidth = (e.currentTarget as HTMLDivElement).offsetWidth
const offsetWidth = e.offsetX
modelValue.value = offsetWidth * 100 / parentWidth
})
})
</script>

<template>
<div
ref="track"
dir="ltr"
aria-disabled="false"
class="relative w-full flex touch-none select-none items-center"
>
<div
class="relative h-2 w-full grow cursor-pointer overflow-hidden rounded-full bg-secondary"
>
<span
class="absolute left-0 h-full bg-primary"
:style="{ right: `${100 - modelValue}%` }"
/>
</div>

<div
class="absolute"
:style="{
transform: `translate(-${modelValue}%)`,
left: `${modelValue}%`,
}"
>
<div
ref="thumb"
role="slider"
aria-valuemin="0"
aria-valuemax="100"
aria-orientation="horizontal"
tabindex="0"
class="h-5 w-5 cursor-pointer border-2 border-primary rounded-full bg-primary-foreground ring-offset-background transition-colors disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring"
:aria-valuenow="modelValue"
@mousedown="isDragging = true"
/>
</div>
</div>
</template>
179 changes: 167 additions & 12 deletions src/frontend/components/ModalEditSegment.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { Metadata } from 'models/api'
import WaveSurfer from 'wavesurfer.js'
const props = defineProps<{
/**
Expand All @@ -14,6 +15,14 @@ const props = defineProps<{
* Array of found metadata of segment
*/
metadata: Metadata[]
/**
* Url for editing segment
*/
url: string
/**
* Computed peaks for segment
*/
peaks?: number[][]
}>()
const emits = defineEmits<{
Expand All @@ -26,11 +35,85 @@ const emits = defineEmits<{
* Emits an event 'cancel' when the "Cancel" action is triggered.
*/
(e: 'cancel'): void
/**
* Emits an event when file's peaks are computed.
* @property {number[]} value Array of peak values.
*/
(e: 'updatePeaks', value: number[][]): void
}>()
const { isDark } = useDarkToggle()
const { t } = useI18n()
const ws = shallowRef<WaveSurfer>()
const index = ref(props.metaIndex)
const _metadata = ref(props.metadata)
const currentProcess = ref(0)
const currentTime = ref(0)
const duration = ref(1)
const isMouseOverRow = ref(false)
const isAudioLoading = ref(true)
const isPlaying = ref(false)
const isInteractingWithSlider = ref(false)
const formatedCurrentTime = computed(() => useConvertSecToMin(currentTime.value, 'mm:ss'))
const formatedDuration = computed(() => useConvertSecToMin(duration.value, 'mm:ss'))
onMounted(() => {
ws.value = WaveSurfer.create({
container: '#waveform',
waveColor: isDark.value ? 'hsl(81, 96%, 55%)' : 'hsl(81, 96%, 45%)',
progressColor: isDark.value ? 'hsl(79, 36%, 50%)' : 'hsl(79, 36%, 42%)',
barRadius: 5,
barWidth: 5,
barGap: 2,
cursorWidth: 3,
url: props.url,
peaks: props.peaks,
dragToSeek: true,
})
ws.value.on('ready', () => {
isAudioLoading.value = false
duration.value = ws.value?.getDuration() ?? 1
emits ('updatePeaks', ws.value?.exportPeaks() ?? [])
})
ws.value.on('timeupdate', (t) => {
if (isInteractingWithSlider.value)
return
currentTime.value = t
currentProcess.value = currentTime.value * 100 / duration.value
})
watch(currentProcess, () => {
if (!isInteractingWithSlider.value)
return
ws.value?.seekTo(currentProcess.value / 100)
})
ws.value.on('finish', () => isPlaying.value = false)
})
function handlePlayPause() {
isPlaying.value = !isPlaying.value
if (isPlaying.value)
ws.value?.play()
else ws.value?.pause()
}
function handleDeleteMetadata(i: number) {
if (index.value === i)
index.value = 0
_metadata.value.splice(i, 1)
}
function handleUpdateMetadata(i: number, key: keyof typeof _metadata.value[0], event: Event) {
const el = event.currentTarget as HTMLElement
_metadata.value[i][key] = el.textContent ?? ''
}
</script>

<template>
Expand All @@ -42,7 +125,30 @@ const isMouseOverRow = ref(false)
</template>

<template #body>
<div class="max-h-400px max-w-1200px wh-full overflow-auto pb-5">
<div>
<div class="relative">
<div id="waveform" class="min-h-128px border rounded-md" />

<div v-if="isAudioLoading" class="absolute-center">
<BaseLoader />
</div>
</div>

<div class="mt-5" @mouseover="isInteractingWithSlider = true" @mouseleave="isInteractingWithSlider = false">
<BaseSlider v-model="currentProcess" />
</div>

<div class="mt-3 flex items-start justify-between">
<span class="min-w-25px text-sm">{{ formatedCurrentTime }}</span>
<BaseButton icon-only variant="ghost" @click="handlePlayPause">
<span v-if="isPlaying" class="i-carbon:pause-filled text-xl" />
<span v-else class="i-carbon:play-filled-alt text-xl" />
</BaseButton>
<span class="min-w-25px text-sm">{{ formatedDuration }}</span>
</div>
</div>

<div class="max-h-400px max-w-1300px wh-full overflow-auto py-5">
<table class="w-full caption-bottom text-sm">
<thead class="sticky left-0 right-0 top-0 z-2 bg-primary-foreground">
<tr class="border-b border-b-border">
Expand Down Expand Up @@ -77,12 +183,16 @@ const isMouseOverRow = ref(false)
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground">
{{ t('song.isrc') }}
</th>

<th class="sticky right-0 top-0 z-1 h-12 bg-primary-foreground px-4 text-right align-middle font-medium text-muted-foreground">
{{ t('button.delete') }}
</th>
</tr>
</thead>

<tbody>
<tbody :key="_metadata.length">
<tr
v-for="({ title, artist, album, year, albumartist, genre, isrc }, i) in metadata"
v-for="({ title, artist, album, year, albumartist, genre, isrc }, i) in _metadata"
:key="i" class="group cursor-pointer border-b border-b-border hover:bg-secondary"
:class="!isMouseOverRow && index === i ? 'bg-secondary' : 'bg-primary-foreground'"
@mouseover="isMouseOverRow = true"
Expand All @@ -97,42 +207,87 @@ const isMouseOverRow = ref(false)
</td>

<td
class="sticky left-50px top-0 z-1 min-w-200px p-4 align-middle font-medium group-hover:bg-secondary"
class="sticky left-50px top-0 z-1 min-w-200px p-4 align-middle font-medium outline-none group-hover:bg-secondary focus:text-neon"
:class="!isMouseOverRow && index === i ? 'bg-secondary' : 'bg-primary-foreground'"
contenteditable
@input="(e) => handleUpdateMetadata(i, 'title', e)"
>
{{ title ?? t('song.unknown') }}
</td>

<td class="min-w-150px p-4 align-middle">
<td
class="min-w-150px p-4 align-middle outline-none focus:text-neon"
contenteditable
@input="(e) => handleUpdateMetadata(i, 'artist', e)"
>
{{ artist ?? t('song.unknown') }}
</td>

<td class="min-w-350px p-4 align-middle">
<td
class="min-w-350px p-4 align-middle outline-none focus:text-neon"
contenteditable
@input="(e) => handleUpdateMetadata(i, 'album', e)"
>
{{ album ?? t('song.unknown') }}
</td>

<td class="min-w-100px p-4 align-middle">
<td
class="min-w-100px p-4 align-middle outline-none focus:text-neon"
contenteditable
@input="(e) => handleUpdateMetadata(i, 'year', e)"
>
{{ year ?? t('song.unknown') }}
</td>

<td class="min-w-120px p-4 align-middle">
<td
class="min-w-120px p-4 align-middle outline-none focus:text-neon"
contenteditable
@input="(e) => handleUpdateMetadata(i, 'albumartist', e)"
>
{{ albumartist ?? t('song.unknown') }}
</td>

<td class="min-w-100px p-4 align-middle">
<td
class="min-w-100px p-4 align-middle outline-none focus:text-neon"
contenteditable
@input="(e) => handleUpdateMetadata(i, 'genre', e)"
>
{{ genre ?? t('song.unknown') }}
</td>

<td class="min-w-120px p-4 align-middle">
<td
class="min-w-120px p-4 align-middle outline-none focus:text-neon"
contenteditable
@input="(e) => handleUpdateMetadata(i, 'isrc', e)"
>
{{ isrc ?? t('song.unknown') }}
</td>

<td
class="sticky right-0 top-0 z-1 px-4 align-middle group-hover:bg-secondary"
:class="!isMouseOverRow && index === i ? 'bg-secondary' : 'bg-primary-foreground'"
>
<BaseButton
variant="ghost" icon-only
:disabled="_metadata.length <= 1"
@click.stop="handleDeleteMetadata(i)"
>
<span class="i-carbon:trash-can" />
</BaseButton>
</td>
</tr>
</tbody>
</table>
</div>

<p class="mt-6 text-center text-muted-foreground">
{{ t('song.metadata_list_caption', { count: metadata.length }) }}
<div class="flex justify-center">
<BaseButton @click=" _metadata.push({ ..._metadata[index] })">
<span class="i-carbon:add mr-1" /> {{ t('button.new') }}
</BaseButton>
</div>

<p class="mt-5 text-center text-muted-foreground">
{{ t('song.metadata_list_caption', { count: _metadata.length }) }}
</p>
</template>

Expand Down
Loading

0 comments on commit 7da61a3

Please sign in to comment.