Skip to content

feat(VAudio): add new component - #23227

Open
J-Sek wants to merge 1 commit into
feat/vvideo-refactorfrom
feat/vaudio
Open

J-Sek wants to merge 1 commit into
feat/vvideo-refactorfrom
feat/vaudio

Conversation

@J-Sek

@J-Sek J-Sek commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Continuation of #23155

Stacked after #23241

Overall goal: introducing new component and extracting relevant composables to be shared with VVideo.

TODO:

  • review and unslopify
  • refine docs (missing icons, sloppy examples)

Preview:

image

Markup:

<template>
  <v-app :theme="dark ? 'dark' : 'light'">
    <v-app-bar density="compact" title="VAudio">
      <v-chip-group v-model="variant" class="mx-auto" mandatory>
        <v-chip
          v-for="name in ['default', 'pill', 'wavy']"
          :key="name"
          :text="name"
          :value="name"
          filter
          label
        />
      </v-chip-group>

      <template #append>
        <v-btn :text="rtl ? 'RTL' : 'LTR'" variant="text" @click="rtl = !rtl" />
        <v-icon-btn
          :icon="dark ? 'mdi-weather-night' : 'mdi-weather-sunny'"
          aria-label="Toggle theme"
          variant="text"
          @click="dark = !dark"
        />
      </template>
    </v-app-bar>

    <v-main>
      <v-locale-provider :rtl="rtl">
        <v-defaults-provider :defaults="{ VMediaProgressBar: { variant } }">
          <v-container class="d-flex flex-column ga-8 py-8" max-width="720">
            <section>
              <div class="text-h6 mb-2">0. Native</div>
              <audio :src="album[0].src" controls="true" controlslist="nodownload" preload="none" />
            </section>

            <section>
              <div class="text-h6 mb-2">1. Default (stacked, time below the bar)</div>
              <v-audio :src="album[0].src" />
            </section>

            <section class="d-flex flex-column ga-2">
              <div class="text-h6">2. Actions</div>
              <code>remaining time + volume + prev/next</code>
              <v-audio
                :src="album[1].src"
                actions="prev play next - volume"
                time-display="remaining"
              >
                <template #action.prev>
                  <v-icon-btn aria-label="Previous" icon="mdi-skip-previous" />
                </template>

                <template #action.next>
                  <v-icon-btn aria-label="Next" icon="mdi-skip-next" />
                </template>
              </v-audio>
              <code>inline ('progress' inside actions)</code>
              <v-audio
                :src="album[2].src"
                actions="play, time, progress"
                actions-class="ga-3"
                class="bg-surface rounded-pill border pa-1"
                max-width="360"
              >
                <template #append>
                  <v-icon-btn aria-label="More actions" icon="mdi-dots-vertical" icon-size="20" />
                </template>
              </v-audio>
            </section>

            <section class="d-flex flex-column ga-2">
              <div class="text-h6">3. Waveform</div>
              <code>decoded from src</code>
              <v-audio
                :src="album[1].src"
                actions="play, progress, time"
                progress-class="mx-3"
                time-display="elapsed"
              >
                <template #progress="{ props }">
                  <v-audio-waveform
                    :peaks-source="firstTrackBlob"
                    v-bind="props"
                    height="80"
                    mirror="0.5"
                  />
                </template>
              </v-audio>
              <code>dense, reveal on play</code>
              <v-audio
                :src="album[4].src"
                actions="play, progress, time"
                preload="none"
                progress-class="mx-3"
                time-display="elapsed"
              >
                <template #progress="{ props }">
                  <v-audio-waveform
                    :peaks-source="fourthTrackBlob"
                    v-bind="props"
                    :tooltip="false"
                    bar-gap="0"
                    bar-width="1"
                    height="20"
                  />
                </template>
              </v-audio>
              <code>live (narrow pill)</code>
              <v-audio
                :src="src"
                actions="play, progress, time"
                class="bg-surface-light rounded-pill pr-2"
                crossorigin="anonymous"
                progress-class="mx-2"
                time-display="elapsed"
                width="240"
              >
                <template #progress="{ props }">
                  <v-audio-waveform v-bind="props" bars="32" live />
                </template>
              </v-audio>
            </section>

            <section class="d-flex flex-column ga-2">
              <div class="text-h6">4. Slots</div>
              <code>skip buttons</code>
              <v-card class="pa-6 rounded-xl" width="400">
                <v-audio :play-props="{ iconSize: 44 }" :src="album[2].src" actions="left, play, right" progress-class="mb-2">
                  <template #action.left="{ seek }">
                    <v-icon-btn aria-label="Toggle playback loop" icon="mdi-repeat" />
                    <v-spacer />
                    <v-icon-btn aria-label="Add to library" icon="mdi-plus" icon-size="20" />
                    <v-spacer />
                    <v-icon-btn aria-label="Rewind 10 seconds" icon="mdi-rewind" @click="seek({ by: -10 })" />
                  </template>

                  <template #action.right="{ seek }">
                    <v-icon-btn aria-label="Forward 10 seconds" icon="mdi-fast-forward" @click="seek({ by: 10 })" />
                    <v-spacer />
                    <v-icon-btn aria-label="More actions" icon="mdi-dots-horizontal" icon-size="20" />
                    <v-spacer />
                    <v-icon-btn aria-label="Shuffle" icon="mdi-shuffle" />
                  </template>
                </v-audio>
              </v-card>
              <code>custom actions with inlined progress</code>
              <v-audio
                :actions="['prev', 'play', 'next', 'progress', 'rate', 'restart']"
                :play-props="{ color: 'primary', size: 36, variant: 'flat' }"
                :src="album[4].src"
                actions-class="ga-1"
                class="py-3 px-2 border"
              >
                <template #action.prev>
                  <v-icon-btn aria-label="Previous" icon="mdi-skip-previous" />
                </template>

                <template #action.next>
                  <v-icon-btn aria-label="Next" icon="mdi-skip-next" />
                </template>

                <template #progress="{ props, currentTime }">
                  <div class="d-flex align-center ga-3 flex-grow-1 mx-1">
                    <v-avatar size="44" style="background: linear-gradient(135deg, #673ab7, #00e676)" rounded />
                    <div class="d-flex flex-column flex-grow-1">
                      <div class="text-title-small font-weight-medium pb-1">{{ album[4].title }}</div>
                      <div class="d-flex align-center ga-2 text-body-small">
                        {{ currentTime.elapsed }}
                        <v-media-progress-bar v-bind="props" height="6" rounded />
                        {{ currentTime.total }}
                      </div>
                    </div>
                  </div>
                </template>

                <template #action.rate="{ playbackRate, setPlaybackRate }">
                  <v-chip
                    :text="`${playbackRate}x`"
                    aria-label="Toggle playback speed"
                    size="small"
                    variant="tonal"
                    label
                    @click="setPlaybackRate(playbackRate >= 2 ? 1 : playbackRate + 0.5)"
                  />
                </template>

                <template #action.restart="{ seek }">
                  <v-icon-btn aria-label="Restart" icon="mdi-restart" @click="seek({ to: 0 })" />
                </template>
              </v-audio>
              <code>chips around the bar (mobile)</code>
              <v-defaults-provider :defaults="{ VAudioControls: { VIconBtn: { color: 'surface', variant: 'flat', width: 60, height: 50, rounded: 'pill' } } }">
                <v-audio
                  :actions="['prev', 'skip-backward', 'play', 'skip-forward', 'next']"
                  :src="album[4].src"
                  actions-class="bg-surface-light rounded-pill pa-2 ga-2 mt-4"
                  color="primary"
                  width="358"
                  hide-time
                >
                  <template #progress="{ props, currentTime }">
                    <div class="d-flex align-center ga-4 w-100">
                      <v-chip :text="currentTime.elapsed" color="primary" size="small" variant="flat" />
                      <v-media-progress-bar v-bind="props" thumb />
                      <v-chip :text="currentTime.total" color="primary" size="small" variant="flat" />
                    </div>
                  </template>

                  <template #action.prev>
                    <v-icon-btn aria-label="Previous" color="secondary" icon="mdi-skip-previous" />
                  </template>

                  <template #play="{ props, playing }">
                    <v-icon-btn v-bind="props" :rounded="playing ? 16 : 25" class="animate-rounded" color="primary" width="70" />
                  </template>

                  <template #action.skip-backward="{ seek }">
                    <v-icon-btn aria-label="Rewind 10 seconds" icon="mdi-rewind-10" @click="seek({ by: -10 })" />
                  </template>

                  <template #action.skip-forward="{ seek }">
                    <v-icon-btn aria-label="Forward 10 seconds" icon="mdi-fast-forward-10" @click="seek({ by: 10 })" />
                  </template>

                  <template #action.next>
                    <v-icon-btn aria-label="Next" color="secondary" icon="mdi-skip-next" />
                  </template>
                </v-audio>
              </v-defaults-provider>
            </section>

            <section>
              <div class="text-h6 mb-2">5. VMediaProgressBar with chapters</div>
              <v-media-progress-bar
                v-model="position"
                :buffer="140"
                :chapters="chapters"
                :max="212"
                aria-label="Seek"
                color="primary"
              />
              <code>{{ position.toFixed(2) }}s</code>
            </section>
          </v-container>
        </v-defaults-provider>
      </v-locale-provider>
    </v-main>
  </v-app>
