2019-06-28 09:27:55 +02:00
// @flow
2020-08-05 19:19:15 +02:00
import type { Node } from 'react' ;
2019-09-27 20:56:15 +02:00
import * as ICONS from 'constants/icons' ;
2020-03-07 00:11:16 +01:00
import React , { useState , useEffect } from 'react' ;
2022-09-19 22:42:16 +02:00
import { ipcRenderer } from 'electron' ;
2021-10-08 05:47:39 +02:00
import { regexInvalidURI } from 'util/lbryURI' ;
2020-07-29 22:30:26 +02:00
import PostEditor from 'component/postEditor' ;
2019-06-28 09:27:55 +02:00
import FileSelector from 'component/common/file-selector' ;
2019-07-17 05:23:45 +02:00
import Button from 'component/button' ;
2019-09-27 20:56:15 +02:00
import Card from 'component/common/card' ;
2020-03-24 18:57:17 +01:00
import { FormField } from 'component/common/form' ;
2019-10-11 02:37:18 +02:00
import Spinner from 'component/spinner' ;
2020-07-01 17:35:05 +02:00
import I18nMessage from 'component/i18nMessage' ;
import usePersistedState from 'effects/use-persisted-state' ;
2020-07-28 01:12:59 +02:00
import * as PUBLISH _MODES from 'constants/publish_types' ;
2021-02-09 17:05:56 +01:00
import PublishName from 'component/publishName' ;
2022-11-17 19:20:01 +01:00
import path from 'path' ;
2019-06-28 09:27:55 +02:00
type Props = {
2020-07-28 01:12:59 +02:00
uri : ? string ,
mode : ? string ,
2019-06-28 09:27:55 +02:00
name : ? string ,
2020-07-28 01:12:59 +02:00
title : ? string ,
2022-09-02 18:43:35 +02:00
filePath : ? string ,
2020-07-29 04:56:07 +02:00
fileMimeType : ? string ,
2019-06-28 09:27:55 +02:00
isStillEditing : boolean ,
balance : number ,
updatePublishForm : ( { } ) => void ,
2019-09-27 20:56:15 +02:00
disabled : boolean ,
2019-10-11 02:37:18 +02:00
publishing : boolean ,
2021-02-16 03:33:34 +01:00
showToast : ( string ) => void ,
2019-12-26 16:37:26 +01:00
inProgress : boolean ,
clearPublish : ( ) => void ,
2020-03-24 18:57:17 +01:00
ffmpegStatus : any ,
optimize : boolean ,
2020-03-30 20:19:32 +02:00
size : number ,
duration : number ,
isVid : boolean ,
2021-04-14 06:06:11 +02:00
subtitle : string ,
2021-02-16 03:33:34 +01:00
setPublishMode : ( string ) => void ,
setPrevFileText : ( string ) => void ,
2020-08-05 19:19:15 +02:00
header : Node ,
2021-04-14 06:06:11 +02:00
channelId : string ,
setWaitForFile : ( boolean ) => void ,
2019-06-28 09:27:55 +02:00
} ;
function PublishFile ( props : Props ) {
2019-12-26 16:37:26 +01:00
const {
2020-07-28 01:12:59 +02:00
uri ,
mode ,
2019-12-26 16:37:26 +01:00
name ,
2020-07-28 01:12:59 +02:00
title ,
2019-12-26 16:37:26 +01:00
balance ,
filePath ,
2020-07-29 04:56:07 +02:00
fileMimeType ,
2019-12-26 16:37:26 +01:00
isStillEditing ,
updatePublishForm ,
disabled ,
publishing ,
inProgress ,
clearPublish ,
2020-03-24 18:57:17 +01:00
optimize ,
ffmpegStatus = { } ,
2020-03-30 20:19:32 +02:00
size ,
duration ,
isVid ,
2020-07-28 01:12:59 +02:00
setPublishMode ,
setPrevFileText ,
2020-08-05 19:06:24 +02:00
header ,
2021-04-14 06:06:11 +02:00
subtitle ,
2019-12-26 16:37:26 +01:00
} = props ;
2019-10-11 02:37:18 +02:00
2021-04-14 06:06:11 +02:00
const RECOMMENDED _BITRATE = 6000000 ;
const PROCESSING _MB _PER _SECOND = 0.5 ;
const MINUTES _THRESHOLD = 30 ;
const HOURS _THRESHOLD = MINUTES _THRESHOLD * 60 ;
const MARKDOWN _FILE _EXTENSIONS = [ 'txt' , 'md' , 'markdown' ] ;
const sizeInMB = Number ( size ) / 1000000 ;
const secondsToProcess = sizeInMB / PROCESSING _MB _PER _SECOND ;
2020-07-01 16:17:07 +02:00
const ffmpegAvail = ffmpegStatus . available ;
2022-09-02 18:43:35 +02:00
const currentFile = filePath ;
2020-07-28 01:12:59 +02:00
const [ currentFileType , setCurrentFileType ] = useState ( null ) ;
2020-07-01 17:35:05 +02:00
const [ optimizeAvail , setOptimizeAvail ] = useState ( false ) ;
const [ userOptimize , setUserOptimize ] = usePersistedState ( 'publish-file-user-optimize' , false ) ;
2020-03-07 00:11:16 +01:00
2020-07-28 01:12:59 +02:00
// Reset filePath if publish mode changed
useEffect ( ( ) => {
2020-07-29 22:30:26 +02:00
if ( mode === PUBLISH _MODES . POST ) {
2020-07-28 04:19:00 +02:00
if ( currentFileType !== 'text/markdown' && ! isStillEditing ) {
2020-07-30 00:55:48 +02:00
updatePublishForm ( { filePath : '' } ) ;
2020-07-28 01:12:59 +02:00
}
}
2020-07-28 04:19:00 +02:00
} , [ currentFileType , mode , isStillEditing , updatePublishForm ] ) ;
2020-07-28 01:12:59 +02:00
2022-09-19 22:42:16 +02:00
// Since the filePath can be updated from outside this component
// (for instance, when the user drags & drops a file), we need
// to check for changes in the selected file using an effect.
useEffect ( ( ) => {
if ( ! filePath ) {
return ;
}
2022-11-17 19:20:01 +01:00
async function readSelectedFileDetails ( ) {
2022-09-19 22:42:16 +02:00
// Read the file to get the file's duration (if possible)
// and offer transcoding it.
2022-11-17 19:20:01 +01:00
const result = await ipcRenderer . invoke ( 'get-file-details-from-path' , filePath ) ;
let file ;
if ( result . buffer ) {
file = new File ( [ result . buffer ] , result . name , {
type : result . mime ,
} ) ;
}
const fileData : FileData = {
path : result . path ,
name : result . name ,
mimeType : result . mime || 'application/octet-stream' ,
size : result . size ,
duration : result . duration ,
file : file ,
} ;
processSelectedFile ( fileData ) ;
2022-09-19 22:42:16 +02:00
}
2022-11-17 19:20:01 +01:00
readSelectedFileDetails ( ) ;
2022-09-19 22:42:16 +02:00
} , [ filePath ] ) ;
2020-07-01 17:35:05 +02:00
useEffect ( ( ) => {
const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail ;
const finalOptimizeState = isOptimizeAvail && userOptimize ;
setOptimizeAvail ( isOptimizeAvail ) ;
updatePublishForm ( { optimize : finalOptimizeState } ) ;
2020-07-28 01:12:59 +02:00
} , [ currentFile , filePath , isVid , ffmpegAvail , userOptimize , updatePublishForm ] ) ;
2020-07-01 17:35:05 +02:00
2020-07-01 16:17:07 +02:00
function updateFileInfo ( duration , size , isvid ) {
2020-03-30 20:19:32 +02:00
updatePublishForm ( { fileDur : duration , fileSize : size , fileVid : isvid } ) ;
}
2020-03-07 00:11:16 +01:00
function getBitrate ( size , duration ) {
const s = Number ( size ) ;
const d = Number ( duration ) ;
if ( s && d ) {
return ( s * 8 ) / d ;
} else {
return 0 ;
}
}
2020-03-24 18:57:17 +01:00
function getTimeForMB ( s ) {
if ( s < MINUTES _THRESHOLD ) {
return Math . floor ( secondsToProcess ) ;
} else if ( s >= MINUTES _THRESHOLD && s < HOURS _THRESHOLD ) {
return Math . floor ( secondsToProcess / 60 ) ;
} else {
return Math . floor ( secondsToProcess / 60 / 60 ) ;
}
}
function getUnitsForMB ( s ) {
if ( s < MINUTES _THRESHOLD ) {
2020-05-28 22:38:54 +02:00
if ( secondsToProcess > 1 ) return _ _ ( 'seconds' ) ;
2020-07-01 16:17:07 +02:00
return _ _ ( 'second' ) ;
2020-03-24 18:57:17 +01:00
} else if ( s >= MINUTES _THRESHOLD && s < HOURS _THRESHOLD ) {
2020-05-28 22:38:54 +02:00
if ( Math . floor ( secondsToProcess / 60 ) > 1 ) return _ _ ( 'minutes' ) ;
return _ _ ( 'minute' ) ;
2020-03-24 18:57:17 +01:00
} else {
2020-05-28 22:38:54 +02:00
if ( Math . floor ( secondsToProcess / 3600 ) > 1 ) return _ _ ( 'hours' ) ;
return _ _ ( 'hour' ) ;
2020-03-24 18:57:17 +01:00
}
}
2021-04-14 06:06:11 +02:00
function getUploadMessage ( ) {
2020-03-07 00:11:16 +01:00
if ( isVid && duration && getBitrate ( size , duration ) > RECOMMENDED _BITRATE ) {
return (
< p className = "help--warning" >
2020-05-12 20:57:02 +02:00
{ _ _ ( 'Your video has a bitrate over 5 Mbps. We suggest transcoding to provide viewers the best experience.' ) } { ' ' }
2020-07-23 19:02:07 +02:00
< Button button = "link" label = { _ _ ( 'Upload Guide' ) } href = "https://lbry.com/faq/video-publishing-guide" / >
2020-03-07 00:11:16 +01:00
< / p >
) ;
}
if ( isVid && ! duration ) {
return (
< p className = "help--warning" >
{ _ _ (
2020-05-12 20:57:02 +02:00
'Your video may not be the best format. Use MP4s in H264/AAC format and a friendly bitrate (under 5 Mbps) and resolution (720p) for more reliable streaming.'
2020-03-07 00:11:16 +01:00
) } { ' ' }
2020-07-23 19:02:07 +02:00
< Button button = "link" label = { _ _ ( 'Upload Guide' ) } href = "https://lbry.com/faq/video-publishing-guide" / >
2020-03-07 00:11:16 +01:00
< / p >
) ;
}
if ( ! ! isStillEditing && name ) {
return (
< p className = "help" >
{ _ _ ( "If you don't choose a file, the file from your existing claim %name% will be used" , { name : name } ) }
< / p >
) ;
}
if ( ! isStillEditing ) {
return (
< p className = "help" >
{ _ _ (
2020-05-12 20:57:02 +02:00
'For video content, use MP4s in H264/AAC format and a friendly bitrate (under 5 Mbps) and resolution (720p) for more reliable streaming.'
2020-03-07 00:11:16 +01:00
) } { ' ' }
2020-07-23 19:02:07 +02:00
< Button button = "link" label = { _ _ ( 'Upload Guide' ) } href = "https://lbry.com/faq/video-publishing-guide" / >
2020-03-07 00:11:16 +01:00
< / p >
) ;
}
}
2020-07-30 00:55:48 +02:00
function parseName ( newName ) {
let INVALID _URI _CHARS = new RegExp ( regexInvalidURI , 'gu' ) ;
return newName . replace ( INVALID _URI _CHARS , '-' ) ;
}
2020-07-30 06:03:56 +02:00
function handleTitleChange ( event ) {
const title = event . target . value ;
2020-07-30 00:55:48 +02:00
// Update title
2020-07-30 06:03:56 +02:00
updatePublishForm ( { title } ) ;
2020-07-30 00:55:48 +02:00
}
2020-08-11 04:08:03 +02:00
function handleFileReaderLoaded ( event : ProgressEvent ) {
// See: https://github.com/facebook/flow/issues/3470
if ( event . target instanceof FileReader ) {
const text = event . target . result ;
updatePublishForm ( { fileText : text } ) ;
setPublishMode ( PUBLISH _MODES . POST ) ;
}
}
2022-11-17 19:20:01 +01:00
function processSelectedFile ( fileData : FileData , clearName = true ) {
2020-03-07 00:11:16 +01:00
window . URL = window . URL || window . webkitURL ;
// select file, start to select a new one, then cancel
2022-11-17 19:20:01 +01:00
if ( ! fileData || fileData . error ) {
2021-04-23 18:57:51 +02:00
if ( isStillEditing || ! clearName ) {
2021-04-14 06:06:11 +02:00
updatePublishForm ( { filePath : '' } ) ;
} else {
updatePublishForm ( { filePath : '' , name : '' } ) ;
}
2020-03-07 00:11:16 +01:00
return ;
}
2020-05-25 16:27:36 +02:00
2022-09-02 18:43:35 +02:00
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
2022-11-17 19:20:01 +01:00
const file = fileData . file ;
// Check to see if it's a video and if mp4
const contentType = fileData . mimeType && fileData . mimeType . split ( '/' ) ; // get this from electron side
const duration = fileData . duration ;
const size = fileData . size ;
2020-05-28 16:45:56 +02:00
const isVideo = contentType && contentType [ 0 ] === 'video' ;
const isMp4 = contentType && contentType [ 1 ] === 'mp4' ;
2020-07-28 01:12:59 +02:00
2020-08-24 22:45:08 +02:00
let isTextPost = false ;
2020-07-28 01:12:59 +02:00
2020-08-24 22:45:08 +02:00
if ( contentType && contentType [ 0 ] === 'text' ) {
isTextPost = contentType [ 1 ] === 'plain' || contentType [ 1 ] === 'markdown' ;
2022-11-03 22:17:56 +01:00
setCurrentFileType ( contentType . join ( '/' ) ) ;
2022-11-17 19:20:01 +01:00
} else if ( path . parse ( fileData . path ) . ext ) {
2022-09-02 18:43:35 +02:00
// If user's machine is missing a valid content type registration
2020-07-28 01:12:59 +02:00
// for markdown content: text/markdown, file extension will be used instead
2022-11-17 19:20:01 +01:00
const extension = path . parse ( fileData . path ) . ext ;
2020-08-24 22:45:08 +02:00
isTextPost = MARKDOWN _FILE _EXTENSIONS . includes ( extension ) ;
2020-07-28 01:12:59 +02:00
}
2020-03-07 00:11:16 +01:00
if ( isVideo ) {
if ( isMp4 ) {
2022-11-17 19:20:01 +01:00
updateFileInfo ( duration || 0 , size , isVideo ) ;
2020-03-07 00:11:16 +01:00
} else {
2022-11-17 19:20:01 +01:00
updateFileInfo ( duration || 0 , size , isVideo ) ;
2020-03-07 00:11:16 +01:00
}
2020-07-14 12:32:08 +02:00
} else {
2022-11-17 19:20:01 +01:00
updateFileInfo ( 0 , size , isVideo ) ;
2020-03-07 00:11:16 +01:00
}
2019-10-11 02:37:18 +02:00
2022-11-17 19:20:01 +01:00
if ( isTextPost && file ) {
2020-07-28 01:12:59 +02:00
// Create reader
const reader = new FileReader ( ) ;
// Handler for file reader
2020-08-11 04:08:03 +02:00
reader . addEventListener ( 'load' , handleFileReaderLoaded ) ;
2020-07-28 01:12:59 +02:00
// Read file contents
reader . readAsText ( file ) ;
setCurrentFileType ( 'text/markdown' ) ;
} else {
setPublishMode ( PUBLISH _MODES . FILE ) ;
}
2022-09-19 22:42:16 +02:00
// Strip off extension and replace invalid characters
2019-12-14 20:35:55 +01:00
if ( ! isStillEditing ) {
2022-11-17 19:20:01 +01:00
const fileWithoutExtension = path . parse ( fileData . path ) . name ;
2022-09-19 22:42:16 +02:00
updatePublishForm ( { name : parseName ( fileWithoutExtension ) } ) ;
2019-12-14 20:35:55 +01:00
}
2022-09-19 22:42:16 +02:00
}
2020-07-30 06:03:56 +02:00
2022-09-19 22:42:16 +02:00
function handleFileChange ( fileWithPath : FileWithPath ) {
if ( fileWithPath ) {
updatePublishForm ( { filePath : fileWithPath . path } ) ;
}
2019-06-28 09:27:55 +02:00
}
2021-04-14 17:56:45 +02:00
const showFileUpload = mode === PUBLISH _MODES . FILE ;
2020-07-29 22:30:26 +02:00
const isPublishPost = mode === PUBLISH _MODES . POST ;
2020-07-28 01:12:59 +02:00
2019-06-28 09:27:55 +02:00
return (
2019-09-27 20:56:15 +02:00
< Card
2020-08-26 18:24:07 +02:00
className = { disabled || balance === 0 ? 'card--disabled' : '' }
2019-12-26 16:37:26 +01:00
title = {
2020-08-05 19:19:15 +02:00
< div >
2021-04-14 06:06:11 +02:00
{ header } { /* display mode buttons from parent */ }
2020-08-05 19:06:24 +02:00
{ publishing && < Spinner type = { 'small' } / > }
2020-08-05 19:19:15 +02:00
{ inProgress && (
< div >
2021-09-21 10:47:45 +02:00
< Button
button = "close"
label = { _ _ ( 'New --[clears Publish Form]--' ) }
icon = { ICONS . REFRESH }
onClick = { clearPublish }
/ >
2020-08-05 19:19:15 +02:00
< / div >
) }
< / div >
2019-12-26 16:37:26 +01:00
}
2021-04-14 06:06:11 +02:00
subtitle = { subtitle || ( isStillEditing && _ _ ( 'You are currently editing your upload.' ) ) }
2019-09-27 20:56:15 +02:00
actions = {
< React.Fragment >
2021-04-14 06:06:11 +02:00
< PublishName uri = { uri } / >
2020-03-24 18:57:17 +01:00
< FormField
2020-07-28 01:12:59 +02:00
type = "text"
name = "content_title"
label = { _ _ ( 'Title' ) }
placeholder = { _ _ ( 'Descriptive titles work best' ) }
disabled = { disabled }
value = { title }
2020-07-30 00:55:48 +02:00
onChange = { handleTitleChange }
2020-03-24 18:57:17 +01:00
/ >
2021-04-14 06:06:11 +02:00
{ showFileUpload && (
2021-10-19 06:49:51 +02:00
< >
< FileSelector
2022-06-28 21:49:41 +02:00
type = "openFile"
2021-10-19 06:49:51 +02:00
label = { _ _ ( 'File' ) }
disabled = { disabled }
currentPath = { currentFile }
onFileChosen = { handleFileChange }
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
placeholder = { _ _ ( 'Select file to upload' ) }
2022-09-19 22:42:16 +02:00
readFile = { false }
2021-10-19 06:49:51 +02:00
/ >
{ getUploadMessage ( ) }
< / >
2020-07-28 01:12:59 +02:00
) }
2021-04-14 06:06:11 +02:00
{ showFileUpload && (
2020-07-28 01:12:59 +02:00
< FormField
type = "checkbox"
checked = { userOptimize }
disabled = { ! optimizeAvail }
onChange = { ( ) => setUserOptimize ( ! userOptimize ) }
label = { _ _ ( 'Optimize and transcode video' ) }
name = "optimize"
/ >
) }
2021-04-14 06:06:11 +02:00
{ showFileUpload && ! ffmpegAvail && (
2020-03-24 18:57:17 +01:00
< p className = "help" >
< I18nMessage
tokens = { {
settings _link : < Button button = "link" navigate = "/$/settings" label = { _ _ ( 'Settings' ) } / > ,
} }
>
FFmpeg not configured . More in % settings _link % .
< / I18nMessage >
< / p >
) }
2021-04-14 06:06:11 +02:00
{ showFileUpload && Boolean ( size ) && ffmpegAvail && optimize && isVid && (
2020-03-24 18:57:17 +01:00
< p className = "help" >
< I18nMessage
tokens = { {
size : Math . ceil ( sizeInMB ) ,
processTime : getTimeForMB ( sizeInMB ) ,
units : getUnitsForMB ( sizeInMB ) ,
} }
>
2020-05-12 20:57:02 +02:00
Transcoding this % size % MB file should take under % processTime % % units % .
2020-03-24 18:57:17 +01:00
< / I18nMessage >
< / p >
) }
2021-04-14 06:06:11 +02:00
{ isPublishPost && (
< PostEditor
label = { _ _ ( 'Post --[noun, markdown post tab button]--' ) }
uri = { uri }
disabled = { disabled }
fileMimeType = { fileMimeType }
setPrevFileText = { setPrevFileText }
setCurrentFileType = { setCurrentFileType }
/ >
) }
2019-09-27 20:56:15 +02:00
< / React.Fragment >
}
/ >
2019-06-28 09:27:55 +02:00
) ;
}
export default PublishFile ;