add youtube iframe

This commit is contained in:
ordinarthur 2026-04-13 11:02:38 +02:00
parent a7c4aa5608
commit 49dcfb28de
2 changed files with 48 additions and 8 deletions

View File

@ -1,15 +1,17 @@
import { useRef, useState, useCallback } from 'react' import { useRef, useState, useCallback } from 'react'
import { Play, Pause, Volume2, VolumeX, SkipBack, SkipForward } from 'lucide-react' import { Play, Pause, Volume2, VolumeX, SkipBack, SkipForward, Maximize2, Minimize2 } from 'lucide-react'
import { usePlayerStore } from '@/stores/player' import { usePlayerStore } from '@/stores/player'
import { formatDuration } from '@/lib/utils' import { formatDuration } from '@/lib/utils'
import { publicUrl } from '@/lib/storage' import { publicUrl } from '@/lib/storage'
import { Avatar } from '@/components/ui/Avatar' import { Avatar } from '@/components/ui/Avatar'
import { isExternalUrl, getEmbedInfo } from '@/lib/embed'
export function PlayerBar() { export function PlayerBar() {
const { current, isPlaying, isExternal, progress, duration, volume, toggle, seek, setVolume } = usePlayerStore() const { current, isPlaying, isExternal, progress, duration, volume, toggle, seek, setVolume } = usePlayerStore()
const seekRef = useRef<HTMLDivElement>(null) const seekRef = useRef<HTMLDivElement>(null)
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
const [dragPct, setDragPct] = useState(0) const [dragPct, setDragPct] = useState(0)
const [ytExpanded, setYtExpanded] = useState(false)
const calcPct = useCallback((clientX: number) => { const calcPct = useCallback((clientX: number) => {
if (!seekRef.current) return 0 if (!seekRef.current) return 0
@ -19,6 +21,7 @@ export function PlayerBar() {
if (!current) return null if (!current) return null
const isYouTube = isExternal && current && isExternalUrl(current.audio_url) && getEmbedInfo(current.audio_url)?.platform === 'youtube'
const pct = duration > 0 ? (progress / duration) * 100 : 0 const pct = duration > 0 ? (progress / duration) * 100 : 0
const displayPct = isDragging ? dragPct : pct const displayPct = isDragging ? dragPct : pct
// Allow seeking for native audio and YouTube (via IFrame API) // Allow seeking for native audio and YouTube (via IFrame API)
@ -72,6 +75,28 @@ export function PlayerBar() {
const currentTime = isDragging ? (dragPct / 100) * duration : progress const currentTime = isDragging ? (dragPct / 100) * duration : progress
return ( return (
<>
{/* YouTube player - mini or expanded */}
{isYouTube && (
<div
className={`fixed z-[60] rounded-xl overflow-hidden shadow-[0_4px_20px_rgba(0,0,0,0.3)] transition-all duration-300 ${
ytExpanded
? 'inset-4 bottom-[106px]'
: 'bottom-[106px] right-4'
}`}
style={ytExpanded ? {} : { width: 200, height: 150 }}
>
<div id="yt-player-portal" className="w-full h-full [&_iframe]:!w-full [&_iframe]:!h-full [&_iframe]:!border-0" />
{/* Gradient overlay to hide YouTube title bar */}
<div className="absolute inset-x-0 top-0 h-10 bg-gradient-to-b from-black/80 to-transparent pointer-events-none" />
<button
onClick={() => setYtExpanded(!ytExpanded)}
className="absolute top-2 right-2 z-10 w-8 h-8 rounded-lg bg-black/60 hover:bg-black/80 flex items-center justify-center text-white transition-colors cursor-pointer"
>
{ytExpanded ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
</button>
</div>
)}
<div className="fixed bottom-0 left-0 right-0 z-50 bg-[#1E1B33] text-white shadow-[0_-2px_20px_rgba(0,0,0,0.3)]"> <div className="fixed bottom-0 left-0 right-0 z-50 bg-[#1E1B33] text-white shadow-[0_-2px_20px_rgba(0,0,0,0.3)]">
<div className="max-w-6xl mx-auto px-4 sm:px-6"> <div className="max-w-6xl mx-auto px-4 sm:px-6">
{/* Main row */} {/* Main row */}
@ -192,5 +217,6 @@ export function PlayerBar() {
</div>} </div>}
</div> </div>
</div> </div>
</>
) )
} }

View File

@ -50,7 +50,15 @@ function destroyYtPlayer() {
clearProgressInterval() clearProgressInterval()
try { ytPlayer?.destroy() } catch { /* ignore */ } try { ytPlayer?.destroy() } catch { /* ignore */ }
ytPlayer = null ytPlayer = null
if (ytContainer) { ytContainer.remove(); ytContainer = null } if (ytContainer) {
// If it's the portal, just clear contents; otherwise remove from DOM
if (ytContainer.id === 'yt-player-portal') {
ytContainer.innerHTML = ''
} else {
ytContainer.remove()
}
ytContainer = null
}
} }
function destroyIframe() { function destroyIframe() {
@ -178,20 +186,26 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
// Wait for previous save to complete, then fetch saved progress // Wait for previous save to complete, then fetch saved progress
savePrevPromise.then(() => fetchSavedProgress(podcast.id)).then((savedTime) => { savePrevPromise.then(() => fetchSavedProgress(podcast.id)).then((savedTime) => {
loadYouTubeAPI().then(() => { loadYouTubeAPI().then(() => {
// Use portal target if available, otherwise create visible container
const portalTarget = document.getElementById('yt-player-portal')
if (portalTarget) {
portalTarget.innerHTML = ''
ytContainer = portalTarget as HTMLDivElement
} else {
ytContainer = document.createElement('div') ytContainer = document.createElement('div')
ytContainer.style.cssText = 'position:fixed;width:1px;height:1px;left:-10px;top:-10px;opacity:0;pointer-events:none;' ytContainer.style.cssText = 'position:fixed;bottom:90px;right:16px;width:200px;height:200px;z-index:60;border-radius:12px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.3);'
document.body.appendChild(ytContainer)
}
const playerDiv = document.createElement('div') const playerDiv = document.createElement('div')
playerDiv.id = 'yt-player-' + Date.now() playerDiv.id = 'yt-player-' + Date.now()
ytContainer.appendChild(playerDiv) ytContainer.appendChild(playerDiv)
document.body.appendChild(ytContainer)
ytPlayer = new window.YT.Player(playerDiv.id, { ytPlayer = new window.YT.Player(playerDiv.id, {
videoId: embed.id, videoId: embed.id,
playerVars: { playerVars: {
autoplay: 1, autoplay: 1,
controls: 0, controls: 1,
disablekb: 1, fs: 1,
fs: 0,
modestbranding: 1, modestbranding: 1,
rel: 0, rel: 0,
start: savedTime > 0 ? Math.floor(savedTime) : undefined, start: savedTime > 0 ? Math.floor(savedTime) : undefined,