</template>

<script setup>
  import { onMounted, shallowRef } from 'vue'

  const dark = shallowRef(true)
  const rtl = shallowRef(false)
  const variant = shallowRef('default')
  const position = shallowRef(47)
  const chapters = [
    { start: 0, title: 'Intro' },
    { start: 38, title: 'Verse' },
    { start: 96, title: 'Chorus' },
    { start: 170, title: 'Outro' },
  ]

  const album = [
    { title: 'Mysterious Future Trap', artist: 'Bransboynd', src: 'https://cdn.pixabay.com/audio/2025/09/29/audio_8bde82c5c0.mp3' },
    { title: 'Cinematic Documentary Background', artist: 'Bransboynd', src: 'https://cdn.pixabay.com/audio/2026/09/07/audio_5e9aae4ea8.mp3' },
    { title: 'Burning Skyline', artist: 'MemoryArcade', src: 'https://cdn.pixabay.com/audio/2026/02/06/audio_565c388dda.mp3' },
    { title: 'Atmospheric Phonk Synthwave (Password Infinity)', artist: 'Evgeny_Bardyuzha', src: 'https://cdn.pixabay.com/audio/2022/10/18/audio_31c2730e64.mp3' },
    { title: 'No Copyright Cyberpunk', artist: 'HumanStudioED', src: 'https://cdn.pixabay.com/audio/2026/05/08/audio_c0a810ecde.mp3' },
  ]

  const src = 'https://cdn.freesound.org/previews/871/871092_14978258-lq.mp3'
  // const src2 = 'https://cdn.freesound.org/previews/612/612095_5674468-lq.mp3'

  const firstTrackBlob = shallowRef(null)
  const fourthTrackBlob = shallowRef(null)
  onMounted(async () => {
    firstTrackBlob.value = await (await fetch(album[1].src)).blob()
    fourthTrackBlob.value = await (await fetch(album[4].src)).blob()
  })
</script>

<style scoped>
.animate-rounded {
  transition: border-radius .2s ease-in-out;
  &:active {
    border-radius: 12px !important;
  }
}
</style>

@J-Sek J-Sek self-assigned this Sep 29, 2026
@J-Sek J-Sek added T: feature A new feature C: New Component This issue would need a new component to be developed. labels Sep 29, 2026
@J-Sek J-Sek added this to the v4.3.0 milestone Sep 29, 2026
@J-Sek
J-Sek marked this pull request as draft October 1, 2026 16:10
@J-Sek
J-Sek force-pushed the feat/vaudio branch 5 times, most recently from d6ad0f6 to 6e386c2 Compare October 6, 2026 15:49
@J-Sek
J-Sek changed the base branch from dev to feat/vvideo-refactor October 6, 2026 16:07
@J-Sek
J-Sek marked this pull request as ready for review October 6, 2026 16:08

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C: New Component This issue would need a new component to be developed. T: feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant