Media attachment cards with popover players and shared volume

- Audio/video attachments render as media cards (cover or icon,
  title/artist/size) instead of plain file chips; image attachments
  use 3:2 thumbnails matching the audio card height
- Players open in a Popover (autoplay on open) with a dimming
  overlay; AudioPlayerDialog/VideoPlayerDialog removed
- New useMediaVolume composable shares volume/mute between all
  audio players and Plyr via the 4brett-player storage key
- Player buttons and Plyr controls follow the theme border radius
- Portrait videos: aspect is applied even when metadata loaded
  before hydration, and the raw video is height-capped pre-JS

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-04 03:06:11 +02:00
co-authored by Devin
parent cd04135463
commit fe9e723c72
6 changed files with 154 additions and 167 deletions
+10 -7
View File
@@ -1,13 +1,16 @@
<script setup lang="ts">
import { useMediaControls } from '@vueuse/core'
defineProps<{
const props = defineProps<{
src: string
name: string
autoplay?: boolean
}>()
const audioRef = ref<HTMLAudioElement>()
const { playing, currentTime, duration, volume, muted } = useMediaControls(audioRef)
const { playing, currentTime, duration, volume, muted } = useMediaVolume(audioRef)
onMounted(() => {
if (props.autoplay) playing.value = true
})
function toggle() {
playing.value = !playing.value
@@ -33,7 +36,7 @@ function formatTime(sec: number) {
<audio ref="audioRef" :src="src" preload="metadata" class="hidden" />
<button
type="button"
class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-accent"
class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-accent"
:aria-label="playing ? $t('audio.pause') : $t('audio.play')"
@click="toggle"
>
@@ -54,7 +57,7 @@ function formatTime(sec: number) {
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">{{ formatTime(duration) }}</span>
<button
type="button"
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-accent"
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-accent"
:aria-label="muted ? $t('audio.unmute') : $t('audio.mute')"
@click="muted = !muted"
>
@@ -74,7 +77,7 @@ function formatTime(sec: number) {
<a
:href="src"
:download="name"
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
:aria-label="$t('audio.download')"
:title="$t('audio.download')"
>