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
+51
View File
@@ -0,0 +1,51 @@
import { useMediaControls } from '@vueuse/core'
// Derselbe localStorage-Key wie Plyr im VideoPlayer — so teilen sich Video- und
// Audio-Player eine gemerkte Lautstärke (in beide Richtungen)
const STORAGE_KEY = '4brett-player'
interface StoredPrefs {
volume?: number
muted?: boolean
}
function readPrefs(): StoredPrefs {
try {
const raw = localStorage.getItem(STORAGE_KEY)
return raw ? JSON.parse(raw) as StoredPrefs : {}
}
catch {
return {}
}
}
export function useMediaVolume(target: Parameters<typeof useMediaControls>[0]) {
const controls = useMediaControls(target)
if (import.meta.client) {
const applyStored = () => {
const prefs = readPrefs()
if (typeof prefs.volume === 'number' && controls.volume.value !== prefs.volume) {
controls.volume.value = prefs.volume
}
if (typeof prefs.muted === 'boolean' && controls.muted.value !== prefs.muted) {
controls.muted.value = prefs.muted
}
}
watch(toRef(target), (el) => {
if (el) applyStored()
}, { immediate: true })
watch([controls.volume, controls.muted], ([v, m]) => {
const prefs = readPrefs()
if (prefs.volume === v && prefs.muted === m) return
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...prefs, volume: v, muted: m }))
}
catch { /* Storage deaktiviert/voll */ }
})
}
return controls
}