feat: Musik global, Dennis-Teil auf eigene Seite, Mobile-Autoplay

- MusicPlayer ins Layout verschoben (läuft auf allen Seiten)
- Mobile-Autoplay: Desktop startet stumm + fade-in bei Scroll,
  Mobile wartet auf ersten Touch und startet dann mit Fade-In
- Dennis-Perspektive auf eigene Seite /meine-oma ausgelagert,
  dezenter Link "Von Dennis" am Ende der Tribute-Sektion
- "Berge von Essen" entfernt
- "Jacky und Niklas" → "ihre Enkelin" / "Meine Schwester"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-02-16 03:57:48 +01:00
parent 4d56d4904a
commit 6363d1327b
6 changed files with 253 additions and 170 deletions
+71 -45
View File
@@ -7,7 +7,7 @@ import type { MediaItem } from '@/lib/types'
const TAIL_SKIP = 10
const CROSSFADE_DURATION = 3
const VOLUME = 0.4
const FADE_IN_DURATION = 2000 // ms to fade in after first interaction
const FADE_IN_MS = 2000
export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
const track = tracks[0] ?? null
@@ -17,7 +17,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
const audioB = useRef<HTMLAudioElement>(null)
const activeRef = useRef<'A' | 'B'>('A')
const fadingRef = useRef(false)
const unmutedRef = useRef(false)
const startedRef = useRef(false)
const [userMuted, setUserMuted] = useState(false)
const [playing, setPlaying] = useState(false)
@@ -31,12 +31,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
[],
)
const getVolume = useCallback(
() => (userMuted ? 0 : VOLUME),
[userMuted],
)
// Crossfade to loop back
// ── Crossfade loop ──────────────────────────────────────────────────────
const crossfade = useCallback(() => {
if (fadingRef.current) return
fadingRef.current = true
@@ -51,7 +46,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
const startTime = performance.now()
const outStartVol = out.volume
const targetVol = getVolume()
const targetVol = userMuted ? 0 : VOLUME
const step = () => {
const t = Math.min((performance.now() - startTime) / (CROSSFADE_DURATION * 1000), 1)
@@ -66,9 +61,9 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
}
}
requestAnimationFrame(step)
}, [getVolume, getActive, getInactive])
}, [userMuted, getActive, getInactive])
// Monitor playback for crossfade trigger
// Monitor for crossfade trigger
useEffect(() => {
if (!playing || !src) return
let id: number
@@ -86,7 +81,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
return () => cancelAnimationFrame(id)
}, [playing, src, getActive, crossfade])
// Fallback: if audio ends without crossfade, restart
// Fallback if crossfade misses
const handleEnded = useCallback(() => {
const a = getActive()
if (a) {
@@ -95,50 +90,81 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
}
}, [getActive])
// Sync volume when user toggles mute
// Sync volume on mute toggle
useEffect(() => {
if (!unmutedRef.current) return
const vol = getVolume()
const vol = userMuted ? 0 : VOLUME
const a = getActive()
const b = getInactive()
if (a && !fadingRef.current) a.volume = vol
if (b && !fadingRef.current && !b.paused) b.volume = vol
}, [userMuted, getVolume, getActive, getInactive])
}, [userMuted, getActive, getInactive])
// Autoplay strategy:
// 1. Start muted (browsers allow this)
// 2. On first user interaction (scroll/click/touch/key), fade volume in
// ── Autoplay strategy ───────────────────────────────────────────────────
// Mobile Safari needs play() called *directly* inside a user gesture.
// Desktop Chrome allows muted autoplay (volume=0).
//
// Strategy:
// 1. Try silent autoplay immediately (desktop)
// 2. On first touch/click, call play() directly in the handler (mobile)
// 3. Then fade volume in over 2s
useEffect(() => {
if (!src) return
const a = audioA.current
if (!a) return
// Start muted playback immediately
a.volume = 0
a.play().then(() => setPlaying(true)).catch(() => {})
// Fade in on first interaction
const fadeIn = () => {
if (unmutedRef.current) return
unmutedRef.current = true
const active = activeRef.current === 'A' ? audioA.current : audioB.current
if (!active) return
const startTime = performance.now()
const step = () => {
const t = Math.min((performance.now() - startTime) / FADE_IN_DURATION, 1)
active.volume = VOLUME * t
if (t < 1) requestAnimationFrame(step)
}
requestAnimationFrame(step)
const startAndFade = (a: HTMLAudioElement) => {
if (startedRef.current) return
startedRef.current = true
a.volume = 0
a.play().then(() => {
setPlaying(true)
// Fade in
const t0 = performance.now()
const fade = () => {
const t = Math.min((performance.now() - t0) / FADE_IN_MS, 1)
a.volume = VOLUME * t
if (t < 1) requestAnimationFrame(fade)
}
requestAnimationFrame(fade)
}).catch(() => {
startedRef.current = false
})
}
const events = ['scroll', 'click', 'touchstart', 'keydown'] as const
events.forEach((e) => window.addEventListener(e, fadeIn, { once: true, passive: true }))
return () => {
events.forEach((e) => window.removeEventListener(e, fadeIn))
// Attempt 1: silent autoplay (works on desktop)
const a = audioA.current
if (a) {
a.volume = 0
a.play().then(() => {
startedRef.current = true
setPlaying(true)
// Already playing silently, fade in on first interaction
const fadeIn = () => {
if (!startedRef.current) return
const t0 = performance.now()
const fade = () => {
const active = activeRef.current === 'A' ? audioA.current : audioB.current
if (!active) return
const t = Math.min((performance.now() - t0) / FADE_IN_MS, 1)
if (!fadingRef.current) active.volume = VOLUME * t
if (t < 1) requestAnimationFrame(fade)
}
requestAnimationFrame(fade)
}
const events = ['scroll', 'click', 'touchstart', 'keydown'] as const
events.forEach((e) => window.addEventListener(e, fadeIn, { once: true, passive: true }))
}).catch(() => {
// Attempt 2: autoplay blocked (mobile) → wait for user gesture
// play() MUST be called synchronously inside the handler
const onGesture = () => {
const el = audioA.current
if (el) startAndFade(el)
// Clean up other listeners
gestureEvents.forEach((e) => document.removeEventListener(e, onGesture))
}
const gestureEvents = ['click', 'touchstart', 'touchend', 'keydown'] as const
gestureEvents.forEach((e) =>
document.addEventListener(e, onGesture, { once: true, passive: true })
)
})
}
}, [src])