forked from Qortal/q-blog
Add video cover images
This commit is contained in:
953
BlogIndividualPost.tsx
Normal file
953
BlogIndividualPost.tsx
Normal file
@@ -0,0 +1,953 @@
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
CardHeader,
|
||||
Avatar,
|
||||
useTheme,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { styled } from '@mui/system'
|
||||
import AudiotrackIcon from '@mui/icons-material/Audiotrack'
|
||||
import ReadOnlySlate from '../../components/editor/ReadOnlySlate'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../state/store'
|
||||
import { checkStructure } from '../../utils/checkStructure'
|
||||
import { BlogContent } from '../../interfaces/interfaces'
|
||||
import ShareIcon from '@mui/icons-material/Share'
|
||||
import {
|
||||
setAudio,
|
||||
setCurrAudio,
|
||||
setIsLoadingGlobal,
|
||||
setVisitingBlog
|
||||
} from '../../state/features/globalSlice'
|
||||
import { VideoPlayer } from '../../components/common/VideoPlayer'
|
||||
import { AudioPlayer, IPlaylist } from '../../components/common/AudioPlayer'
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout'
|
||||
import '/node_modules/react-grid-layout/css/styles.css'
|
||||
import '/node_modules/react-resizable/css/styles.css'
|
||||
import DynamicHeightItem from '../../components/DynamicHeightItem'
|
||||
import {
|
||||
addPrefix,
|
||||
buildIdentifierFromCreateTitleIdAndId,
|
||||
removePrefix
|
||||
} from '../../utils/blogIdformats'
|
||||
import { DynamicHeightItemMinimal } from '../../components/DynamicHeightItemMinimal'
|
||||
import { ReusableModal } from '../../components/modals/ReusableModal'
|
||||
import AudioElement from '../../components/AudioElement'
|
||||
import ErrorBoundary from '../../components/common/ErrorBoundary'
|
||||
import { CommentSection } from '../../components/common/Comments/CommentSection'
|
||||
import { Tipping } from '../../components/common/Tipping/Tipping'
|
||||
import FileElement from '../../components/FileElement'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
import { setNotification } from '../../state/features/notificationsSlice'
|
||||
import ContextMenuResource from '../../components/common/ContextMenu/ContextMenuResource'
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive)
|
||||
const initialMinHeight = 2 // Define an initial minimum height for grid items
|
||||
|
||||
const md = [
|
||||
{ i: 'a', x: 0, y: 0, w: 4, h: initialMinHeight },
|
||||
{ i: 'b', x: 6, y: 0, w: 4, h: initialMinHeight }
|
||||
]
|
||||
const sm = [
|
||||
{ i: 'a', x: 0, y: 0, w: 6, h: initialMinHeight },
|
||||
{ i: 'b', x: 6, y: 0, w: 6, h: initialMinHeight }
|
||||
]
|
||||
const xs = [
|
||||
{ i: 'a', x: 0, y: 0, w: 6, h: initialMinHeight },
|
||||
{ i: 'b', x: 6, y: 0, w: 6, h: initialMinHeight }
|
||||
]
|
||||
|
||||
interface ILayoutGeneralSettings {
|
||||
padding: number
|
||||
blogPostType: string
|
||||
}
|
||||
export const BlogIndividualPost = () => {
|
||||
const { user, postId: postIdTemp, blog:blogTemp } = useParams()
|
||||
|
||||
const blog = React.useMemo(()=> {
|
||||
if(postIdTemp && postIdTemp?.includes('-post-')){
|
||||
const str = postIdTemp
|
||||
const arr = str.split('-post-')
|
||||
const str1 = arr[0]
|
||||
const blogId = removePrefix(str1)
|
||||
return blogId
|
||||
} else {
|
||||
return blogTemp
|
||||
}
|
||||
}, [postIdTemp])
|
||||
|
||||
const postId = React.useMemo(()=> {
|
||||
if(postIdTemp && postIdTemp?.includes('-post-')){
|
||||
const str = postIdTemp
|
||||
const arr = str.split('-post-')
|
||||
const str2 = arr[1]
|
||||
return str2
|
||||
} else {
|
||||
return postIdTemp
|
||||
}
|
||||
}, [postIdTemp])
|
||||
|
||||
const blogFull = React.useMemo(() => {
|
||||
if (!blog) return ''
|
||||
return addPrefix(blog)
|
||||
}, [blog])
|
||||
const { user: userState } = useSelector((state: RootState) => state.auth)
|
||||
const { audios, audioPostId } = useSelector(
|
||||
(state: RootState) => state.global
|
||||
)
|
||||
|
||||
const [avatarUrl, setAvatarUrl] = React.useState<string>('')
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const theme = useTheme()
|
||||
// const [currAudio, setCurrAudio] = React.useState<number | null>(null)
|
||||
const [layouts, setLayouts] = React.useState<any>({ md, sm, xs })
|
||||
const [count, setCount] = React.useState<number>(1)
|
||||
const [layoutGeneralSettings, setLayoutGeneralSettings] =
|
||||
React.useState<ILayoutGeneralSettings | null>(null)
|
||||
const [currentBreakpoint, setCurrentBreakpoint] = React.useState<any>()
|
||||
const handleLayoutChange = (layout: any, layoutss: any) => {
|
||||
// const redoLayouts = setAutoHeight(layoutss)
|
||||
setLayouts(layoutss)
|
||||
// saveLayoutsToLocalStorage(layoutss)
|
||||
}
|
||||
const [blogContent, setBlogContent] = React.useState<BlogContent | null>(null)
|
||||
const [isOpenSwitchPlaylistModal, setisOpenSwitchPlaylistModal] =
|
||||
useState<boolean>(false)
|
||||
const tempSaveAudio = useRef<any>(null)
|
||||
const saveAudio = React.useRef<any>(null)
|
||||
|
||||
const fullPostId = useMemo(() => {
|
||||
if (!blog || !postId) return ''
|
||||
dispatch(setIsLoadingGlobal(true))
|
||||
const formBlogId = addPrefix(blog)
|
||||
const formPostId = buildIdentifierFromCreateTitleIdAndId(formBlogId, postId)
|
||||
return formPostId
|
||||
}, [blog, postId])
|
||||
const getBlogPost = React.useCallback(async () => {
|
||||
try {
|
||||
if (!blog || !postId) return
|
||||
dispatch(setIsLoadingGlobal(true))
|
||||
const formBlogId = addPrefix(blog)
|
||||
const formPostId = buildIdentifierFromCreateTitleIdAndId(
|
||||
formBlogId,
|
||||
postId
|
||||
)
|
||||
const url = `/arbitrary/BLOG_POST/${user}/${formPostId}`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
const responseData = await response.json()
|
||||
|
||||
if (checkStructure(responseData)) {
|
||||
setBlogContent(responseData)
|
||||
if (responseData?.layouts) {
|
||||
setLayouts(responseData?.layouts)
|
||||
}
|
||||
if (responseData?.layoutGeneralSettings) {
|
||||
setLayoutGeneralSettings(responseData.layoutGeneralSettings)
|
||||
}
|
||||
const filteredAudios = (responseData?.postContent || []).filter(
|
||||
(content: any) => content?.type === 'audio'
|
||||
)
|
||||
|
||||
const transformAudios = filteredAudios?.map((fa: any) => {
|
||||
return {
|
||||
...(fa?.content || {}),
|
||||
id: fa?.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!audios && transformAudios.length > 0) {
|
||||
saveAudio.current = { audios: transformAudios, postId: formPostId }
|
||||
dispatch(setAudio({ audios: transformAudios, postId: formPostId }))
|
||||
} else if (
|
||||
formPostId === audioPostId &&
|
||||
audios?.length !== transformAudios.length
|
||||
) {
|
||||
tempSaveAudio.current = {
|
||||
message:
|
||||
"This post's audio playlist has updated. Would you like to switch?"
|
||||
}
|
||||
setisOpenSwitchPlaylistModal(true)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false))
|
||||
}
|
||||
}, [user, postId, blog])
|
||||
React.useEffect(() => {
|
||||
getBlogPost()
|
||||
}, [postId])
|
||||
|
||||
const switchPlayList = () => {
|
||||
const filteredAudios = (blogContent?.postContent || []).filter(
|
||||
(content) => content?.type === 'audio'
|
||||
)
|
||||
|
||||
const formatAudios = filteredAudios.map((fa) => {
|
||||
return {
|
||||
...(fa?.content || {}),
|
||||
id: fa?.id
|
||||
}
|
||||
})
|
||||
if (!blog || !postId) return
|
||||
const formBlogId = addPrefix(blog)
|
||||
const formPostId = buildIdentifierFromCreateTitleIdAndId(formBlogId, postId)
|
||||
dispatch(setAudio({ audios: formatAudios, postId: formPostId }))
|
||||
if (tempSaveAudio?.current?.currentSelection) {
|
||||
const findIndex = (formatAudios || []).findIndex(
|
||||
(item) =>
|
||||
item?.identifier ===
|
||||
tempSaveAudio?.current?.currentSelection?.content?.identifier
|
||||
)
|
||||
if (findIndex >= 0) {
|
||||
dispatch(setCurrAudio(findIndex))
|
||||
}
|
||||
}
|
||||
setisOpenSwitchPlaylistModal(false)
|
||||
}
|
||||
|
||||
const getAvatar = React.useCallback(async () => {
|
||||
try {
|
||||
let url = await qortalRequest({
|
||||
action: 'GET_QDN_RESOURCE_URL',
|
||||
name: user,
|
||||
service: 'THUMBNAIL',
|
||||
identifier: 'qortal_avatar'
|
||||
})
|
||||
|
||||
setAvatarUrl(url)
|
||||
} catch (error) {}
|
||||
}, [user])
|
||||
React.useEffect(() => {
|
||||
getAvatar()
|
||||
}, [])
|
||||
|
||||
const onBreakpointChange = React.useCallback((newBreakpoint: any) => {
|
||||
setCurrentBreakpoint(newBreakpoint)
|
||||
}, [])
|
||||
|
||||
const onResizeStop = React.useCallback((layout: any, layoutItem: any) => {
|
||||
// Update the layout state with the new position and size of the component
|
||||
setCount((prev) => prev + 1)
|
||||
}, [])
|
||||
|
||||
// const audios = React.useMemo<IPlaylist[]>(() => {
|
||||
// const filteredAudios = (blogContent?.postContent || []).filter(
|
||||
// (content) => content.type === 'audio'
|
||||
// )
|
||||
|
||||
// return filteredAudios.map((fa) => {
|
||||
// return {
|
||||
// ...fa.content,
|
||||
// id: fa.id
|
||||
// }
|
||||
// })
|
||||
// }, [blogContent])
|
||||
|
||||
const handleResize = () => {
|
||||
setCount((prev) => prev + 1)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleCount = React.useCallback(() => {
|
||||
// Update the layout state with the new position and size of the component
|
||||
setCount((prev) => prev + 1)
|
||||
}, [])
|
||||
|
||||
const getBlog = React.useCallback(async () => {
|
||||
let name = user
|
||||
if (!name) return
|
||||
if (!blogFull) return
|
||||
try {
|
||||
const urlBlog = `/arbitrary/BLOG/${name}/${blogFull}`
|
||||
const response = await fetch(urlBlog, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
dispatch(setVisitingBlog({ ...responseData, name }))
|
||||
} catch (error) {}
|
||||
}, [user, blogFull])
|
||||
|
||||
React.useEffect(() => {
|
||||
getBlog()
|
||||
}, [user, blogFull])
|
||||
|
||||
if (!blogContent) return null
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: '1400px',
|
||||
// margin: '15px',
|
||||
width: '95%',
|
||||
paddingBottom: '50px'
|
||||
}}
|
||||
>
|
||||
{user === userState?.name && (
|
||||
<Button
|
||||
sx={{ backgroundColor: theme.palette.secondary.main }}
|
||||
onClick={() => {
|
||||
navigate(`/${user}/${blog}/${postId}/edit`)
|
||||
}}
|
||||
>
|
||||
Edit Post
|
||||
</Button>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<CardHeader
|
||||
onClick={() => {
|
||||
navigate(`/${user}/${blog}`)
|
||||
}}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'& .MuiCardHeader-content': {
|
||||
overflow: 'hidden'
|
||||
},
|
||||
padding: '10px 0px'
|
||||
}}
|
||||
avatar={<Avatar src={avatarUrl} alt={`${user}'s avatar`} />}
|
||||
subheader={
|
||||
<Typography
|
||||
sx={{ fontFamily: 'Cairo', fontSize: '25px' }}
|
||||
color={theme.palette.text.primary}
|
||||
>{` ${user}`}</Typography>
|
||||
}
|
||||
/>
|
||||
{user && (
|
||||
<Tipping
|
||||
name={user || ''}
|
||||
onSubmit={() => {
|
||||
// setNameTip('')
|
||||
}}
|
||||
onClose={() => {
|
||||
// setNameTip('')
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
color="textPrimary"
|
||||
sx={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{blogContent?.title}
|
||||
</Typography>
|
||||
<Tooltip title={`Copy post link`} arrow>
|
||||
<Box
|
||||
sx={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<CopyToClipboard
|
||||
text={`qortal://APP/Q-Blog/${user}/${blog}/${postId}`}
|
||||
onCopy={() => {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: 'Copied to clipboard!',
|
||||
alertType: 'success'
|
||||
})
|
||||
)
|
||||
}}
|
||||
>
|
||||
<ShareIcon />
|
||||
</CopyToClipboard>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<CommentSection postId={fullPostId} postName={user || ''} />
|
||||
</Box>
|
||||
|
||||
{(layoutGeneralSettings?.blogPostType === 'builder' ||
|
||||
!layoutGeneralSettings?.blogPostType) && (
|
||||
<Content
|
||||
layouts={layouts}
|
||||
blogContent={blogContent}
|
||||
onResizeStop={onResizeStop}
|
||||
onBreakpointChange={onBreakpointChange}
|
||||
handleLayoutChange={handleLayoutChange}
|
||||
>
|
||||
{blogContent?.postContent?.map((section: any) => {
|
||||
if (section?.type === 'editor') {
|
||||
return (
|
||||
<div key={section?.id} className="grid-item-view">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItem
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={layoutGeneralSettings?.padding}
|
||||
>
|
||||
<ReadOnlySlate content={section.content} />
|
||||
</DynamicHeightItem>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (section?.type === 'image') {
|
||||
return (
|
||||
<div key={section?.id} className="grid-item-view">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItem
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={layoutGeneralSettings?.padding}
|
||||
>
|
||||
<img
|
||||
src={section.content.image}
|
||||
className="post-image"
|
||||
/>
|
||||
</DynamicHeightItem>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (section?.type === 'video') {
|
||||
return (
|
||||
<div key={section?.id} className="grid-item-view">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItem
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={layoutGeneralSettings?.padding}
|
||||
>
|
||||
<ContextMenuResource
|
||||
name={section.content.name}
|
||||
service={section.content.service}
|
||||
identifier={section.content.identifier}
|
||||
link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
|
||||
>
|
||||
<VideoPlayer
|
||||
name={section.content.name}
|
||||
service={section.content.service}
|
||||
identifier={section.content.identifier}
|
||||
poster={section.content.poster}
|
||||
setCount={handleCount}
|
||||
user={user}
|
||||
postId={fullPostId}
|
||||
/>
|
||||
</ContextMenuResource>
|
||||
</DynamicHeightItem>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (section?.type === 'audio') {
|
||||
return (
|
||||
<div key={section?.id} className="grid-item-view">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItem
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={layoutGeneralSettings?.padding}
|
||||
>
|
||||
<ContextMenuResource
|
||||
name={section.content.name}
|
||||
service={section.content.service}
|
||||
identifier={section.content.identifier}
|
||||
link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
|
||||
>
|
||||
<AudioElement
|
||||
key={section.id}
|
||||
audioInfo={section.content}
|
||||
postId={fullPostId}
|
||||
user={user ? user : ''}
|
||||
onClick={() => {
|
||||
if (!blog || !postId) return
|
||||
|
||||
const formBlogId = addPrefix(blog)
|
||||
const formPostId =
|
||||
buildIdentifierFromCreateTitleIdAndId(
|
||||
formBlogId,
|
||||
postId
|
||||
)
|
||||
if (audioPostId && formPostId !== audioPostId) {
|
||||
tempSaveAudio.current = {
|
||||
...(tempSaveAudio.current || {}),
|
||||
currentSelection: section,
|
||||
message:
|
||||
'You are current on a playlist. Would you like to switch?'
|
||||
}
|
||||
setisOpenSwitchPlaylistModal(true)
|
||||
} else {
|
||||
if (!audios && saveAudio?.current) {
|
||||
const findIndex = (
|
||||
saveAudio?.current?.audios || []
|
||||
).findIndex(
|
||||
(item: any) =>
|
||||
item.identifier ===
|
||||
section.content.identifier
|
||||
)
|
||||
dispatch(setAudio(saveAudio?.current))
|
||||
dispatch(setCurrAudio(findIndex))
|
||||
return
|
||||
}
|
||||
|
||||
const findIndex = (audios || []).findIndex(
|
||||
(item) =>
|
||||
item.identifier ===
|
||||
section.content.identifier
|
||||
)
|
||||
if (findIndex >= 0) {
|
||||
dispatch(setCurrAudio(findIndex))
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={section.content?.title}
|
||||
description={section.content?.description}
|
||||
author=""
|
||||
/>
|
||||
</ContextMenuResource>
|
||||
</DynamicHeightItem>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (section?.type === 'file') {
|
||||
return (
|
||||
<div key={section?.id} className="grid-item">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItemMinimal
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={0}
|
||||
>
|
||||
<ContextMenuResource
|
||||
name={section.content.name}
|
||||
service={section.content.service}
|
||||
identifier={section.content.identifier}
|
||||
link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
|
||||
>
|
||||
<FileElement
|
||||
key={section.id}
|
||||
fileInfo={section.content}
|
||||
postId={fullPostId}
|
||||
user={user ? user : ''}
|
||||
title={section.content?.title}
|
||||
description={section.content?.description}
|
||||
mimeType={section.content?.mimeType}
|
||||
author=""
|
||||
/>
|
||||
</ContextMenuResource>
|
||||
</DynamicHeightItemMinimal>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Content>
|
||||
)}
|
||||
{layoutGeneralSettings?.blogPostType === 'minimal' && (
|
||||
<>
|
||||
{layouts?.rows?.map((row: any, rowIndex: number) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: '25px',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
{row?.ids?.map((elementId: string) => {
|
||||
const section: any = blogContent?.postContent?.find(
|
||||
(el) => el?.id === elementId
|
||||
)
|
||||
if (!section) return null
|
||||
if (section?.type === 'editor') {
|
||||
return (
|
||||
<div
|
||||
key={section?.id}
|
||||
className="grid-item"
|
||||
style={{
|
||||
maxWidth: '800px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItemMinimal
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={0}
|
||||
>
|
||||
<ReadOnlySlate
|
||||
key={section.id}
|
||||
content={section.content}
|
||||
/>
|
||||
</DynamicHeightItemMinimal>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (section?.type === 'image') {
|
||||
return (
|
||||
<div key={section.id} className="grid-item">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItemMinimal
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
type="image"
|
||||
padding={0}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={section.content.image}
|
||||
className="post-image"
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
maxHeight: '50vh'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</DynamicHeightItemMinimal>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (section?.type === 'video') {
|
||||
return (
|
||||
<div key={section?.id} className="grid-item">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItemMinimal
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={0}
|
||||
>
|
||||
<ContextMenuResource
|
||||
name={section.content.name}
|
||||
service={section.content.service}
|
||||
identifier={section.content.identifier}
|
||||
link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<VideoPlayer
|
||||
name={section.content.name}
|
||||
service={section.content.service}
|
||||
identifier={section.content.identifier}
|
||||
poster={section.content.poster}
|
||||
customStyle={{
|
||||
height: '50vh'
|
||||
}}
|
||||
user={user}
|
||||
postId={fullPostId}
|
||||
/>
|
||||
</Box>
|
||||
</ContextMenuResource>
|
||||
</DynamicHeightItemMinimal>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (section?.type === 'audio') {
|
||||
return (
|
||||
<div key={section?.id} className="grid-item">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItemMinimal
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={0}
|
||||
>
|
||||
<ContextMenuResource
|
||||
name={section.content.name}
|
||||
service={section.content.service}
|
||||
identifier={section.content.identifier}
|
||||
link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
|
||||
>
|
||||
<AudioElement
|
||||
key={section.id}
|
||||
audioInfo={section.content}
|
||||
postId={fullPostId}
|
||||
user={user ? user : ''}
|
||||
onClick={() => {
|
||||
if (!blog || !postId) return
|
||||
const formBlogId = addPrefix(blog)
|
||||
const formPostId =
|
||||
buildIdentifierFromCreateTitleIdAndId(
|
||||
formBlogId,
|
||||
postId
|
||||
)
|
||||
if (formPostId !== audioPostId) {
|
||||
tempSaveAudio.current = {
|
||||
...(tempSaveAudio.current || {}),
|
||||
currentSelection: section,
|
||||
message:
|
||||
'You are current on a playlist. Would you like to switch?'
|
||||
}
|
||||
setisOpenSwitchPlaylistModal(true)
|
||||
} else {
|
||||
const findIndex = (
|
||||
audios || []
|
||||
).findIndex(
|
||||
(item) =>
|
||||
item.identifier ===
|
||||
section.content.identifier
|
||||
)
|
||||
if (findIndex >= 0) {
|
||||
dispatch(setCurrAudio(findIndex))
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={section.content?.title}
|
||||
description={section.content?.description}
|
||||
author=""
|
||||
/>
|
||||
</ContextMenuResource>
|
||||
</DynamicHeightItemMinimal>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (section?.type === 'file') {
|
||||
return (
|
||||
<div key={section?.id} className="grid-item">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<DynamicHeightItemMinimal
|
||||
layouts={layouts}
|
||||
setLayouts={setLayouts}
|
||||
i={section.id}
|
||||
breakpoint={currentBreakpoint}
|
||||
count={count}
|
||||
padding={0}
|
||||
>
|
||||
<ContextMenuResource
|
||||
name={section.content.name}
|
||||
service={section.content.service}
|
||||
identifier={section.content.identifier}
|
||||
link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
|
||||
>
|
||||
<FileElement
|
||||
key={section.id}
|
||||
fileInfo={section.content}
|
||||
postId={fullPostId}
|
||||
user={user ? user : ''}
|
||||
title={section.content?.title}
|
||||
description={section.content?.description}
|
||||
mimeType={section.content?.mimeType}
|
||||
author=""
|
||||
/>
|
||||
</ContextMenuResource>
|
||||
</DynamicHeightItemMinimal>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<ReusableModal open={isOpenSwitchPlaylistModal}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
{tempSaveAudio?.current?.message
|
||||
? tempSaveAudio?.current?.message
|
||||
: 'You are current on a playlist. Would you like to switch?'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setisOpenSwitchPlaylistModal(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" onClick={switchPlayList}>
|
||||
Switch
|
||||
</Button>
|
||||
</ReusableModal>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const Content = ({
|
||||
children,
|
||||
layouts,
|
||||
blogContent,
|
||||
onResizeStop,
|
||||
onBreakpointChange,
|
||||
handleLayoutChange
|
||||
}: any) => {
|
||||
if (layouts && blogContent?.layouts) {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>Error loading content: Invalid Layout</Typography>
|
||||
}
|
||||
>
|
||||
<ResponsiveGridLayout
|
||||
layouts={layouts}
|
||||
breakpoints={{ md: 996, sm: 768, xs: 480 }}
|
||||
cols={{ md: 4, sm: 3, xs: 1 }}
|
||||
measureBeforeMount={false}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
autoSize={true}
|
||||
compactType={null}
|
||||
isBounded={true}
|
||||
resizeHandles={['se', 'sw', 'ne', 'nw']}
|
||||
rowHeight={25}
|
||||
onResizeStop={onResizeStop}
|
||||
onBreakpointChange={onBreakpointChange}
|
||||
isDraggable={false}
|
||||
isResizable={false}
|
||||
margin={[0, 0]}
|
||||
>
|
||||
{children}
|
||||
</ResponsiveGridLayout>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
return children
|
||||
}
|
1586
CreatePostBuilder.tsx
Normal file
1586
CreatePostBuilder.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1565
CreatePostMinimal.tsx
Normal file
1565
CreatePostMinimal.tsx
Normal file
File diff suppressed because it is too large
Load Diff
832
VideoPlayer.tsx
Normal file
832
VideoPlayer.tsx
Normal file
@@ -0,0 +1,832 @@
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Box, IconButton, Slider } from '@mui/material'
|
||||
import { CircularProgress, Typography } from '@mui/material'
|
||||
import { Key } from 'ts-key-enum'
|
||||
import {
|
||||
PlayArrow,
|
||||
Pause,
|
||||
VolumeUp,
|
||||
Fullscreen,
|
||||
PictureInPicture, VolumeOff
|
||||
} from '@mui/icons-material'
|
||||
import { styled } from '@mui/system'
|
||||
import { MyContext } from '../../wrappers/DownloadWrapper'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../state/store'
|
||||
import { Refresh } from '@mui/icons-material'
|
||||
|
||||
import { Menu, MenuItem } from '@mui/material'
|
||||
import { MoreVert as MoreIcon } from '@mui/icons-material'
|
||||
const VideoContainer = styled(Box)`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
`
|
||||
|
||||
const VideoElement = styled('video')`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: rgb(33, 33, 33);
|
||||
`
|
||||
|
||||
const ControlsContainer = styled(Box)`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
`
|
||||
|
||||
interface VideoPlayerProps {
|
||||
src?: string
|
||||
poster?: string
|
||||
name?: string
|
||||
identifier?: string
|
||||
service?: string
|
||||
autoplay?: boolean
|
||||
from?: string | null
|
||||
setCount?: () => void
|
||||
customStyle?: any
|
||||
user?: string
|
||||
postId?: string
|
||||
}
|
||||
|
||||
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
poster,
|
||||
name,
|
||||
identifier,
|
||||
service,
|
||||
autoplay = true,
|
||||
from = null,
|
||||
setCount,
|
||||
customStyle = {},
|
||||
user = '',
|
||||
postId = ''
|
||||
}) => {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [volume, setVolume] = useState(1)
|
||||
const [mutedVolume, setMutedVolume] = useState(1)
|
||||
const [isMuted, setIsMuted] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [canPlay, setCanPlay] = useState(false)
|
||||
const [startPlay, setStartPlay] = useState(false)
|
||||
const [isMobileView, setIsMobileView] = useState(false)
|
||||
const [playbackRate, setPlaybackRate] = useState(1)
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [consoleLog, setConsoleLog] = useState('Console Log Here')
|
||||
const [debug, setDebug] = useState(false)
|
||||
|
||||
const reDownload = useRef<boolean>(false)
|
||||
const { downloads } = useSelector((state: RootState) => state.global)
|
||||
const download = useMemo(() => {
|
||||
if (!downloads || !identifier) return {}
|
||||
const findDownload = downloads[identifier]
|
||||
|
||||
if (!findDownload) return {}
|
||||
return findDownload
|
||||
}, [downloads, identifier])
|
||||
|
||||
const src = useMemo(() => {
|
||||
return download?.url || ''
|
||||
}, [download?.url])
|
||||
const resourceStatus = useMemo(() => {
|
||||
return download?.status || {}
|
||||
}, [download])
|
||||
|
||||
const minSpeed = 0.25;
|
||||
const maxSpeed = 4.0;
|
||||
const speedChange = 0.25;
|
||||
|
||||
const updatePlaybackRate = (newSpeed: number) => {
|
||||
if(videoRef.current) {
|
||||
if(newSpeed > maxSpeed || newSpeed < minSpeed)
|
||||
newSpeed = minSpeed
|
||||
videoRef.current.playbackRate = newSpeed
|
||||
setPlaybackRate(newSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
const increaseSpeed = (wrapOverflow = true) => {
|
||||
const changedSpeed = playbackRate + speedChange
|
||||
let newSpeed = wrapOverflow ? changedSpeed: Math.min(changedSpeed, maxSpeed)
|
||||
|
||||
|
||||
if (videoRef.current) {
|
||||
updatePlaybackRate(newSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
const decreaseSpeed = () => {
|
||||
if (videoRef.current) {
|
||||
updatePlaybackRate(playbackRate - speedChange);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const toggleRef = useRef<any>(null)
|
||||
const { downloadVideo } = useContext(MyContext)
|
||||
const togglePlay = async () => {
|
||||
if (!videoRef.current) return
|
||||
setStartPlay(true)
|
||||
if (!src) {
|
||||
const el = document.getElementById('videoWrapper')
|
||||
if (el) {
|
||||
el?.parentElement?.removeChild(el)
|
||||
}
|
||||
ReactDOM.flushSync(() => {
|
||||
setIsLoading(true)
|
||||
})
|
||||
getSrc()
|
||||
}
|
||||
if (playing) {
|
||||
videoRef.current.pause()
|
||||
} else {
|
||||
videoRef.current.play()
|
||||
}
|
||||
setPlaying(!playing)
|
||||
}
|
||||
|
||||
const onVolumeChange = (_: any, value: number | number[]) => {
|
||||
if (!videoRef.current) return
|
||||
videoRef.current.volume = value as number
|
||||
setVolume(value as number)
|
||||
setIsMuted(false)
|
||||
}
|
||||
|
||||
const onProgressChange = (_: any, value: number | number[]) => {
|
||||
if (!videoRef.current) return
|
||||
videoRef.current.currentTime = value as number
|
||||
setProgress(value as number)
|
||||
if (!playing) {
|
||||
videoRef.current.play()
|
||||
setPlaying(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnded = () => {
|
||||
setPlaying(false)
|
||||
}
|
||||
|
||||
const updateProgress = () => {
|
||||
if (!videoRef.current) return
|
||||
setProgress(videoRef.current.currentTime)
|
||||
}
|
||||
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
|
||||
const enterFullscreen = () => {
|
||||
if (!videoRef.current) return
|
||||
if (videoRef.current.requestFullscreen) {
|
||||
videoRef.current.requestFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen ? exitFullscreen(): enterFullscreen()
|
||||
}
|
||||
const togglePictureInPicture = async () => {
|
||||
if (!videoRef.current) return
|
||||
if (document.pictureInPictureElement === videoRef.current) {
|
||||
await document.exitPictureInPicture()
|
||||
} else {
|
||||
await videoRef.current.requestPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setIsFullscreen(!!document.fullscreenElement)
|
||||
}
|
||||
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleCanPlay = () => {
|
||||
if (setCount) {
|
||||
setCount()
|
||||
}
|
||||
setIsLoading(false)
|
||||
setCanPlay(true)
|
||||
}
|
||||
|
||||
const getSrc = React.useCallback(async () => {
|
||||
if (!name || !identifier || !service || !postId || !user) return
|
||||
try {
|
||||
downloadVideo({
|
||||
name,
|
||||
service,
|
||||
identifier,
|
||||
blogPost: {
|
||||
postId,
|
||||
user
|
||||
}
|
||||
})
|
||||
} catch (error) {}
|
||||
}, [identifier, name, service])
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current
|
||||
|
||||
const handleLeavePictureInPicture = async (event: any) => {
|
||||
const target = event?.target
|
||||
if (target) {
|
||||
target.pause()
|
||||
if (setPlaying) {
|
||||
setPlaying(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (videoElement) {
|
||||
videoElement.addEventListener(
|
||||
'leavepictureinpicture',
|
||||
handleLeavePictureInPicture
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (videoElement) {
|
||||
videoElement.removeEventListener(
|
||||
'leavepictureinpicture',
|
||||
handleLeavePictureInPicture
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current
|
||||
|
||||
const minimizeVideo = async () => {
|
||||
if (!videoElement) return
|
||||
const handleClose = () => {
|
||||
if (videoElement && videoElement.parentElement) {
|
||||
const el = document.getElementById('videoWrapper')
|
||||
if (el) {
|
||||
el?.parentElement?.removeChild(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
const createCloseButton = (): HTMLButtonElement => {
|
||||
const closeButton = document.createElement('button')
|
||||
closeButton.textContent = 'X'
|
||||
closeButton.style.position = 'absolute'
|
||||
closeButton.style.top = '0'
|
||||
closeButton.style.right = '0'
|
||||
closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.7)'
|
||||
closeButton.style.border = 'none'
|
||||
closeButton.style.fontWeight = 'bold'
|
||||
closeButton.style.fontSize = '1.2rem'
|
||||
closeButton.style.cursor = 'pointer'
|
||||
closeButton.style.padding = '2px 8px'
|
||||
closeButton.style.borderRadius = '0 0 0 4px'
|
||||
|
||||
closeButton.addEventListener('click', handleClose)
|
||||
|
||||
return closeButton
|
||||
}
|
||||
const buttonClose = createCloseButton()
|
||||
const videoWrapper = document.createElement('div')
|
||||
videoWrapper.id = 'videoWrapper'
|
||||
videoWrapper.style.position = 'fixed'
|
||||
videoWrapper.style.zIndex = '900000009'
|
||||
videoWrapper.style.bottom = '0px'
|
||||
videoWrapper.style.right = '0px'
|
||||
|
||||
videoElement.parentElement?.insertBefore(videoWrapper, videoElement)
|
||||
videoWrapper.appendChild(videoElement)
|
||||
|
||||
videoWrapper.appendChild(buttonClose)
|
||||
videoElement.controls = true
|
||||
videoElement.style.height = 'auto'
|
||||
videoElement.style.width = '300px'
|
||||
|
||||
document.body.appendChild(videoWrapper)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (videoElement) {
|
||||
if (videoElement && !videoElement.paused && !videoElement.ended) {
|
||||
minimizeVideo()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
seconds = Math.floor(seconds)
|
||||
let minutes: number | string = Math.floor(seconds / 60)
|
||||
let hours: number | string = Math.floor(minutes / 60)
|
||||
|
||||
let remainingSeconds: number | string = seconds % 60
|
||||
let remainingMinutes: number | string = minutes % 60
|
||||
|
||||
if (remainingSeconds < 10) {
|
||||
remainingSeconds = '0' + remainingSeconds
|
||||
}
|
||||
|
||||
if (remainingMinutes < 10) {
|
||||
remainingMinutes = '0' + remainingMinutes
|
||||
}
|
||||
|
||||
if(hours === 0){
|
||||
hours = ''
|
||||
}
|
||||
else
|
||||
{
|
||||
hours = hours + ':'
|
||||
}
|
||||
|
||||
return hours + remainingMinutes + ':' + remainingSeconds
|
||||
}
|
||||
|
||||
const reloadVideo = () => {
|
||||
if (!videoRef.current) return
|
||||
const currentTime = videoRef.current.currentTime
|
||||
videoRef.current.src = src
|
||||
videoRef.current.load()
|
||||
videoRef.current.currentTime = currentTime
|
||||
if (playing) {
|
||||
videoRef.current.play()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
resourceStatus?.status === 'DOWNLOADED' &&
|
||||
reDownload?.current === false
|
||||
) {
|
||||
getSrc()
|
||||
reDownload.current = true
|
||||
}
|
||||
}, [getSrc, resourceStatus])
|
||||
|
||||
const handleMenuOpen = (event: any) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const videoWidth = videoRef?.current?.offsetWidth
|
||||
if (videoWidth && videoWidth <= 600) {
|
||||
setIsMobileView(true)
|
||||
}
|
||||
}, [canPlay])
|
||||
|
||||
const getDownloadProgress = (current: number, total: number) => {
|
||||
const progress = current /total * 100;
|
||||
return Number.isNaN(progress) ? '': progress.toFixed(0)+'%'
|
||||
}
|
||||
const mute = () => {
|
||||
setIsMuted(true)
|
||||
setMutedVolume(volume)
|
||||
setVolume(0)
|
||||
if(videoRef.current) videoRef.current.volume = 0
|
||||
}
|
||||
const unMute = () => {
|
||||
setIsMuted(false)
|
||||
setVolume(mutedVolume)
|
||||
if(videoRef.current) videoRef.current.volume = mutedVolume
|
||||
}
|
||||
|
||||
const toggleMute = () => {
|
||||
isMuted ? unMute() : mute();
|
||||
}
|
||||
|
||||
const changeVolume = (volumeChange: number) =>
|
||||
{
|
||||
if(videoRef.current){
|
||||
const minVolume = 0;
|
||||
const maxVolume = 1;
|
||||
|
||||
|
||||
let newVolume = volumeChange + volume
|
||||
|
||||
newVolume = Math.max(newVolume, minVolume)
|
||||
newVolume = Math.min(newVolume, maxVolume)
|
||||
|
||||
setIsMuted(false)
|
||||
setMutedVolume(newVolume)
|
||||
videoRef.current.volume = newVolume
|
||||
setVolume(newVolume);
|
||||
}
|
||||
|
||||
}
|
||||
const setProgressRelative = (secondsChange: number) => {
|
||||
if(videoRef.current){
|
||||
const currentTime = videoRef.current?.currentTime
|
||||
const minTime = 0
|
||||
const maxTime = videoRef.current?.duration || 100
|
||||
|
||||
let newTime = currentTime + secondsChange;
|
||||
newTime = Math.max(newTime, minTime)
|
||||
newTime = Math.min(newTime, maxTime)
|
||||
videoRef.current.currentTime = newTime;
|
||||
setProgress(newTime);
|
||||
}
|
||||
}
|
||||
|
||||
const setProgressAbsolute = (videoPercent: number) => {
|
||||
if(videoRef.current){
|
||||
videoPercent = Math.min(videoPercent, 100)
|
||||
videoPercent = Math.max(videoPercent, 0)
|
||||
const finalTime = videoRef.current?.duration*videoPercent / 100
|
||||
videoRef.current.currentTime = finalTime
|
||||
setProgress(finalTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||
{
|
||||
e.preventDefault()
|
||||
//setConsoleLog(`Alt: ${e.altKey} Shift: ${e.shiftKey} Control: ${e.ctrlKey} Key: ${e.key}`)
|
||||
|
||||
switch(e.key) {
|
||||
case Key.Add: increaseSpeed(false); break;
|
||||
case '+': increaseSpeed(false); break;
|
||||
case '>': increaseSpeed(false); break;
|
||||
|
||||
case Key.Subtract: decreaseSpeed(); break;
|
||||
case '-': decreaseSpeed(); break;
|
||||
case '<': decreaseSpeed(); break;
|
||||
|
||||
case Key.ArrowLeft: {
|
||||
if(e.shiftKey) setProgressRelative(-300);
|
||||
else if(e.ctrlKey) setProgressRelative(-60);
|
||||
else if(e.altKey) setProgressRelative(-10);
|
||||
else setProgressRelative(-5);
|
||||
} break;
|
||||
|
||||
case Key.ArrowRight: {
|
||||
if(e.shiftKey) setProgressRelative(300);
|
||||
else if(e.ctrlKey) setProgressRelative(60);
|
||||
else if(e.altKey) setProgressRelative(10);
|
||||
else setProgressRelative(5);
|
||||
} break;
|
||||
|
||||
case Key.ArrowDown: changeVolume(-0.05) ; break;
|
||||
case Key.ArrowUp: changeVolume(0.05) ; break;
|
||||
}
|
||||
}
|
||||
|
||||
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||
{
|
||||
e.preventDefault()
|
||||
//setConsoleLog(`Alt: ${e.altKey} Shift: ${e.shiftKey} Control: ${e.ctrlKey} Key: ${e.key}`)
|
||||
|
||||
switch(e.key) {
|
||||
case ' ': togglePlay(); break;
|
||||
case 'm': toggleMute(); break;
|
||||
|
||||
case 'f': enterFullscreen(); break;
|
||||
case Key.Escape: exitFullscreen(); break;
|
||||
|
||||
case '0': setProgressAbsolute(0); break;
|
||||
case '1': setProgressAbsolute(10); break;
|
||||
case '2': setProgressAbsolute(20); break;
|
||||
case '3': setProgressAbsolute(30); break;
|
||||
case '4': setProgressAbsolute(40); break;
|
||||
case '5': setProgressAbsolute(50); break;
|
||||
case '6': setProgressAbsolute(60); break;
|
||||
case '7': setProgressAbsolute(70); break;
|
||||
case '8': setProgressAbsolute(80); break;
|
||||
case '9': setProgressAbsolute(90); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<VideoContainer
|
||||
tabIndex={0}
|
||||
onKeyUp={keyboardShortcutsUp}
|
||||
onKeyDown={keyboardShortcutsDown}
|
||||
style={{
|
||||
padding: from === 'create' ? '8px' : 0
|
||||
}}
|
||||
>
|
||||
{/* <Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '-30px',
|
||||
right: '-15px'
|
||||
}}
|
||||
>
|
||||
<CopyToClipboard
|
||||
text={`qortal://${service}/${name}/${identifier}`}
|
||||
onCopy={() => {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: 'Copied to clipboard!',
|
||||
alertType: 'success'
|
||||
})
|
||||
)
|
||||
}}
|
||||
>
|
||||
<LinkIcon
|
||||
sx={{
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
</Box> */}
|
||||
{isLoading && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={resourceStatus?.status === 'READY' ? '55px ' : 0}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
zIndex={4999}
|
||||
bgcolor="rgba(0, 0, 0, 0.6)"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
<CircularProgress color="secondary" />
|
||||
{resourceStatus && (
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
component="div"
|
||||
sx={{
|
||||
color: 'white',
|
||||
fontSize: '15px',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{resourceStatus?.status === 'REFETCHING' ? (
|
||||
<>
|
||||
<>
|
||||
{getDownloadProgress(resourceStatus?.localChunkCount,resourceStatus?.totalChunkCount)}
|
||||
</>
|
||||
|
||||
<> Refetching in 25 seconds</>
|
||||
</>
|
||||
) : resourceStatus?.status === 'DOWNLOADED' ? (
|
||||
<>Download Completed: building video...</>
|
||||
) : resourceStatus?.status !== 'READY' ? (
|
||||
<>
|
||||
{getDownloadProgress(resourceStatus?.localChunkCount,resourceStatus?.totalChunkCount)}
|
||||
|
||||
</>
|
||||
) : (
|
||||
<>Download Completed: fetching video...</>
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{((!src && !isLoading) || !startPlay) && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
zIndex={500}
|
||||
bgcolor={poster ? 'transparent' : 'rgba(0, 0, 0, 0.6)'}
|
||||
onClick={() => {
|
||||
if (from === 'create') return
|
||||
|
||||
togglePlay()
|
||||
}}
|
||||
sx={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<PlayArrow
|
||||
sx={{
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
color: 'white'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<VideoElement
|
||||
ref={videoRef}
|
||||
src={!startPlay ? '' : resourceStatus?.status === 'READY' ? src : ''}
|
||||
poster={poster}
|
||||
onTimeUpdate={updateProgress}
|
||||
autoPlay={autoplay}
|
||||
onClick={togglePlay}
|
||||
onEnded={handleEnded}
|
||||
// onLoadedMetadata={handleLoadedMetadata}
|
||||
onCanPlay={handleCanPlay}
|
||||
preload="metadata"
|
||||
style={{
|
||||
...customStyle
|
||||
}}
|
||||
/>
|
||||
|
||||
<ControlsContainer
|
||||
style={{
|
||||
bottom: from === 'create' ? '15px' : 0
|
||||
}}
|
||||
>
|
||||
{isMobileView && canPlay ? (
|
||||
<>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}}
|
||||
onClick={togglePlay}
|
||||
>
|
||||
{playing ? <Pause /> : <PlayArrow />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
marginLeft: '15px'
|
||||
}}
|
||||
onClick={reloadVideo}
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
<Slider
|
||||
value={progress}
|
||||
onChange={onProgressChange}
|
||||
min={0}
|
||||
max={videoRef.current?.duration || 100}
|
||||
sx={{ flexGrow: 1, mx: 2 }}
|
||||
/>
|
||||
<IconButton
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
onClick={handleMenuOpen}
|
||||
>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="simple-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
PaperProps={{
|
||||
style: {
|
||||
width: '250px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem>
|
||||
<VolumeUp />
|
||||
<Slider
|
||||
value={volume}
|
||||
onChange={onVolumeChange}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}/>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => increaseSpeed()}>
|
||||
<Typography
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
Speed: {playbackRate}x
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={togglePictureInPicture}>
|
||||
<PictureInPicture />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={toggleFullscreen}>
|
||||
<Fullscreen />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
) : canPlay ? (
|
||||
<>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}}
|
||||
onClick={togglePlay}
|
||||
>
|
||||
{playing ? <Pause /> : <PlayArrow />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
marginLeft: '15px'
|
||||
}}
|
||||
onClick={reloadVideo}
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
<Slider
|
||||
value={progress}
|
||||
onChange={onProgressChange}
|
||||
min={0}
|
||||
max={videoRef.current?.duration || 100}
|
||||
sx={{ flexGrow: 1, mx: 2 }}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '14px',
|
||||
marginRight: '5px',
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
visibility:
|
||||
!videoRef.current?.duration || !progress
|
||||
? 'hidden'
|
||||
: 'visible'
|
||||
}}
|
||||
>
|
||||
{progress && videoRef.current?.duration && formatTime(progress)}/
|
||||
{progress &&
|
||||
videoRef.current?.duration &&
|
||||
formatTime(videoRef.current?.duration)}
|
||||
</Typography>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
marginRight: '10px'
|
||||
}}
|
||||
onClick={toggleMute}
|
||||
>
|
||||
{isMuted ? <VolumeOff/>:<VolumeUp/>}
|
||||
</IconButton>
|
||||
<Slider
|
||||
value={volume}
|
||||
onChange={onVolumeChange}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: '14px',
|
||||
marginLeft: '5px'
|
||||
}}
|
||||
onClick={(e) => increaseSpeed()}
|
||||
>
|
||||
Speed: {playbackRate}x
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
marginLeft: '15px'
|
||||
}}
|
||||
ref={toggleRef}
|
||||
onClick={togglePictureInPicture}
|
||||
>
|
||||
<PictureInPicture />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}}
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
<Fullscreen />
|
||||
</IconButton>
|
||||
</>
|
||||
) : null}
|
||||
</ControlsContainer>
|
||||
{debug ? <span>{consoleLog}</span>: <></>}
|
||||
</VideoContainer>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user