add youtube iframe
This commit is contained in:
parent
a7c4aa5608
commit
49dcfb28de
@ -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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(() => {
|
||||||
ytContainer = document.createElement('div')
|
// Use portal target if available, otherwise create visible container
|
||||||
ytContainer.style.cssText = 'position:fixed;width:1px;height:1px;left:-10px;top:-10px;opacity:0;pointer-events:none;'
|
const portalTarget = document.getElementById('yt-player-portal')
|
||||||
|
if (portalTarget) {
|
||||||
|
portalTarget.innerHTML = ''
|
||||||
|
ytContainer = portalTarget as HTMLDivElement
|
||||||
|
} else {
|
||||||
|
ytContainer = document.createElement('div')
|
||||||
|
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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user