import { useCallback, useEffect, useRef, useState } from 'react' import YoutubeEmbed from 'react-youtube' import NicoViewer from '@/components/NicoViewer' import TwitterEmbed from '@/components/TwitterEmbed' import useDialogue from '@/lib/dialogues/useDialogue' import { Button } from '@/components/ui/button' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { useUITranslation } from '@/lib/useUITranslation' import type { FC, RefObject } from 'react' import type { NiconicoMetadata, NiconicoVideoInfo, NiconicoViewerHandle, Post } from '@/types' import type { YouTubePlayer } from 'react-youtube' type YouTubeEvent = { data: T target: YouTubePlayer } type Props = { ref?: RefObject post: Post onLoadComplete?: (info: NiconicoVideoInfo) => void onMetadataChange?: (meta: NiconicoMetadata) => void onVideoReady?: (durationMs: number) => void onPlaybackChange?: (currentTimeMs: number) => number | void onError?: (data: unknown) => void } const PostEmbed: FC = ( { ref, post, onLoadComplete, onMetadataChange, onVideoReady, onPlaybackChange, onError }, ) => { const dialogue = useDialogue () const { label } = useUITranslation ('posts') const behaviourSettings = useClientBehaviourSettings () const embedAutoLoadMode = behaviourSettings.embedAutoLoad ?? 'auto' const [manualLoadRequested, setManualLoadRequested] = useState (false) const [framed, setFramed] = useState (false) const [youtubePlayer, setYoutubePlayer] = useState (null) const niconicoVideoReadyRef = useRef (false) const notifyNiconicoVideoReady = useCallback ((durationMs: number) => { if (niconicoVideoReadyRef.current || !(Number.isFinite (durationMs)) || durationMs <= 0) return niconicoVideoReadyRef.current = true onVideoReady?.(durationMs) }, [onVideoReady]) const reportYoutubePlayback = useCallback (async (player: YouTubePlayer) => { const currentTime = await player.getCurrentTime () const currentTimeMs = currentTime * 1_000 const targetTimeMs = onPlaybackChange?.(currentTimeMs) if (typeof targetTimeMs !== 'number') return if (Math.abs (currentTimeMs - targetTimeMs) > 5_000) await player.seekTo (targetTimeMs / 1_000, true) }, [onPlaybackChange]) const handleYoutubeReady = async (event: YouTubeEvent) => { setYoutubePlayer (event.target) try { await event.target.playVideo () const duration = await event.target.getDuration () const durationMs = duration * 1_000 onVideoReady?.(durationMs) if (!(Number.isFinite (durationMs)) || durationMs <= 0) return await reportYoutubePlayback (event.target) } catch (error) { onError?.({ platform: 'youtube', error }) } } const handleYoutubeStateChange = (event: YouTubeEvent) => { void reportYoutubePlayback (event.target) } const handleYoutubeError = (event: YouTubeEvent) => { onError?.({ platform: 'youtube', code: event.data }) } const handleNiconicoLoadComplete = (info: NiconicoVideoInfo) => { notifyNiconicoVideoReady (info.lengthInSeconds * 1_000) onLoadComplete?.(info) } const handleNiconicoMetadataChange = (meta: NiconicoMetadata) => { notifyNiconicoVideoReady (meta.duration) onPlaybackChange?.(meta.currentTime) onMetadataChange?.(meta) } useEffect (() => { niconicoVideoReadyRef.current = false }, [post.url]) useEffect (() => { setManualLoadRequested (false) }, [embedAutoLoadMode, post.url]) useEffect (() => { if (!(youtubePlayer) || !(onPlaybackChange)) return const timer = setInterval ( () => void reportYoutubePlayback (youtubePlayer), 1_000) return () => clearInterval (timer) }, [onPlaybackChange, reportYoutubePlayback, youtubePlayer]) const url = new URL (post.url) const shouldLoadEmbed = embedAutoLoadMode === 'auto' || ( embedAutoLoadMode === 'manual' && manualLoadRequested ) const externalLink = ( {label ('embed.openExternal')} ) const manualLoadControl = (
{externalLink}
) switch (url.hostname.split ('.').slice (-2).join ('.')) { case 'nicovideo.jp': { const mVideoId = url.pathname.match (/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/) if (!(mVideoId)) break const [videoId] = mVideoId if (embedAutoLoadMode === 'off') return externalLink if (shouldLoadEmbed === false) return manualLoadControl return ( ) } case 'pixiv.net': { if (url.hostname !== 'pixiv.net' && url.hostname !== 'www.pixiv.net') break const artworkId = url.pathname === '/member_illust.php' ? url.searchParams.get ('illust_id') : url.pathname.match ( /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?artworks\/(\d+)\/?$/)?.[1] if (!(artworkId) || !(/^\d+$/.test (artworkId))) break if (embedAutoLoadMode === 'off') return externalLink if (shouldLoadEmbed === false) return manualLoadControl const embedUrl = `https://embed.pixiv.net/oembed_iframe.php?type=illust&id=${ artworkId }` return (