"use client" import { useEffect, useRef, useState, useTransition } from "react" import { RiCheckboxCircleFill } from "react-icons/ri" import { PiShareFatLight } from "react-icons/pi" import { BsHeadsetVr } from "react-icons/bs" import CopyToClipboard from "react-copy-to-clipboard" import { LuCopyCheck } from "react-icons/lu" import { LuScrollText } from "react-icons/lu" import { BiCameraMovie } from "react-icons/bi" import { useStore } from "@/app/state/useStore" import { cn } from "@/lib/utils" import { VideoPlayer } from "@/app/interface/video-player" import { ActionButton, actionButtonClassName } from "@/app/interface/action-button" import { RecommendedVideos } from "@/app/interface/recommended-videos" import { isCertifiedUser } from "@/app/certification" import { watchVideo } from "@/app/server/actions/stats" import { formatTimeAgo } from "@/lib/formatTimeAgo" import { DefaultAvatar } from "@/app/interface/default-avatar" import { LikeButton } from "@/app/interface/like-button" import { ReportModal } from "../report-modal" import { formatLargeNumber } from "@/lib/formatLargeNumber" import { CommentList } from "@/app/interface/comment-list" import { Input } from "@/components/ui/input" import { localStorageKeys } from "@/app/state/localStorageKeys" import { defaultSettings } from "@/app/state/defaultSettings" import { getComments, submitComment } from "@/app/server/actions/comments" import { useCurrentUser } from "@/app/state/useCurrentUser" import { useLocalStorage } from "usehooks-ts" import { parseProjectionFromLoRA } from "@/app/server/actions/utils/parseProjectionFromLoRA" export function PublicVideoView() { const [_pending, startTransition] = useTransition() const [commentDraft, setCommentDraft] = useState("") const [isCommenting, setCommenting] = useState(false) const [isFocusedOnInput, setFocusedOnInput] = useState(false) // current time in the video // note: this is used to *set* the current time, not to read it // EDIT: you know what, let's do this the dirty way for now // const [desiredCurrentTime, setDesiredCurrentTime] = useState() const { user } = useCurrentUser() const [userThumbnail, setUserThumbnail] = useState("") useEffect(() => { setUserThumbnail(user?.thumbnail || "") }, [user?.thumbnail]) const handleBadUserThumbnail = () => { if (userThumbnail) { setUserThumbnail("") } } const video = useStore(s => s.publicVideo) const videoId = `${video?.id || ""}` const [copied, setCopied] = useState(false) const [channelThumbnail, setChannelThumbnail] = useState(`${video?.channel.thumbnail || ""}`) const setPublicVideo = useStore(s => s.setPublicVideo) const publicComments = useStore(s => s.publicComments) const setPublicComments = useStore(s => s.setPublicComments) const isEquirectangular = ( video?.projection === "equirectangular" || parseProjectionFromLoRA(video?.lora) === "equirectangular" ) // we inject the current videoId in the URL, if it's not already present // this is a hack for Hugging Face iframes useEffect(() => { const queryString = new URL(location.href).search const searchParams = new URLSearchParams(queryString) if (videoId) { if (searchParams.get("v") !== videoId) { console.log(`current videoId "${videoId}" isn't set in the URL query params.. TODO we should set it`) // searchParams.set("v", videoId) // location.search = searchParams.toString() } } else { // searchParams.delete("v") // location.search = searchParams.toString() } }, [videoId]) useEffect(() => { if (copied) { setTimeout(() => { setCopied(false) }, 2000) } }, [copied]) const handleBadChannelThumbnail = () => { if (channelThumbnail) { setChannelThumbnail("") } } useEffect(() => { startTransition(async () => { if (!video || !video.id) { return } const numberOfViews = await watchVideo(videoId) setPublicVideo({ ...video, numberOfViews }) }) }, [video?.id]) useEffect(() => { startTransition(async () => { if (!video || !video.id) { return } const comments = await getComments(videoId) setPublicComments(comments) }) }, [video?.id]) const [huggingfaceApiKey] = useLocalStorage( localStorageKeys.huggingfaceApiKey, defaultSettings.huggingfaceApiKey ) /* useEffect(() => { window.addEventListener("keydown", function (e) { if (e.code === "Space") { e.preventDefault(); } }) }, []) */ if (!video) { return null } const handleSubmitComment = () => { startTransition(async () => { if (!commentDraft || !huggingfaceApiKey || !videoId) { return } const limitedSizeComment = commentDraft.trim().slice(0, 1024).trim() const comment = await submitComment(video.id, limitedSizeComment, huggingfaceApiKey) setPublicComments( [comment].concat(publicComments) ) setCommentDraft("") setFocusedOnInput(false) setCommenting(false) }) } return (
{/** VIDEO PLAYER - HORIZONTAL */} {/** VIDEO TITLE - HORIZONTAL */}
{video.label}
{/*
AI Video Model: {video.model || "HotshotXL"}
*/}
{/** VIDEO TOOLBAR - HORIZONTAL */}
{/** LEFT PART OF THE TOOLBAR */} {/** RIGHT PART OF THE TOOLBAR */}
{/* SHARE */}
setCopied(true)}>
{ copied ? : }
{copied ? "Copied!" : "Share"}
Made with {video.model} {video.model} {isEquirectangular && See in VR } Source
{/** VIDEO DESCRIPTION - VERTICAL */}
{/* DESCRIPTION BLOCK */}
{formatLargeNumber(video.numberOfViews)} views
{formatTimeAgo(video.updatedAt).replace("about ", "")}

{video.description}

{/* COMMENTS */}
{Number(publicComments?.length || 0).toLocaleString()} Comment{ Number(publicComments?.length || 0) === 1 ? '' : 's' }
{/* COMMENT INPUT BLOCK - HORIZONTAL */} {user &&
{/* AVATAR */}
{ userThumbnail ?
: }
{/* COMMENT INPUTS AND BUTTONS - VERTICAL */}
{ if (!isFocusedOnInput) { setFocusedOnInput(true) } if (!isCommenting) { setCommenting(true) } setCommentDraft(x.target.value) }} value={commentDraft} onFocus={() => { if (!isFocusedOnInput) { setFocusedOnInput(true) } if (!isCommenting) { setCommenting(true) } }} onBlur={() => { setFocusedOnInput(false) }} onKeyDown={({ key }) => { if (key === 'Enter') { handleSubmitComment() } else { if (!isFocusedOnInput) { setFocusedOnInput(true) } if (!isCommenting) { setCommenting(true) } } }} />
{ setCommentDraft("") setCommenting(false) setFocusedOnInput(false) }} >Cancel Comment
}
) }