@tycoonsystems/tycoon-modules
Version:
www.tv.tycoon.systems/documentation
355 lines (336 loc) • 24.3 kB
JavaScript
import React from 'react'
import styles from '@tycoonsystems/tycoon-modules/video/upload/upload.module.scss'
import WatchPageStyles from '@tycoonsystems/tycoon-modules/streaming/watch/WatchPage.module.scss'
import { SignIn, Username } from '@tycoonsystems/tycoon-modules/onboarding/signin'
import { Player } from '@tycoonsystems/tycoon-modules/streaming/watch'
import UploadVideoFileInternal from '@tycoonsystems/tycoon-modules/video/upload/UploadVideoFileInternal'
import { HMSDurationToSeconds, secondsToHMSString } from '@tycoonsystems/tycoon-modules/utility/utility/date'
import { resolveNestedProperty } from '@tycoonsystems/tycoon-modules/util'
import TextareaAutosize from 'react-textarea-autosize'
import Close from '@mui/icons-material/Close'
import dynamic from 'next/dynamic'
const VideoReel = dynamic(() => import('/layout/upload/VideoReel'), {
ssr: false, // Optional: Disable server-side rendering for the Help component
loading: () => <p></p>, // Optional: Provide a fallback UI
// You can also add an `onError` callback to handle errors:
// onError: (error) => console.error('Failed to load Help component:', error),
})
const Module = props => {
const { ASSOCIATE_RECORDS, ASSOCIATION_METHODS, handleClearError, setPageError, pageError, setProcessing, processing, handlingMeta, setHandlingMetaProxy, setVideoDocumentProxy, fetchBusy, useVideos, videosContainerRef, loadVideo, status, componentId, initialized, videoDocumentRasterized, clipStartRef, clipDescriptionRef, currentAssociation, currentAssociationMethod, associateRecords, setAssociateRecords, videoDocument, handlePublish, handleStartUpload, setVideoDocumentRasterized, setCurrentAssociationMethod, setCurrentAssociation, currentAssociationLimit, getAssociateAttributes, loadRecord, handleDisposePlayer } = props
const updateInput = React.useCallback(e => {
const modif = e?.target?.getAttribute('modif')
let value = e?.target?.value
if (modif && videoDocument) {
const instance = videoDocument.handleUsePayload(value, modif)
setVideoDocumentProxy(instance)
}
}, [ videoDocument, props?._loggedIn?.identifier ])
console.log(videoDocument)
const inputData = React.useMemo(() => {
return videoDocumentRasterized && videoDocument?.usePayload
? Object.entries(videoDocument.usePayload).map((m, i) => (
<div className='label_input' key={i}>
<div className='label_data'>
{
m[1]?.type
? m[1].type === 'string' && m[0] !== 'author'
? <React.Fragment>
<label>{m[0].toUpperCase ? `${m[0].charAt(0).toUpperCase()}${m[0].length > 1 ? m[0].substring(1, m[0].length) : ''}` : ''}</label>
<input className={`uploadPage_${m[0]} ${m[1].readonly ? 'input_readonly' : null}`} type='text' selectelement={`${componentId}-${m[0]}`} disabled={m[1].readonly} modif={m[0]} onChange={updateInput} />
</React.Fragment>
: m[1].type === 'text'
? <React.Fragment>
<label>{m[0].toUpperCase ? `${m[0].charAt(0).toUpperCase()}${m[0].length > 1 ? m[0].substring(1, m[0].length) : ''}` : ''}</label>
<TextareaAutosize className={`uploadPage_${m[0]} ${m[1].readonly ? 'input_readonly' : null}`} selectelement={`${componentId}-${m[0]}`} minRows={m[1]?.rows ?? 2} disabled={m[1].readonly} modif={m[0]} onChange={updateInput} />
</React.Fragment>
: m[1].type === 'array' && m[1].item === 'string'
? <React.Fragment>
<label>{m[0].toUpperCase ? `${m[0].charAt(0).toUpperCase()}${m[0].length > 1 ? m[0].substring(1, m[0].length) : ''}` : ''}</label>
<div className='label_data_container'>
<TextareaAutosize className={`uploadPage_${m[0]} ${m[1].readonly ? 'input_readonly' : null}`} selectelement={`${componentId}-${m[0]}`} minRows={m[1]?.rows ?? 2} disabled={m[1].readonly} modif={m[0]} onChange={updateInput} />
{
resolveNestedProperty(videoDocumentRasterized, m[1].path)?.map ?
<div className='tagContainer' style={{ marginTop: '.25rem' }}>
{
resolveNestedProperty(videoDocumentRasterized, m[1].path).map((d, i) => {
return (d !== '' ? <div className='tagItem' key={i}>{d}</div> : <div></div>)
})
}
</div>
: (<div></div>)
}
</div>
</React.Fragment>
: m[1].type === 'date'
? <React.Fragment>
<label>{m[0].toUpperCase ? `${m[0].charAt(0).toUpperCase()}${m[0].length > 1 ? m[0].substring(1, m[0].length) : ''}` : ''}</label>
<input type='date' className={`uploadPage_${m[0]} ${m[1].readonly ? 'input_readonly' : null}`} selectelement={`${componentId}-${m[0]}`} disabled={m[1].readonly} modif={m[0]} onChange={updateInput} />
</React.Fragment>
: null
: null
}
</div>
</div>
))
: null
}, [ videoDocumentRasterized, videoDocument ])
const handleDeleteClip = React.useCallback(e => {
if (e?.currentTarget?.getAttribute('identifier')) {
videoDocument.deleteClip(e.currentTarget.getAttribute('identifier'))
setVideoDocumentRasterized(videoDocument.getInstance())
}
})
const clipsRender = React.useMemo(() => {
return (
<div className={`${WatchPageStyles.clipsContainer} Upload_ClipsContainer`}>
{
videoDocumentRasterized?.timeline?.filter(m => m['type'] === 'clip')?.map((m, i) => (
<div className={`${WatchPageStyles.clipViewContainer} Upload_ClipViewContainer`} key={i}>
<div className={`${WatchPageStyles.clipViewStart} Upload_ClipViewStart`}>{secondsToHMSString(m.startOffset)}</div>
<div className={`${WatchPageStyles.clipViewNameContainer} Upload_ClipViewNameContainer`}>
<div className={`${WatchPageStyles.clipViewName} Upload_ClipViewName`}>{m.name}</div>
<Close className={'close'} onClick={handleDeleteClip} modif='close' identifier={m.id} style={{ cursor: 'pointer' }}></Close>
</div>
</div>
))
}
</div>
)
}, [ videoDocumentRasterized?.timeline, handleDeleteClip ])
const handleAddClip = React.useCallback(e => {
const seconds = HMSDurationToSeconds(clipStartRef?.current.value)
const description = clipDescriptionRef.current.value
if (!isNaN(seconds)) {
videoDocument.addClip(props, seconds, description, `https://${props.domainUrl}/w?u=${videoDocument.id}&time=${seconds}`)
clipStartRef.current.value = '00:00'
clipDescriptionRef.current.value = ''
setVideoDocumentRasterized(videoDocument.getInstance())
}
})
const handleSetClipTimeInput = React.useCallback(e => {
let r = HMSDurationToSeconds(e?.currentTarget.value)
let value = '00:00'
if (r !== null && e?.key && [ 'ArrowUp', 'ArrowDown' ].indexOf(e.key) > -1) {
if (e.key == 'ArrowUp') {
r += 1
} else if (e.key == 'ArrowDown') {
r -= 1
}
if (r > 0) {
const HMS = {
hours: Math.floor(r / 3600),
minutes: Math.floor((r % 3600) / 60),
seconds: r % 60
}
value = `${HMS.hours > 0 ? `${String(HMS.hours).padStart(2, '0')}:` : ''}${String(HMS.minutes).padStart(2, '0')}:${String(HMS.seconds).padStart(2, '0')}`
}
e.target.value = value
}
})
const handleSetCurrentAssociationOption = React.useCallback(e => {
if (e?.currentTarget?.value) {
const value = e.currentTarget.value
const limit = value === currentAssociation ? currentAssociationLimit + 10 : 10
setCurrentAssociation(value)
getAssociateAttributes(value, limit)
}
})
const handleLoadMoreAssociateRecords = React.useCallback(e => {
getAssociateAttributes(currentAssociation, currentAssociationLimit + 10)
})
const handleSetAuthorizationMethod = React.useCallback(e => {
if (e?.currentTarget?.getAttribute('modif')) {
const modif = e.currentTarget.getAttribute('modif')
setCurrentAssociationMethod(modif)
}
})
const handleAssociateRecord = React.useCallback(e => {
if (e?.currentTarget?.id) {
const id = e.currentTarget.id
const m = associateRecords.find(n => n.id === id)
const doSet = e?.currentTarget?.getAttribute('unsetrecord') === 'false' ? true : false
if (m) {
if (currentAssociationMethod === 'authorize') {
let r = videoDocument.setAuthorizedBy(m.id, currentAssociation, doSet)
setVideoDocumentProxy(r)
} else {
let r = videoDocument.setAssociation(m.id, currentAssociation, doSet, currentAssociationMethod)
setVideoDocumentProxy(r)
}
setAssociateRecords(associateRecords)
}
}
})
const checkAssociationSelected = (id, useMethod, ofType) => {
const p = useMethod === 'authorize' ? 'authBy' : 'associations'
const detail = p === 'authBy' ? 'authorize' : useMethod
if (videoDocument?.meta && videoDocument?.meta[p] && videoDocument.meta[p][ofType] && videoDocument.meta[p][ofType][id] && videoDocument.meta[p][ofType][id][detail]) {
return true
}
return false
}
// (props) handleDisposePlayer(playerId: String optional) // Will dispose player after it is removed from DOM
// (props) loadRecord(videoDocument: Video, useFields: Boolean, playerId: String optional) // Will instantiate player using "Player". Run in setTimeout after painting player to DOM
return (
<div className={`${styles.container} ${props.className} Upload_Container PagePadding`}>
<h3>Upload</h3>
{
!props?._loggedIn
? <SignIn {...props}></SignIn>
: props._loggedIn && !props._loggedIn?.username
? <Username { ...props}></Username>
: null
}
{
props?._loggedIn?.username
? <div>
{
pageError
? <div className='error' style={{ margin: '.25rem', marginBottom: '0' }} onClick={handleClearError}>{pageError.message}</div>
: null
}
{
!handlingMeta
? <div>
<UploadVideoFileInternal { ...props } setProcessing={setProcessing} processing={processing} handlingMeta={handlingMeta} setHandlingMeta={setHandlingMetaProxy} setVideoDocumentProxy={setVideoDocumentProxy} />
</div>
: null
}
<VideoReel { ...props } fetchBusy={fetchBusy} useVideos={useVideos} videosContainerRef={videosContainerRef} loadVideo={loadVideo} />
<div className={`${styles.videoMessageContainer} Video_MessageContainer`}>
<p className={`${styles.videoMessage} ${status?.message ? styles.videoMessageVisible : null}`}>{status.message}</p>
</div>
<div className={`${styles.transcodeProgressContainer} Video_TranscodeProgressContainer ${props?.showingTranscodeProgress ? `${styles.showingTranscodeProgress} Video_ShowingTranscodeProgress` : ''}`}>
<div style={{ width: `${props.uploadTranscodeProgress ? props.uploadTranscodeProgress * 100 : 0}%` }} className={`${styles.transcodeProgress} Video_TranscodeProgress`}></div>
</div>
<div style={{ display: handlingMeta ? 'block' : 'none' }}>
<div className={`${styles.videoContainer} Video_VideoUploadContainer`} style={{ marginBottom: '1rem' }}>
<div className={`${WatchPageStyles.videoQuadrant} ${WatchPageStyles.videoQuadrantSimple} WatchPage_VideoQuadrant`} style={{ height: `calc(100vh - ${props?.menuHeight})` }}>
<Player { ...props } playerName={componentId ? `player-${componentId}` : null} playerInitialized={initialized} playerVisible={true} />
</div>
</div>
<div className={`${WatchPageStyles.uploadMetaContainer} Video_UploadMetaContainer`}>
<div className={`${WatchPageStyles.uploadMetaPrimaryContainer} Video_UploadMetaPrimaryContainer`}>
{inputData}
</div>
<div style={{ maxWidth: 'min-content' }}>
{
videoDocumentRasterized?.timeline?.map
? <div>
<div className='flex gap-p5' style={{ marginBottom: '.5rem' }}>
<div>
<h5 style={{ margin: 0, marginBottom: '.5rem' }}>Add a clip</h5>
<input className={`${WatchPageStyles.clipStart} Upload_ClipStart`} defaultValue={'00:00'} onKeyDown={handleSetClipTimeInput} ref={clipStartRef} />
</div>
<div>
<h5 style={{ margin: 0, marginBottom: '.5rem' }}>Describe this section of the video</h5>
<div className='flex gap-p5'>
<input className={`${WatchPageStyles.clipDescriptiveTitle} Upload_ClipDescriptiveTitle`} placeholder='Clip Description' type='text' ref={clipDescriptionRef} />
<button onClick={handleAddClip} style={{ textWrap: 'nowrap' }}>Add Clip</button>
</div>
</div>
</div>
{clipsRender}
</div>
: null
}
{
videoDocumentRasterized?.meta
? <div>
<div>
<div className='flex gap-p2' style={{ alignItems: 'center', margin: '0px 0px 0.5rem' }}>
<h5 style={{ margin: '0' }}>Associate with</h5>
<select style={{ borderRadius: '1rem' }} onChange={handleSetCurrentAssociationOption}>
{
ASSOCIATE_RECORDS.map((m, i) => {
return (<option value={m} className={`style_option ${currentAssociation === m ? 'option_currently_selected' : ''}`} key={i}>{m}</option>)})
}
</select>
</div>
<div className='flex gap-p2' style={{ marginBottom: '.5rem' }}>
{
ASSOCIATION_METHODS.map
? ASSOCIATION_METHODS.map((m, i) => (
<div onClick={handleSetAuthorizationMethod} modif={m} key={i}>
<button className={`${currentAssociationMethod === m ? `${WatchPageStyles.currentSelected} Upload_currentSelected` : ''}}`}>{`${m.charAt(0).toUpperCase()}${m.substring(1, m.length)}`}</button>
</div>
))
: null
}
</div>
</div>
<div>
{
associateRecords?.map
? <div>
<div className='flex gap-p5' style={{ flexWrap: 'wrap' }}>
{
associateRecords.map((m, i) => (
<div className={`item_container ${checkAssociationSelected(m.id, currentAssociationMethod, currentAssociation) ? `${WatchPageStyles.currentSelected} Upload_currentSelected` : ''}`} key={i} id={m.id} unsetrecord={`${checkAssociationSelected(m.id, currentAssociationMethod, currentAssociation)}`} onClick={handleAssociateRecord}>
<label style={{ cursor: 'pointer', fontSize: '.8rem' }}>{m?.title ?? m?.name ?? m.id}</label>
</div>
))
}
</div>
<button onClick={handleLoadMoreAssociateRecords}>Load more</button>
</div>
: null
}
</div>
</div>
: null
}
</div>
</div>
{
videoDocument?.status
? videoDocument.status === 'processing'
? <div>
<h4>Your video is processing</h4>
</div>
: [ 'ready', 'good' ].indexOf(videoDocument.status) > -1
? <div>
<h4>Your video is ready to be published</h4>
</div>
: videoDocument.status === 'published'
? <div>
<h4>Your video was published {new Date(Number(videoDocument.publish))?.toDateString() ?? ''}</h4>
</div>
: null
: null
}
<div>
<div className='flex gap-p5' style={{ paddingBottom: '.5rem' }}>
{
videoDocument?.status && [ 'ready', 'good' ].indexOf(videoDocument.status) > -1
? <button className='Video_PublishButton' onClick={handlePublish} modif='publish'>Publish</button>
: null
}
{
videoDocument
? <button className={`Video_UpdateButton ${Object.entries(videoDocument?.updatedFields).length > 0 ? `${WatchPageStyles.UploadChangesWaiting} Upload_UploadChangesWaiting` : ''}`} onClick={handlePublish} modif='update'>Update</button>
: null
}
{
videoDocument?.status === 'published'
? <React.Fragment>
{
videoDocumentRasterized?.meta?.private
? <button className='Video_UnprivateButton' onClick={handlePublish} modif='unprivate'>Unprivate</button>
: <button className='Video_PrivateButton' onClick={handlePublish} modif='private'>Make Private</button>
}
<button className='Video_UnpublishButton' onClick={handlePublish} modif='unpublish'>Unpublish</button>
</React.Fragment>
: null
}
<button className='Video_UploadButton' onClick={handleStartUpload}>Upload New</button>
</div>
</div>
</div>
</div>
: null
}
</div>
)
}
export default Module