mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 18:21:12 +00:00
Merge branch 'feature/mvp' of https://github.com/storytold/storyteller-frontend into feature/mvp
This commit is contained in:
commit
5ef7071979
32 files changed
+1276
-666
No files matched your search
@@ -87,6 +87,17 @@
|
||||
&:hover .card-video-play {
|
||||
color: rgba($white, 0.7);
|
||||
}
|
||||
|
||||
&.card-hover-border-red {
|
||||
&:hover {
|
||||
border: 1px solid $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom-text-hover {
|
||||
background-color: lighten($panel-inner, 2%);
|
||||
border: 1px solid $primary;
|
||||
}
|
||||
}
|
||||
|
||||
h6 {
|
||||
@@ -97,3 +108,14 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.card-bottom-text {
|
||||
padding-top: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.centered-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import "./Card.scss";
|
||||
|
||||
interface CardProps {
|
||||
@@ -8,6 +8,12 @@ interface CardProps {
|
||||
canHover?: boolean;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
backgroundImage?: string;
|
||||
height?: string;
|
||||
borderWidth?: string;
|
||||
hoverPrimaryColor?: true;
|
||||
aspectRatio?: string;
|
||||
bottomText?: string;
|
||||
}
|
||||
|
||||
export default function Card({
|
||||
@@ -17,17 +23,48 @@ export default function Card({
|
||||
canHover,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
backgroundImage,
|
||||
height,
|
||||
borderWidth,
|
||||
hoverPrimaryColor,
|
||||
aspectRatio = "auto",
|
||||
bottomText,
|
||||
}: CardProps) {
|
||||
const [textHovered, setTextHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`card ${padding ? "p-3" : ""} ${
|
||||
onClick || canHover ? "card-clickable" : ""
|
||||
}`}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={`card ${padding ? "p-3" : ""} ${
|
||||
onClick || canHover ? "card-clickable" : ""
|
||||
} ${hoverPrimaryColor ? "card-hover-border-red" : ""} ${
|
||||
textHovered ? "bottom-text-hover" : ""
|
||||
}`.trim()}
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
height: height || "auto",
|
||||
borderWidth: borderWidth || "1px",
|
||||
borderStyle: "solid",
|
||||
aspectRatio: aspectRatio || "auto",
|
||||
}}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{bottomText && (
|
||||
<h6
|
||||
className="card-bottom-text"
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setTextHovered(true)}
|
||||
onMouseLeave={() => setTextHovered(false)}
|
||||
>
|
||||
{bottomText}
|
||||
</h6>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import './styles.scss';
|
||||
|
||||
|
||||
export interface VideoFakeyouProps{
|
||||
debug?: boolean;
|
||||
width?: number|string;
|
||||
height?: number|string;
|
||||
hideIfNoMedia?: boolean;
|
||||
@@ -30,6 +31,7 @@ export interface VideoFakeyouProps{
|
||||
type Ref = HTMLVideoElement;
|
||||
|
||||
const VideoFakeyou = memo(forwardRef<Ref, VideoFakeyouProps>(({
|
||||
debug: propsDebug = false,
|
||||
width,
|
||||
height,
|
||||
hideIfNoMedia = false,
|
||||
@@ -44,11 +46,15 @@ const VideoFakeyou = memo(forwardRef<Ref, VideoFakeyouProps>(({
|
||||
...rest
|
||||
}: VideoFakeyouProps, ref) => {
|
||||
//console.log(`Video Player rerender: ${mediaToken}`);
|
||||
const debug = false;// || propsDebug;
|
||||
|
||||
if(debug) console.log("VideoFakeyou reRENDER!!");
|
||||
|
||||
const [mediaFile, setMediaFile] = useState<MediaFileType|null>(null);
|
||||
useEffect(()=>{
|
||||
setMediaFile(null);
|
||||
},[mediaToken])
|
||||
|
||||
useMedia({
|
||||
mediaToken: mediaToken,
|
||||
onSuccess: (res: any) => {
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, {
|
||||
useRef,
|
||||
useState,
|
||||
PointerEvent
|
||||
} from 'react';
|
||||
import {
|
||||
faGripDots
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { QuickTrimData } from './types';
|
||||
|
||||
export default function TrimScrubber({
|
||||
trimStart,
|
||||
trimDuration,
|
||||
duration,
|
||||
width,
|
||||
boundingWidth,
|
||||
onChange
|
||||
}:{
|
||||
trimStart:number;
|
||||
trimDuration: number;
|
||||
duration: number;
|
||||
boundingWidth: number;
|
||||
width:number;
|
||||
onChange:(val:QuickTrimData)=>void
|
||||
}){
|
||||
const [{
|
||||
left,
|
||||
leftOffset,
|
||||
pointerStart
|
||||
}, setState] = useState<{
|
||||
left: number,
|
||||
leftOffset:number
|
||||
pointerStart:number
|
||||
}>({
|
||||
left: trimStart/duration*boundingWidth,
|
||||
leftOffset: trimStart/duration*boundingWidth,
|
||||
pointerStart: -1
|
||||
});
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
if(ref.current){
|
||||
ref.current.style.cursor = pointerStart >= 0
|
||||
? 'grabbing' : 'grab';
|
||||
}
|
||||
|
||||
function handleScrubStart(e: PointerEvent<HTMLDivElement>){
|
||||
e.persist();
|
||||
// console.log(`start: ${pointerStart}` );
|
||||
setState((curr)=>({
|
||||
...curr,
|
||||
pointerStart: e.clientX
|
||||
}));
|
||||
}
|
||||
function handleScrubEnd(e: PointerEvent<HTMLDivElement>){
|
||||
e.persist();
|
||||
// console.log(`end` );
|
||||
setState((curr)=>({
|
||||
...curr,
|
||||
pointerStart: -1,
|
||||
leftOffset: left,
|
||||
}));
|
||||
//console.log(`${duration} * ${left} / ${boundingWidth} = ${duration * left / boundingWidth}`)
|
||||
onChange(({
|
||||
trimStartSeconds: duration * left / boundingWidth,
|
||||
trimEndSeconds: duration * left / boundingWidth + trimDuration,
|
||||
}))
|
||||
}
|
||||
function handleScrubMove(e: PointerEvent<HTMLDivElement>){
|
||||
e.persist();
|
||||
if(pointerStart >= 0 && pointerStart!==null && ref.current){
|
||||
// console.log(`${e.clientX} - ${pointerStart} = ${e.clientX-pointerStart}`)
|
||||
let newLeft = leftOffset + e.clientX - pointerStart;
|
||||
// console.log(`${newLeft} + ${boundingWidth * widthPercent/100} > ${boundingWidth}`)
|
||||
if (newLeft + width > boundingWidth) {
|
||||
newLeft = boundingWidth - width;
|
||||
}else if(newLeft < 0) {
|
||||
newLeft = 0;
|
||||
}
|
||||
setState((curr)=>({
|
||||
...curr,
|
||||
left: newLeft
|
||||
}));
|
||||
}
|
||||
}
|
||||
return(
|
||||
<div className="trim-scrubber"
|
||||
ref={ref}
|
||||
style={{
|
||||
width: width + "px",
|
||||
left: left,
|
||||
}}
|
||||
onPointerDown={handleScrubStart}
|
||||
onPointerUp={handleScrubEnd}
|
||||
onPointerLeave={handleScrubEnd}
|
||||
onPointerMove={handleScrubMove}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGripDots} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,296 +1,231 @@
|
||||
import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
useEffect,
|
||||
useReducer,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
faArrowsRepeat,
|
||||
faPlay,
|
||||
faPause,
|
||||
faVolume,
|
||||
faVolumeSlash,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, SelectionBubbles, VideoFakeyou } from "components/common";
|
||||
|
||||
import { useLocalize } from "hooks";
|
||||
import { VideoFakeyou } from "components/common";
|
||||
import { VideoFakeyouProps } from "../VideoFakeyou/VideoFakeyou";
|
||||
|
||||
import { QuickTrimData, TrimStates, PlaybarStates } from "./types";
|
||||
import { formatSecondsToHHMMSSCS } from "./helpers";
|
||||
import TrimScrbber from "./TrimScrubber";
|
||||
import {
|
||||
reducer,
|
||||
initialState,
|
||||
ACTION_TYPES,
|
||||
PLAYPUASE_STATES,
|
||||
STATE_STATUSES,
|
||||
} from "./reducer";
|
||||
|
||||
import { QuickTrimData, ONE_MS } from "./utilities";
|
||||
|
||||
import { ProgressBar } from "./components/ProgressBar";
|
||||
import { ControlBar } from "./components/ControlBar";
|
||||
import './styles.scss'
|
||||
|
||||
interface VideoQuickTrimProps extends VideoFakeyouProps {
|
||||
debug?: boolean
|
||||
onSelect: (values: QuickTrimData) => void;
|
||||
trimStartSeconds: number;
|
||||
trimEndSeconds: number;
|
||||
}
|
||||
|
||||
const trimOptions: { [key: string]: number } = {
|
||||
"3s": 3,
|
||||
"5s": 5,
|
||||
"10s": 10,
|
||||
"15s": 15,
|
||||
};
|
||||
|
||||
const initialTrimState: TrimStates = {
|
||||
canNotTrim: true,
|
||||
trimDuration: 0,
|
||||
trimReset: new Date(),
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
maxDuration: 0,
|
||||
};
|
||||
|
||||
const initialPlaybarState: PlaybarStates = {
|
||||
playbarWidth: 0,
|
||||
timeCursorOffset: 0,
|
||||
};
|
||||
|
||||
export default memo(function VideoQuickTrim({
|
||||
export const VideoQuickTrim = memo(({
|
||||
debug: propsDebug = false,
|
||||
onSelect,
|
||||
trimStartSeconds: propsTrimStartSeconds,
|
||||
trimEndSeconds : propsTrimEndSeconds,
|
||||
...rest
|
||||
}: VideoQuickTrimProps) {
|
||||
// console.log("VideoQuickTrim Rerender!!");
|
||||
}: VideoQuickTrimProps) => {
|
||||
const debug = true || propsDebug;
|
||||
|
||||
const { t } = useLocalize("VideoPlayerQuickTrim");
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const playbarRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [{
|
||||
playbarWidth,
|
||||
timeCursorOffset
|
||||
}, setPlaybarState] = useState<PlaybarStates>(initialPlaybarState);
|
||||
const [playpause, setPlaypause] = useState<'playing'|'paused'|'ended'>('paused');
|
||||
const [isMuted, setIsMuted] = useState<boolean>(false);
|
||||
const [isRepeatOn, setIsRepatOn] = useState<boolean>(true);
|
||||
const [{
|
||||
canNotTrim,
|
||||
trimReset,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
trimDuration,
|
||||
maxDuration
|
||||
}, setTrimState] = useState<TrimStates>(initialTrimState);
|
||||
const [compState, dispatchCompState] = useReducer(reducer, initialState);
|
||||
|
||||
|
||||
if(compState.status === STATE_STATUSES.LOAD_ORDER_ERROR){
|
||||
console.log(`${compState.errorMessage[compState.errorMessage.length -1]}`);
|
||||
}
|
||||
|
||||
const videoRefCallback = useCallback(node => {
|
||||
//console.log("videoRefCallback");
|
||||
function setPlaypause(newState: PLAYPUASE_STATES){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_PLAYPUASE,
|
||||
payload: { playpause: newState}
|
||||
})
|
||||
}
|
||||
if (node !== null) {
|
||||
// DOM node referenced by ref has changed and exists
|
||||
videoRef.current = node;
|
||||
|
||||
node.onloadedmetadata = ()=>{
|
||||
if (node.duration >= 3){
|
||||
setTrimState((curr)=>({
|
||||
...curr,
|
||||
canNotTrim: false,
|
||||
trimDuration: 3,
|
||||
trimEnd: 3,
|
||||
maxDuration: node.duration
|
||||
}));
|
||||
//TODO: this sbould be set in USEEFFECT
|
||||
//IFF USEEFFECT starts working again
|
||||
onSelect({
|
||||
trimStartSeconds: 0,
|
||||
trimEndSeconds: 3,
|
||||
})
|
||||
}
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.ON_LOADED_METADATA,
|
||||
payload:{ videoDuration: node.duration,}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
node.ontimeupdate = ()=>{
|
||||
if(isRepeatOn &&
|
||||
(node.currentTime >= trimEnd || node.currentTime <= trimStart)
|
||||
if(compState.trimStartSeconds !== undefined &&
|
||||
compState.trimEndSeconds !== undefined &&
|
||||
compState.playbarWidth !== undefined){
|
||||
// reset current time when on repeat
|
||||
if(compState.isRepeatOn &&
|
||||
(node.currentTime > compState.trimEndSeconds || node.currentTime < compState.trimStartSeconds)
|
||||
){
|
||||
node.currentTime = trimStart;
|
||||
node.currentTime = compState.trimStartSeconds + ONE_MS;
|
||||
// if (debug){
|
||||
// console.log(`Loop Playing trimStart@${compState.trimStartSeconds}`);
|
||||
// console.log(`Loop Playing currentTime@${node.currentTime}`);
|
||||
// }
|
||||
}
|
||||
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.MOVE_TIMECURSOR,
|
||||
payload: {
|
||||
timeCursorOffset: (node.currentTime / node.duration) * (compState.playbarWidth)
|
||||
}
|
||||
});
|
||||
}else{
|
||||
console.log('ontimeupdate failed');
|
||||
}
|
||||
setPlaybarState((curr)=>({
|
||||
...curr,
|
||||
timeCursorOffset: (node.currentTime / node.duration) * (playbarWidth-8)
|
||||
}));
|
||||
|
||||
};
|
||||
|
||||
node.onplay = ()=>{ setPlaypause("playing");};
|
||||
node.onpause = ()=>{setPlaypause("paused");};
|
||||
node.onended = ()=>{setPlaypause("ended");};
|
||||
node.onplay = ()=>setPlaypause(PLAYPUASE_STATES.PLAYING);
|
||||
node.onpause = ()=>setPlaypause(PLAYPUASE_STATES.PAUSED);
|
||||
node.onended = ()=>setPlaypause(PLAYPUASE_STATES.ENDED);
|
||||
|
||||
node.onprogress = ()=>{
|
||||
console.log(node.buffered);
|
||||
if(node.buffered.length > 0){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_VIDEO_LOAD_PROGRESS,
|
||||
payload: {videoLoadProgress: node.buffered}
|
||||
});
|
||||
}
|
||||
}
|
||||
} // else{} DOM node referenced by ref has been unmounted
|
||||
}, [playbarWidth, onSelect, isRepeatOn, trimStart, trimEnd]); //END videoRefCallback
|
||||
}, [
|
||||
compState.trimStartSeconds,
|
||||
compState.trimEndSeconds,
|
||||
compState.playbarWidth,
|
||||
compState.isRepeatOn
|
||||
]); //END videoRefCallback\
|
||||
|
||||
function handleWindowResize() {
|
||||
if (playbarRef.current !== null) {
|
||||
const newWidth = playbarRef.current.getBoundingClientRect().width;
|
||||
setPlaybarState(curr => ({
|
||||
playbarWidth: newWidth,
|
||||
timeCursorOffset:
|
||||
(curr.timeCursorOffset / (curr.playbarWidth - 8)) * (newWidth - 8),
|
||||
}));
|
||||
}
|
||||
}
|
||||
const playbarRefCallback = useCallback(node => {
|
||||
if (node !== null) {
|
||||
playbarRef.current = node;
|
||||
setPlaybarState(curr => ({
|
||||
...curr,
|
||||
playbarWidth: node.getBoundingClientRect().width,
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
useLayoutEffect(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleChangeTrimDuration = (selected: string) =>{
|
||||
if(!canNotTrim && maxDuration > 0 && trimOptions[selected] <= maxDuration){
|
||||
let newTrimStart = trimStart;
|
||||
let newTrimEnd = trimStart+ trimOptions[selected];
|
||||
if (newTrimEnd > maxDuration){
|
||||
newTrimEnd = maxDuration;
|
||||
newTrimStart = maxDuration - trimOptions[selected];
|
||||
useEffect(()=>{
|
||||
if( compState.trimStartSeconds
|
||||
&& compState.trimEndSeconds
|
||||
&& (
|
||||
compState.trimStartSeconds !== propsTrimStartSeconds
|
||||
|| compState.trimEndSeconds !== propsTrimEndSeconds
|
||||
)
|
||||
){
|
||||
onSelect({
|
||||
trimStartSeconds: compState.trimStartSeconds,
|
||||
trimEndSeconds: compState.trimEndSeconds,
|
||||
});
|
||||
}
|
||||
setTrimState((curr)=>({
|
||||
...curr,
|
||||
trimReset: new Date(),
|
||||
trimDuration: trimOptions[selected],
|
||||
trimStart: newTrimStart,
|
||||
trimEnd: newTrimEnd,
|
||||
}))
|
||||
onSelect({
|
||||
trimStartSeconds: newTrimStart,
|
||||
trimEndSeconds: newTrimEnd,
|
||||
});
|
||||
if (isRepeatOn
|
||||
&& videoRef.current
|
||||
&& videoRef.current.currentTime > newTrimEnd
|
||||
){
|
||||
videoRef.current.currentTime = newTrimStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [onSelect, compState.trimStartSeconds, compState.trimEndSeconds, propsTrimStartSeconds, propsTrimEndSeconds])
|
||||
|
||||
const handlePlaypause = ()=>{
|
||||
if (playpause === 'paused' || playpause === 'ended'){
|
||||
function videoCanPlay(){
|
||||
return (compState.playpause === PLAYPUASE_STATES.PAUSED
|
||||
|| compState.playpause === PLAYPUASE_STATES.ENDED
|
||||
|| compState.playpause === PLAYPUASE_STATES.READY
|
||||
);
|
||||
}
|
||||
function handlePlaypause(){
|
||||
if (videoCanPlay()){
|
||||
videoRef.current?.play();
|
||||
}else{
|
||||
}else if (compState.playpause === PLAYPUASE_STATES.PLAYING){
|
||||
videoRef.current?.pause();
|
||||
}else {
|
||||
console.log('Playpause is triggered while it is NOT_READY');
|
||||
}
|
||||
};
|
||||
|
||||
const trimScrubberWidth = videoRef.current && playbarWidth > 0
|
||||
? trimDuration > 0 && trimDuration < videoRef.current.duration
|
||||
? (trimDuration / videoRef.current.duration * playbarWidth)
|
||||
: playbarWidth
|
||||
: 0;
|
||||
|
||||
function disableRepeatOn(){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.TOGGLE_REPEAT,
|
||||
payload: {isRepeatOn: false}
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div className="fy-video-quicktrim">
|
||||
<div className="video-wrapper">
|
||||
<VideoFakeyou
|
||||
debug={false}
|
||||
height={500}
|
||||
controls={false}
|
||||
muted={isMuted}
|
||||
muted={compState.isMuted}
|
||||
ref={videoRefCallback}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
<div className="playpause-overlay" onClick={handlePlaypause}>
|
||||
{playpause === "paused" && (
|
||||
<FontAwesomeIcon
|
||||
className="playpause-icon"
|
||||
icon={faPlay}
|
||||
size="8x"
|
||||
/>
|
||||
)}
|
||||
{playpause === "playing" && (
|
||||
<FontAwesomeIcon
|
||||
className="playpause-icon"
|
||||
icon={faPause}
|
||||
size="8x"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{videoRef.current && canNotTrim && (
|
||||
{compState.playpause !== PLAYPUASE_STATES.NOT_READY &&
|
||||
<div className="playpause-overlay" onClick={handlePlaypause}>
|
||||
{videoCanPlay() && (
|
||||
<FontAwesomeIcon
|
||||
className="playpause-icon"
|
||||
icon={faPlay}
|
||||
size="8x"
|
||||
/>
|
||||
)}
|
||||
{compState.playpause === PLAYPUASE_STATES.PLAYING && (
|
||||
<FontAwesomeIcon
|
||||
className="playpause-icon"
|
||||
icon={faPause}
|
||||
size="8x"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{ compState.canNotTrim === true &&
|
||||
<div className="warning-too-short">
|
||||
<div className="background"></div>
|
||||
<p>Warning: Sorry Your Video is TOO Short</p>
|
||||
<h1>{t('error.videoTooShort')}</h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* END of Video Wrapper */}
|
||||
<div className="playbar" ref={playbarRefCallback}>
|
||||
<div className="playbar-bg" />
|
||||
{trimScrubberWidth > 0 &&
|
||||
playbarWidth > 0 &&
|
||||
maxDuration > 0 &&
|
||||
<TrimScrbber
|
||||
key={trimReset.toString()}
|
||||
boundingWidth={playbarWidth}
|
||||
width={trimScrubberWidth}
|
||||
trimStart={trimStart}
|
||||
trimDuration={trimDuration}
|
||||
duration={maxDuration}
|
||||
onChange={(val: QuickTrimData)=>{
|
||||
//console.log(val);
|
||||
setTrimState((curr)=>({
|
||||
...curr,
|
||||
trimStart: val.trimStartSeconds,
|
||||
trimEnd: val.trimEndSeconds
|
||||
}))
|
||||
onSelect({
|
||||
trimStartSeconds: val.trimStartSeconds,
|
||||
trimEndSeconds: val.trimEndSeconds,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<div className="playcursor" style={{left: timeCursorOffset+"px"}}/>
|
||||
</div> {/* END of Playbar */}
|
||||
|
||||
<div className="d-flex w-100 justify-content-between mt-3 flex-wrap">
|
||||
<div className="playpause-external d-flex align-items-center flex-wrap mb-2">
|
||||
<Button
|
||||
className="button-playpause"
|
||||
icon={playpause === "playing" ? faPause : faPlay}
|
||||
variant="secondary"
|
||||
onClick={handlePlaypause}
|
||||
/>
|
||||
<Button
|
||||
className="button-repeat"
|
||||
icon={faArrowsRepeat}
|
||||
variant={isRepeatOn ? "primary":"secondary"}
|
||||
onClick={()=>setIsRepatOn((curr)=>(!curr))}
|
||||
/>
|
||||
<Button
|
||||
className="button-mute"
|
||||
icon={isMuted ? faVolumeSlash : faVolume}
|
||||
variant="secondary"
|
||||
onClick={() => setIsMuted(curr => !curr)}
|
||||
/>
|
||||
<div className="playtime d-flex">
|
||||
<span >
|
||||
<p>
|
||||
{`${formatSecondsToHHMMSSCS(
|
||||
videoRef.current?.currentTime || 0
|
||||
)}`}
|
||||
</p>
|
||||
</span>
|
||||
<div>/</div>
|
||||
<span>
|
||||
<p>
|
||||
{`${formatSecondsToHHMMSSCS(videoRef.current?.duration || 0)}`}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SelectionBubbles
|
||||
options={Object.keys(trimOptions)}
|
||||
onSelect={handleChangeTrimDuration}
|
||||
selectedStyle="outline"
|
||||
/>
|
||||
</div>
|
||||
</div>{/* END of Video Wrapper */}
|
||||
<ProgressBar
|
||||
debug={debug}
|
||||
readyToMount={(compState.status === STATE_STATUSES.VIDEO_METADATA_LOADED)}
|
||||
timeCursorOffset={compState.timeCursorOffset ||0}
|
||||
trimStartSeconds={compState.trimStartSeconds ||0}
|
||||
trimDuration={compState.trimDuration ||0}
|
||||
playbarWidth={compState.playbarWidth ||0}
|
||||
scrubberWidth={compState.scrubberWidth ||0}
|
||||
videoBuffered={compState.videoLoadProgress || undefined}
|
||||
videoDuration={compState.videoDuration ||0}
|
||||
dispatchCompState={dispatchCompState}
|
||||
onPlayCursorChanged={(newPos: number)=>{
|
||||
if(videoRef.current !== null && videoRef.current.currentTime
|
||||
&& compState.playbarWidth && compState.videoDuration){
|
||||
const newTime = newPos / compState.playbarWidth * compState.videoDuration;
|
||||
if(compState.trimStartSeconds
|
||||
&& compState.trimEndSeconds
|
||||
&& (newTime < compState.trimStartSeconds
|
||||
|| newTime > compState.trimEndSeconds
|
||||
)){
|
||||
disableRepeatOn();
|
||||
}
|
||||
videoRef.current.currentTime = newTime;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ControlBar
|
||||
debug={debug}
|
||||
readyToMount={(compState.status === STATE_STATUSES.VIDEO_METADATA_LOADED)}
|
||||
videoCurrentTime={videoRef.current?.currentTime}
|
||||
videoDuration={videoRef.current?.duration}
|
||||
isMuted={compState.isMuted}
|
||||
isRepeatOn={compState.isRepeatOn}
|
||||
playpause={compState.playpause}
|
||||
handlePlaypause={handlePlaypause}
|
||||
dispatchCompState={dispatchCompState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { memo } from 'react'
|
||||
import {
|
||||
faArrowsRepeat,
|
||||
faPlay,
|
||||
faPause,
|
||||
faVolume,
|
||||
faVolumeSlash,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button, SelectionBubbles } from "components/common";
|
||||
|
||||
import {
|
||||
Action,
|
||||
ACTION_TYPES,
|
||||
PLAYPUASE_STATES
|
||||
} from '../reducer';
|
||||
import { TRIM_OPTIONS, formatSecondsToHHMMSSCS } from "../utilities";
|
||||
|
||||
export const ControlBar = memo(({
|
||||
debug: propsDebug = false,
|
||||
readyToMount,
|
||||
videoCurrentTime,
|
||||
videoDuration,
|
||||
isRepeatOn,
|
||||
isMuted,
|
||||
playpause,
|
||||
handlePlaypause,
|
||||
dispatchCompState
|
||||
}:{
|
||||
debug?:boolean;
|
||||
readyToMount: boolean;
|
||||
videoCurrentTime: number | undefined;
|
||||
videoDuration: number | undefined;
|
||||
isMuted: boolean,
|
||||
isRepeatOn: boolean;
|
||||
playpause: string;
|
||||
handlePlaypause: ()=>void;
|
||||
dispatchCompState: (action: Action) => void;
|
||||
})=>{
|
||||
const debug = false || propsDebug;
|
||||
if (debug) console.log("ControlBar reRENDERED!!");
|
||||
|
||||
function handleSetTrimDuration(selected: string){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_TRIM_DURATION,
|
||||
payload: {
|
||||
trimDurationString: selected
|
||||
}
|
||||
});
|
||||
};
|
||||
if(readyToMount){
|
||||
return(
|
||||
<div className="d-flex w-100 justify-content-between mt-3 flex-wrap">
|
||||
<div className="playpause-external d-flex align-items-center flex-wrap mb-2">
|
||||
<Button
|
||||
className="button-playpause"
|
||||
icon={ playpause === PLAYPUASE_STATES.PLAYING
|
||||
? faPause : faPlay
|
||||
}
|
||||
variant="secondary"
|
||||
onClick={handlePlaypause}
|
||||
/>
|
||||
<Button
|
||||
className="button-repeat"
|
||||
icon={faArrowsRepeat}
|
||||
variant={isRepeatOn ? "primary":"secondary"}
|
||||
onClick={()=>dispatchCompState({type:ACTION_TYPES.TOGGLE_REPEAT})}
|
||||
/>
|
||||
<Button
|
||||
className="button-mute"
|
||||
icon={isMuted ? faVolumeSlash : faVolume}
|
||||
variant="secondary"
|
||||
onClick={()=>dispatchCompState({type:ACTION_TYPES.TOGGLE_MUTE})}
|
||||
/>
|
||||
<div className="playtime d-flex">
|
||||
<span >
|
||||
<p>
|
||||
{`${formatSecondsToHHMMSSCS(
|
||||
videoCurrentTime || 0
|
||||
)}`}
|
||||
</p>
|
||||
</span>
|
||||
<div>/</div>
|
||||
<span>
|
||||
<p>
|
||||
{`${formatSecondsToHHMMSSCS(videoDuration || 0)}`}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SelectionBubbles
|
||||
options={Object.keys(TRIM_OPTIONS)}
|
||||
onSelect={handleSetTrimDuration}
|
||||
selectedStyle="outline"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}else{
|
||||
console.log('TODO: ControlBar should rendering loading state instead of null');
|
||||
return null;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { withScrubbing, withScrubbingPropsI } from './withScrubbing';
|
||||
|
||||
interface PlayCursorPropsI extends withScrubbingPropsI{
|
||||
onChanged: (posPercent: number)=>void;
|
||||
}
|
||||
|
||||
export const PlayCursor = ({
|
||||
onChanged,
|
||||
...rest
|
||||
}:PlayCursorPropsI)=>{
|
||||
const PlayCursorWithScrubbing = withScrubbing<withScrubbingPropsI>(()=>{
|
||||
return(
|
||||
<div className="playcursor" />
|
||||
);
|
||||
});
|
||||
|
||||
return(
|
||||
<PlayCursorWithScrubbing
|
||||
onScrubChanged={onChanged}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useRef
|
||||
} from 'react';
|
||||
import {
|
||||
Action,
|
||||
ACTION_TYPES,
|
||||
} from '../reducer';
|
||||
import { TrimScrubber } from './TrimScrubber';
|
||||
import { PlayCursor } from './PlayCursor';
|
||||
|
||||
export const ProgressBar = memo(({
|
||||
debug: propsDebug = false,
|
||||
readyToMount,
|
||||
timeCursorOffset,
|
||||
trimStartSeconds,
|
||||
trimDuration,
|
||||
playbarWidth,
|
||||
scrubberWidth,
|
||||
videoDuration,
|
||||
videoBuffered,
|
||||
onPlayCursorChanged,
|
||||
dispatchCompState
|
||||
}:{
|
||||
debug?: boolean;
|
||||
readyToMount: boolean;
|
||||
timeCursorOffset: number;
|
||||
trimStartSeconds: number;
|
||||
trimDuration: number;
|
||||
playbarWidth: number;
|
||||
scrubberWidth: number;
|
||||
videoDuration: number;
|
||||
videoBuffered: TimeRanges | undefined;
|
||||
onPlayCursorChanged: (newPos: number) => void;
|
||||
dispatchCompState: (action: Action) => void;
|
||||
})=>{
|
||||
const debug = false || propsDebug;
|
||||
|
||||
const playbarRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const playbarRefCallback = useCallback(node => {
|
||||
if (node !== null) {
|
||||
playbarRef.current = node;
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_PLAYBAR_LAYOUT,
|
||||
payload: {
|
||||
playbarWidth: node.getBoundingClientRect().width,
|
||||
}
|
||||
})
|
||||
}else{
|
||||
// console.log(node);
|
||||
}
|
||||
}, [dispatchCompState]);
|
||||
|
||||
const handleWindowResize = useCallback(()=> {
|
||||
if (playbarRef.current !== null && readyToMount){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_PLAYBAR_LAYOUT,
|
||||
payload: {
|
||||
playbarWidth: playbarRef.current.getBoundingClientRect().width,
|
||||
}
|
||||
});
|
||||
}
|
||||
},[dispatchCompState, readyToMount]);
|
||||
useLayoutEffect(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, [handleWindowResize]);
|
||||
|
||||
if(readyToMount){
|
||||
// console.log('progress bar rendering');
|
||||
return(
|
||||
<div className="playbar" ref={playbarRefCallback}>
|
||||
<div className="playbar-bg">
|
||||
{videoBuffered !== undefined &&
|
||||
<span className="loaded" style={{width: (videoBuffered.end(0) / videoDuration* 100) + "%"}} />
|
||||
}
|
||||
<span className="played" style={{width: timeCursorOffset+"px"}} />
|
||||
</div>
|
||||
<TrimScrubber
|
||||
debug={debug}
|
||||
boundingWidth={playbarWidth}
|
||||
scrubberWidth={scrubberWidth}
|
||||
trimStartSeconds={trimStartSeconds}
|
||||
trimDuration={trimDuration}
|
||||
videoDuration={videoDuration}
|
||||
onChange={(newPos: number)=>{
|
||||
const newTrimStartSeconds = newPos / (playbarWidth) * videoDuration;
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.MOVE_TRIM,
|
||||
payload: {
|
||||
trimStartSeconds: newTrimStartSeconds,
|
||||
trimEndSeconds: newTrimStartSeconds + trimDuration
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PlayCursor
|
||||
scrubPosition={timeCursorOffset}
|
||||
onChanged={onPlayCursorChanged}
|
||||
boundingWidth={playbarWidth}
|
||||
scrubberWidth={8}
|
||||
/>
|
||||
{/* END of Playbar */}</div>
|
||||
);
|
||||
}else{
|
||||
console.log('TODO: ProgressBar should rendering loading state instead of null');
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, {
|
||||
memo,
|
||||
} from 'react';
|
||||
import {
|
||||
faGripDots
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { withScrubbing, withScrubbingPropsI } from './withScrubbing';
|
||||
|
||||
interface TrimScrubberPropsI extends withScrubbingPropsI {
|
||||
trimStartSeconds:number;
|
||||
trimDuration: number;
|
||||
videoDuration: number;
|
||||
onChange:(newPos:number)=>void
|
||||
}
|
||||
export const TrimScrubber = memo(({
|
||||
debug: propsDebug = false,
|
||||
trimStartSeconds,
|
||||
trimDuration,
|
||||
videoDuration,
|
||||
onChange,
|
||||
...rest
|
||||
}:TrimScrubberPropsI)=>{
|
||||
const debug = false || propsDebug;
|
||||
|
||||
const TrimScrubberWithScrubbing = withScrubbing<TrimScrubberPropsI>(() => {
|
||||
return(
|
||||
<div className="trim-scrubber">
|
||||
<FontAwesomeIcon icon={faGripDots} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<TrimScrubberWithScrubbing
|
||||
debug={debug}
|
||||
styleOverride={{
|
||||
top: '-1rem',
|
||||
}}
|
||||
scrubPosition={trimStartSeconds/videoDuration*(rest.boundingWidth)}
|
||||
onScrubChanged={onChange}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import React,{
|
||||
memo,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
} from 'react';
|
||||
|
||||
export interface withScrubbingPropsI {
|
||||
debug?: boolean
|
||||
boundingWidth: number;
|
||||
scrubberWidth: number;
|
||||
scrubPosition?: number; // scrubber location as px
|
||||
styleOverride?: {[key: string]: string|number };
|
||||
onScrubChanged?: (newPos: number)=>void;
|
||||
//return scrubber location as px
|
||||
}
|
||||
|
||||
type withSrcubbingStates = {
|
||||
currLeftOffset: number,
|
||||
prevLeftOffset: number;
|
||||
pointerStartPos: number;
|
||||
}
|
||||
|
||||
export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.ComponentType<P>) => memo(({
|
||||
debug: propsDebug = false,
|
||||
boundingWidth,
|
||||
scrubberWidth,
|
||||
scrubPosition: propsLeftOffset = 0,
|
||||
styleOverride = {},
|
||||
onScrubChanged,
|
||||
...rest
|
||||
}: withScrubbingPropsI) => {
|
||||
const debug = false ;//|| propsDebug;
|
||||
if (debug) console.log(`withSCRUBBING reRENDERING!! `);
|
||||
|
||||
const refEl = useRef<HTMLDivElement| null>(null);
|
||||
const refListener = useRef<number>(Date.now());
|
||||
|
||||
const [{
|
||||
currLeftOffset, pointerStartPos,
|
||||
prevLeftOffset
|
||||
}, setStates] = useState<withSrcubbingStates>({
|
||||
currLeftOffset: propsLeftOffset,
|
||||
prevLeftOffset: propsLeftOffset,
|
||||
pointerStartPos: -1 // negative denotes pointer not engaged
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// if (debug) console.log(`withSCRUBBING useLAYOUTeffect!! `);
|
||||
function handleScrubStart (e: MouseEvent) {
|
||||
if(refEl.current){
|
||||
if(refEl.current.contains(e.target as Node)){
|
||||
setStates((curr)=>({
|
||||
...curr,
|
||||
pointerStartPos: e.clientX
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
function handleScrubEnd (e: MouseEvent){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setStates((curr)=>({
|
||||
...curr,
|
||||
pointerStartPos: -1,
|
||||
prevLeftOffset: curr.currLeftOffset,
|
||||
setBySelf: Date.now(),
|
||||
}));
|
||||
};
|
||||
function handleScrubMove (e: MouseEvent){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setStates((curr)=>{
|
||||
if(curr.pointerStartPos >= 0 && curr.pointerStartPos!==null){
|
||||
let newLeftOffset = curr.prevLeftOffset + e.clientX - curr.pointerStartPos;
|
||||
if (newLeftOffset + scrubberWidth > boundingWidth) {
|
||||
newLeftOffset = boundingWidth - scrubberWidth;
|
||||
}else if(newLeftOffset < 0) {
|
||||
newLeftOffset = 0;
|
||||
}
|
||||
if(newLeftOffset !== curr.currLeftOffset){
|
||||
return{
|
||||
...curr,
|
||||
currLeftOffset: newLeftOffset,
|
||||
}
|
||||
}
|
||||
}
|
||||
return curr;
|
||||
});
|
||||
};
|
||||
if(!(window as any)[`listener-id-${refListener}`]){
|
||||
(window as any)[`listender-id-${refListener}`] = true;
|
||||
window.addEventListener("mousedown", handleScrubStart);
|
||||
window.addEventListener("mouseup", handleScrubEnd);
|
||||
window.addEventListener("mousemove", handleScrubMove);
|
||||
return () => {
|
||||
(window as any)[`listener-id-${refListener}`] = false;
|
||||
window.removeEventListener("mousedown", handleScrubStart);
|
||||
window.removeEventListener("mouseup", handleScrubEnd);
|
||||
window.removeEventListener("mousemove", handleScrubMove);
|
||||
};
|
||||
}
|
||||
}, [scrubberWidth, boundingWidth]);
|
||||
|
||||
useEffect(()=>{
|
||||
// if (debug) console.log(`withSCRUBBING useEFFECT!! `);
|
||||
if(onScrubChanged && boundingWidth > 0 && prevLeftOffset >= 0 && propsLeftOffset !== prevLeftOffset){
|
||||
onScrubChanged(prevLeftOffset);
|
||||
}
|
||||
},[propsLeftOffset, prevLeftOffset, boundingWidth, onScrubChanged]);
|
||||
|
||||
useEffect(()=>{
|
||||
// this takes a forced reset on leftoffset
|
||||
setStates((curr)=>{
|
||||
if(curr.prevLeftOffset === curr.currLeftOffset
|
||||
&& propsLeftOffset !== curr.prevLeftOffset
|
||||
&& propsLeftOffset !== curr.prevLeftOffset
|
||||
){
|
||||
return {
|
||||
...curr,
|
||||
currLeftOffset: propsLeftOffset, // in pixels
|
||||
prevLeftOffset: propsLeftOffset, //in pixels
|
||||
}
|
||||
}else{
|
||||
return curr
|
||||
}
|
||||
});
|
||||
}, [propsLeftOffset])
|
||||
|
||||
return(
|
||||
<div
|
||||
className="scrubber-wrapper"
|
||||
ref={refEl}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top:0,
|
||||
width: scrubberWidth + 'px',
|
||||
left: currLeftOffset + 'px',
|
||||
cursor: pointerStartPos >=0 ? 'grabbing': 'grab',
|
||||
...styleOverride
|
||||
}}
|
||||
>
|
||||
<Component
|
||||
{...rest as P}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
export function roundTo2Dec(floaty:number){
|
||||
return Math.round(floaty*100)/100;
|
||||
}
|
||||
export function fractionToPercentage(fraction:number){
|
||||
return roundTo2Dec(fraction * 100);
|
||||
}
|
||||
export function formatSecondsToHHMMSSCS(seconds:number){
|
||||
//example of the ISO String: 1970-01-01T00:01:40.774Z
|
||||
const isoString = new Date(seconds * 1000).toISOString();
|
||||
if(seconds > 3600)
|
||||
return isoString.substring(11, 19) + "." + isoString.substring(20, 22)
|
||||
else
|
||||
return isoString.substring(14, 19) + "." + isoString.substring(20, 22);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import VideoQuickTrim from './VideoQuickTrim'
|
||||
import { QuickTrimData } from './types';
|
||||
import {VideoQuickTrim} from './VideoQuickTrim'
|
||||
import { QuickTrimData } from './utilities';
|
||||
|
||||
export type { QuickTrimData };
|
||||
export default VideoQuickTrim;
|
||||
@@ -0,0 +1,223 @@
|
||||
import {
|
||||
MIN_VID_DURATION,
|
||||
TIME_CURSOR_WIDTH,
|
||||
TRIM_OPTIONS,
|
||||
roundToMilliseconds
|
||||
} from "./utilities";
|
||||
|
||||
export enum STATE_STATUSES {
|
||||
INIT = "init",
|
||||
VIDEO_METADATA_LOADED = "video_metadata_loaded",
|
||||
LOAD_ORDER_ERROR = "load_order_error",
|
||||
ACTION_TYPE_ERROR = "no_such_action_type"
|
||||
};
|
||||
|
||||
export enum PLAYPUASE_STATES{
|
||||
NOT_READY = "not_ready",
|
||||
READY = "ready",
|
||||
PLAYING = "playing",
|
||||
PAUSED = "paused",
|
||||
ENDED = "ended",
|
||||
}
|
||||
|
||||
export type State = {
|
||||
status: string;
|
||||
errorMessage: string[];
|
||||
|
||||
playpause: string;
|
||||
|
||||
canNotTrim: boolean | undefined;
|
||||
trimReset: number;
|
||||
trimDuration: number | undefined;
|
||||
trimStartSeconds: number | undefined;
|
||||
trimEndSeconds: number | undefined;
|
||||
videoLoadProgress: TimeRanges | undefined;
|
||||
videoDuration: number | undefined;
|
||||
|
||||
|
||||
playbarWidth: number | undefined;
|
||||
scrubberWidth: number | undefined;
|
||||
timeCursorOffset: number | undefined;
|
||||
|
||||
isMuted: boolean;
|
||||
isRepeatOn: boolean;
|
||||
};
|
||||
|
||||
export const initialState = {
|
||||
status: STATE_STATUSES.INIT,
|
||||
errorMessage : [],
|
||||
playpause: PLAYPUASE_STATES.NOT_READY,
|
||||
canNotTrim: undefined,
|
||||
trimReset: Date.now(),
|
||||
trimDuration: undefined,
|
||||
trimStartSeconds: undefined,
|
||||
trimEndSeconds: undefined,
|
||||
videoLoadProgress: undefined,
|
||||
videoDuration: undefined,
|
||||
playbarWidth: undefined,
|
||||
scrubberWidth: undefined,
|
||||
timeCursorOffset: undefined,
|
||||
isMuted: false,
|
||||
isRepeatOn: true,
|
||||
}
|
||||
|
||||
export enum ACTION_TYPES {
|
||||
RESET = "reset",
|
||||
TOGGLE_REPEAT = "toggle_repeat",
|
||||
TOGGLE_MUTE = "toggle_mute",
|
||||
ON_LOADED_METADATA = "on_loaded_metadata",
|
||||
MOVE_TIMECURSOR = "move_timecursor",
|
||||
SET_PLAYPUASE = "set_playpause",
|
||||
SET_PLAYBAR_LAYOUT = "set_playbar_layout",
|
||||
SET_TRIM_DURATION = "set_trim_duration",
|
||||
SET_VIDEO_LOAD_PROGRESS = "set_video_load_progress",
|
||||
MOVE_TRIM = "move_trim"
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| {type: ACTION_TYPES.RESET}
|
||||
| {type: ACTION_TYPES.TOGGLE_REPEAT, payload?:{isRepeatOn: boolean}}
|
||||
| {type: ACTION_TYPES.TOGGLE_MUTE}
|
||||
| {type: ACTION_TYPES.ON_LOADED_METADATA, payload: {videoDuration: number}}
|
||||
| {type: ACTION_TYPES.MOVE_TIMECURSOR, payload:{ timeCursorOffset: number}}
|
||||
| {type: ACTION_TYPES.SET_PLAYPUASE, payload:{ playpause: string}}
|
||||
| {type: ACTION_TYPES.SET_PLAYBAR_LAYOUT, payload:{ playbarWidth: number}}
|
||||
| {type: ACTION_TYPES.SET_TRIM_DURATION, payload:{ trimDurationString: string}}
|
||||
| {type: ACTION_TYPES.SET_VIDEO_LOAD_PROGRESS, payload: {
|
||||
videoLoadProgress: TimeRanges
|
||||
}}
|
||||
| {type: ACTION_TYPES.MOVE_TRIM, payload:{
|
||||
trimStartSeconds: number,
|
||||
trimEndSeconds: number,
|
||||
}}
|
||||
;
|
||||
|
||||
export function reducer(state: State, action: Action): State {
|
||||
console.log("VideoQuickTrim State Dispatch");
|
||||
console.log(action);
|
||||
|
||||
switch(action.type){
|
||||
case ACTION_TYPES.SET_PLAYPUASE:{
|
||||
return {...state, ...action.payload};
|
||||
}
|
||||
case ACTION_TYPES.TOGGLE_REPEAT:{
|
||||
if(action.payload){
|
||||
return {...state, isRepeatOn: action.payload.isRepeatOn}
|
||||
}
|
||||
return {...state, isRepeatOn: !state.isRepeatOn}
|
||||
}
|
||||
case ACTION_TYPES.TOGGLE_MUTE:{
|
||||
return {...state, isMuted: !state.isMuted}
|
||||
}
|
||||
case ACTION_TYPES.ON_LOADED_METADATA:{
|
||||
if(action.payload.videoDuration >= MIN_VID_DURATION){
|
||||
return{
|
||||
...state,
|
||||
status: STATE_STATUSES.VIDEO_METADATA_LOADED,
|
||||
videoDuration: action.payload.videoDuration,
|
||||
canNotTrim: false,
|
||||
trimDuration: 3,
|
||||
trimStartSeconds: 0,
|
||||
trimEndSeconds: 3,
|
||||
}
|
||||
}else{
|
||||
return {
|
||||
...state,
|
||||
videoDuration: action.payload.videoDuration,
|
||||
canNotTrim: true
|
||||
}
|
||||
}
|
||||
}
|
||||
case ACTION_TYPES.SET_VIDEO_LOAD_PROGRESS:{
|
||||
return{
|
||||
...state,
|
||||
videoLoadProgress: action.payload.videoLoadProgress
|
||||
};
|
||||
}
|
||||
case ACTION_TYPES.SET_PLAYBAR_LAYOUT:{
|
||||
const newWidth = action.payload.playbarWidth;
|
||||
const prevOffset = state.timeCursorOffset || 0;
|
||||
if(state.trimDuration && state.videoDuration){
|
||||
return {
|
||||
...state,
|
||||
playpause: PLAYPUASE_STATES.READY,
|
||||
playbarWidth: newWidth,
|
||||
scrubberWidth: newWidth * (state.trimDuration / state.videoDuration),
|
||||
timeCursorOffset: (newWidth-TIME_CURSOR_WIDTH)*(prevOffset/ state.videoDuration)
|
||||
};
|
||||
}else{
|
||||
return{
|
||||
...state,
|
||||
status: STATE_STATUSES.LOAD_ORDER_ERROR,
|
||||
errorMessage: [...state.errorMessage, 'Setting playbar layout before video and trim are loaded']
|
||||
}
|
||||
}
|
||||
}
|
||||
case ACTION_TYPES.SET_TRIM_DURATION:{
|
||||
const selected = action.payload.trimDurationString;
|
||||
if( state.canNotTrim !== undefined
|
||||
&& state.canNotTrim === false
|
||||
&& state.playbarWidth !==undefined
|
||||
&& state.videoDuration !== undefined
|
||||
&& TRIM_OPTIONS[selected] <= state.videoDuration
|
||||
&& state.trimStartSeconds !== undefined
|
||||
){
|
||||
let newTrimStart = state.trimStartSeconds;
|
||||
let newTrimEnd = state.trimStartSeconds + TRIM_OPTIONS[selected];
|
||||
if (newTrimEnd > state.videoDuration){
|
||||
newTrimEnd = state.videoDuration;
|
||||
if (state.videoDuration - TRIM_OPTIONS[selected] >=0) {
|
||||
newTrimStart = roundToMilliseconds(state.videoDuration - TRIM_OPTIONS[selected]);
|
||||
}else{
|
||||
newTrimStart = 0;
|
||||
}
|
||||
}
|
||||
return{
|
||||
...state,
|
||||
trimDuration: newTrimEnd-newTrimStart,
|
||||
trimStartSeconds: newTrimStart,
|
||||
trimEndSeconds: newTrimEnd,
|
||||
scrubberWidth: (newTrimEnd-newTrimStart)/state.videoDuration * state.playbarWidth,
|
||||
}
|
||||
}else{
|
||||
return {
|
||||
...state,
|
||||
status: STATE_STATUSES.LOAD_ORDER_ERROR,
|
||||
errorMessage: [...state.errorMessage, 'Setting trim duration before video and trim are loaded']
|
||||
}
|
||||
}
|
||||
}
|
||||
case ACTION_TYPES.MOVE_TRIM:{
|
||||
return{
|
||||
...state,
|
||||
trimStartSeconds: roundToMilliseconds(action.payload.trimStartSeconds),
|
||||
trimEndSeconds: roundToMilliseconds(action.payload.trimEndSeconds),
|
||||
}
|
||||
}
|
||||
case ACTION_TYPES.MOVE_TIMECURSOR:{
|
||||
return {
|
||||
...state,
|
||||
timeCursorOffset: action.payload.timeCursorOffset
|
||||
}
|
||||
}
|
||||
case ACTION_TYPES.RESET:{
|
||||
return initialState
|
||||
}
|
||||
default:{
|
||||
return {
|
||||
...state,
|
||||
status: STATE_STATUSES.ACTION_TYPE_ERROR,
|
||||
errorMessage: [...state.errorMessage, "Reducer Action switch's default case is reached"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
const trimScrubberWidth = videoRef.current && playbarWidth > 0
|
||||
? trimDuration > 0 && trimDuration < videoRef.current.duration
|
||||
? (trimDuration / videoRef.current.duration * playbarWidth)
|
||||
: playbarWidth
|
||||
: 0;
|
||||
*/
|
||||
|
||||
@@ -60,11 +60,11 @@ $playbar-height: 2rem;
|
||||
background-color: #e66462;
|
||||
opacity: 70%;
|
||||
}
|
||||
p {
|
||||
h1 {
|
||||
position: absolute;
|
||||
margin-top: 230px;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
.playbar {
|
||||
@@ -80,17 +80,34 @@ $playbar-height: 2rem;
|
||||
left: 0;
|
||||
background-color: $panel-inner-border;
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
overflow: hidden;
|
||||
|
||||
.played, .loaded{
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.played{
|
||||
background-color: $primary-dark;
|
||||
}
|
||||
.loaded{
|
||||
background-color: $gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
.scrubber-wrapper{
|
||||
box-shadow: 1px 1px 4px 0px rgba(0,0,0,1);
|
||||
}
|
||||
.trim-scrubber{
|
||||
position: absolute;
|
||||
top: -1rem;
|
||||
height: 3rem;
|
||||
background-color: #e66462;
|
||||
opacity: 0.8;
|
||||
outline: 0.125rem $white solid;
|
||||
border-top: 1rem $white solid;
|
||||
border-radius: 0.25rem 0.25rem 0.125rem 0.125rem;
|
||||
cursor: grab;
|
||||
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -103,9 +120,12 @@ $playbar-height: 2rem;
|
||||
.playcursor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 0.5rem;
|
||||
width: 0.5rem; //=8px
|
||||
transform: translate(-3px, 0px);
|
||||
height: $playbar-height;
|
||||
background-color: $white;
|
||||
border-radius: 1px;
|
||||
box-shadow: 1px 1px 4px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.playpause-external {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
//For integration into other components
|
||||
export type QuickTrimData = {
|
||||
trimStartSeconds: number;
|
||||
trimEndSeconds: number;
|
||||
}
|
||||
|
||||
// INTERNALS
|
||||
export type TrimStates = {
|
||||
canNotTrim: boolean;
|
||||
trimReset: Date;
|
||||
trimDuration: number;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
maxDuration: number;
|
||||
}
|
||||
|
||||
export type PlaybarStates = {
|
||||
playbarWidth: number;
|
||||
timeCursorOffset: number;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//For integration into other components
|
||||
export const TIME_CURSOR_WIDTH = 8;
|
||||
export const MIN_VID_DURATION = 3;
|
||||
export const ONE_MS = 0.001;
|
||||
|
||||
export type QuickTrimData = {
|
||||
trimStartSeconds: number;
|
||||
trimEndSeconds: number;
|
||||
}
|
||||
|
||||
export function roundToMilliseconds(floaty:number){
|
||||
return Math.round(floaty*1000)/1000;
|
||||
}
|
||||
|
||||
export function formatSecondsToHHMMSSCS(seconds:number){
|
||||
//example of the ISO String: 1970-01-01T00:01:40.774Z
|
||||
const isoString = new Date(seconds * 1000).toISOString();
|
||||
if(seconds > 3600)
|
||||
return isoString.substring(11, 19) + "." + isoString.substring(20, 22)
|
||||
else
|
||||
return isoString.substring(14, 19) + "." + isoString.substring(20, 22);
|
||||
}
|
||||
|
||||
export const TRIM_OPTIONS:{[key: string]: number } = {
|
||||
"3s": 3,
|
||||
"5s": 5,
|
||||
"10s":10,
|
||||
"15s":15
|
||||
};
|
||||
@@ -4,20 +4,20 @@ export enum Website {
|
||||
}
|
||||
|
||||
export interface DomainConfig {
|
||||
website: Website,
|
||||
logo: string,
|
||||
title: string,
|
||||
link: string,
|
||||
website: Website;
|
||||
logo: string;
|
||||
title: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const domainConfigs: Record<string, DomainConfig> = {
|
||||
"storyteller": {
|
||||
storyteller: {
|
||||
website: Website.StorytellerAi,
|
||||
logo: "/fakeyou/Storyteller-Logo-1.png",
|
||||
title: "Storyteller AI",
|
||||
link: "https://storyteller.ai",
|
||||
},
|
||||
"fakeyou": {
|
||||
fakeyou: {
|
||||
website: Website.FakeYou,
|
||||
logo: "/fakeyou/FakeYou-Logo-2.png",
|
||||
title: "FakeYou",
|
||||
@@ -35,7 +35,7 @@ export const getCurrentDomainConfig = (): DomainConfig => {
|
||||
}
|
||||
|
||||
if (window.location.hostname.includes("storyteller")) {
|
||||
return domainConfigs.storyteller;
|
||||
return domainConfigs.storyteller;
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import VideoQuickTrim from 'components/common/VideoQuickTrim';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
@@ -7,9 +7,13 @@ import {Container, Panel} from 'components/common';
|
||||
export default function ComponentSandbox(
|
||||
{parentPath}:{parentPath:string}
|
||||
){
|
||||
const [state1, setState1] = useState<number|string|boolean|undefined>(undefined);
|
||||
const [state2, setState2] = useState<number|string|boolean|undefined>(undefined);
|
||||
const handleCallback = (val:any)=>{
|
||||
console.log('onChange is triggered')
|
||||
console.log(val);
|
||||
setState1(val.trimStartSeconds);
|
||||
setState2(val.trimEndSeconds);
|
||||
}
|
||||
return (<Container>
|
||||
<NavLink to={`${parentPath}`}>← Back</NavLink>
|
||||
@@ -18,18 +22,22 @@ export default function ComponentSandbox(
|
||||
|
||||
<div className="m-4">
|
||||
<VideoQuickTrim
|
||||
trimStartSeconds={Number(state1 ||0)}
|
||||
trimEndSeconds={Number(state2 || 0)}
|
||||
onSelect={handleCallback}
|
||||
mediaToken='m_wdptbe5rfzpb1gzhva78gpw8qrkgs9'
|
||||
/>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<p>{`Start Seconds: ${state1}`}</p>
|
||||
<p>{`Start Seconds: ${state2}`}</p>
|
||||
{/* <br/>
|
||||
<div className="m-4">
|
||||
<VideoQuickTrim
|
||||
onSelect={handleCallback}
|
||||
mediaToken='m_h21nykes0j3ph23y5z7w9axtjjn2rr'
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</Panel>
|
||||
</Container>);
|
||||
}
|
||||
@@ -2,14 +2,12 @@ import React from "react";
|
||||
import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import {} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
|
||||
import { Container } from "components/common";
|
||||
import FakeYouLandingHeader from "./fakeyou/FakeYouLandingHeader";
|
||||
import Dashboard from "./Dashboard";
|
||||
import { useDomainConfig } from "context/DomainConfigContext";
|
||||
import StorytellerLanding from "./storyteller/StorytellerLanding";
|
||||
import LandingVideoReel from "./components/LandingVideoReel";
|
||||
import LandingVideoReel from "./components/LandingVideoReel/LandingVideoReel";
|
||||
import {
|
||||
FrontendInferenceJobType,
|
||||
InferenceJob,
|
||||
@@ -20,6 +18,8 @@ import "./LandingPage.scss";
|
||||
import VstSectionV2 from "./components/VstSectionV2";
|
||||
import FakeYouLandingBody from "./fakeyou/FakeYouLandingBody";
|
||||
import { DomainConfig, Website } from "utils/domainConfig";
|
||||
import OnboardingSelection from "./storyteller/OnboardingSelection";
|
||||
import TtsDemoSection from "./components/TtsDemoSection/TtsDemoSection";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -39,13 +39,15 @@ function LandingPage(props: Props) {
|
||||
|
||||
PosthogClient.recordPageview();
|
||||
|
||||
const domain : DomainConfig = useDomainConfig();
|
||||
const domain: DomainConfig = useDomainConfig();
|
||||
|
||||
const isLoggedIn = props.sessionWrapper.isLoggedIn();
|
||||
|
||||
return (
|
||||
<>
|
||||
{domain.website === Website.StorytellerAi && !isLoggedIn && <LandingVideoReel />}
|
||||
{domain.website === Website.StorytellerAi && !isLoggedIn && (
|
||||
<LandingVideoReel />
|
||||
)}
|
||||
<Container type="panel">
|
||||
{domain.website === Website.FakeYou ? (
|
||||
<>
|
||||
@@ -76,15 +78,23 @@ function LandingPage(props: Props) {
|
||||
<>
|
||||
{/* STORYTELLER,AI */}
|
||||
{!isLoggedIn ? (
|
||||
<StorytellerLanding
|
||||
sessionWrapper={props.sessionWrapper}
|
||||
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
|
||||
inferenceJobs={props.inferenceJobs}
|
||||
ttsInferenceJobs={props.ttsInferenceJobs}
|
||||
enqueueInferenceJob={props.enqueueInferenceJob}
|
||||
inferenceJobsByCategory={props.inferenceJobsByCategory}
|
||||
enqueueTtsJob={props.enqueueTtsJob}
|
||||
/>
|
||||
<>
|
||||
<OnboardingSelection />
|
||||
<VstSectionV2 />
|
||||
<TtsDemoSection
|
||||
sessionWrapper={props.sessionWrapper}
|
||||
sessionSubscriptionsWrapper={
|
||||
props.sessionSubscriptionsWrapper
|
||||
}
|
||||
inferenceJobs={props.inferenceJobs}
|
||||
ttsInferenceJobs={props.ttsInferenceJobs}
|
||||
enqueueInferenceJob={props.enqueueInferenceJob}
|
||||
inferenceJobsByCategory={props.inferenceJobsByCategory}
|
||||
enqueueTtsJob={props.enqueueTtsJob}
|
||||
/>
|
||||
<Dashboard sessionWrapper={props.sessionWrapper} />
|
||||
<FakeYouLandingBody />
|
||||
</>
|
||||
) : (
|
||||
<Dashboard sessionWrapper={props.sessionWrapper} />
|
||||
)}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.storyteller-landing-reel {
|
||||
display: flex;
|
||||
height: 74vh;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
video::-webkit-media-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
video::-webkit-media-controls-start-playback-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reel-overlay {
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(20, 20, 27, 0.6);
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 20%;
|
||||
background: linear-gradient(transparent, $bg);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.storyteller-landing-heading {
|
||||
font-size: 52px;
|
||||
}
|
||||
|
||||
.storyteller-landing-sub-text {
|
||||
max-width: 900px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.storyteller-landing-heading {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.storyteller-landing-sub-text {
|
||||
max-width: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.storyteller-landing-heading {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.storyteller-landing-sub-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-19
@@ -1,5 +1,7 @@
|
||||
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button } from "components/common";
|
||||
import React, { useEffect } from "react";
|
||||
import "./LandingVideoReel.scss";
|
||||
|
||||
interface LandingVideoReelProps {}
|
||||
|
||||
@@ -42,27 +44,30 @@ export default function LandingVideoReel(props: LandingVideoReelProps) {
|
||||
}, [isOnLandingPage]);
|
||||
|
||||
return (
|
||||
<div className="vh-100 d-flex align-items-center justify-content-center position-relative">
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
className="position-absolute w-100 h-100"
|
||||
style={{ objectFit: "cover" }}
|
||||
>
|
||||
<div className="storyteller-landing-reel">
|
||||
<video autoPlay muted loop controls={false} playsInline>
|
||||
<source src="/videos/landing-reel.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<div
|
||||
className="position-absolute w-100 h-100"
|
||||
style={{ backgroundColor: "rgba(20, 20, 27, 0.6)", zIndex: 0 }}
|
||||
/>
|
||||
<div className="text-center text-white d-flex flex-column align-items-center zi-2">
|
||||
<h1 className="display-2 fw-bolder">Enabling Anyone to Make Professional Film, Animation, and Music</h1>
|
||||
<h5 className="fw-normal mb-3 mt-2 w-75 lh-base opacity-75">
|
||||
StorytellerAI's powerful new engine combines Generative AI and
|
||||
User Generated Content to radically democratize both audio and video production.
|
||||
</h5>
|
||||
<Button label="Sign Up Now" to="/signup" className="mt-5" />
|
||||
<div className="reel-overlay">
|
||||
<div className="container h-100">
|
||||
<div className="text-center text-white d-flex flex-column justify-content-center align-items-center zi-2 h-100">
|
||||
<h1 className="storyteller-landing-heading fw-bold">
|
||||
Enabling Anyone to Make Professional Film, Animation, and Music
|
||||
</h1>
|
||||
<h5 className="fw-normal mb-2 mt-2 lh-base opacity-75 storyteller-landing-sub-text">
|
||||
StorytellerAI's powerful new engine combines Generative AI and
|
||||
User Generated Content to radically democratize both audio and
|
||||
video production.
|
||||
</h5>
|
||||
<Button
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
label="Sign Up Now"
|
||||
to="/signup"
|
||||
className="mt-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./LandingVideoReel";
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Panel } from "components/common";
|
||||
import React from "react";
|
||||
import LandingDemo from "../../fakeyou/LandingDemo/FakeYouLandingDemo";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import {
|
||||
FrontendInferenceJobType,
|
||||
InferenceJob,
|
||||
} from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs";
|
||||
|
||||
interface TtsDemoSectionProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
|
||||
inferenceJobs: Array<InferenceJob>;
|
||||
ttsInferenceJobs: Array<TtsInferenceJob>;
|
||||
enqueueInferenceJob: (
|
||||
jobToken: string,
|
||||
frontendInferenceJobType: FrontendInferenceJobType
|
||||
) => void;
|
||||
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
|
||||
enqueueTtsJob: (jobToken: string) => void;
|
||||
}
|
||||
|
||||
export default function TtsDemoSection(props: TtsDemoSectionProps) {
|
||||
return (
|
||||
<Panel clear={true}>
|
||||
<div className="row g-4 section">
|
||||
<div className="col-12 col-lg-6 d-flex flex-column justify-content-center">
|
||||
<h1 className="fw-bold">Generate Text to Speech</h1>
|
||||
<p className="opacity-75 mb-0">
|
||||
Create audio of any character saying anything you want.
|
||||
</p>
|
||||
<div className="d-flex mt-4">
|
||||
<Button
|
||||
label="Try all 3000+ TTS Voices"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
to="/tts"
|
||||
small={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<LandingDemo
|
||||
inferenceJobs={props.inferenceJobs}
|
||||
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
|
||||
enqueueInferenceJob={props.enqueueInferenceJob}
|
||||
inferenceJobsByCategory={props.inferenceJobsByCategory}
|
||||
ttsInferenceJobs={props.ttsInferenceJobs}
|
||||
enqueueTtsJob={props.enqueueTtsJob}
|
||||
showHanashi={false}
|
||||
autoFocusTextBox={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
+13
@@ -3,6 +3,14 @@
|
||||
.vst-section {
|
||||
padding-top: 5rem;
|
||||
padding-bottom: 5rem;
|
||||
|
||||
video::-webkit-media-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
video::-webkit-media-controls-start-playback-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vst-carousel-panel {
|
||||
@@ -166,4 +174,9 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.vst-section {
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -155,6 +155,8 @@ export default function VstSectionV2(props: VstSectionV2Props) {
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
controls={false}
|
||||
key={`${index}-${activeIndex}`}
|
||||
onPlay={handleVideoPlay}
|
||||
onPause={handleVideoPause}
|
||||
@@ -184,7 +186,7 @@ export default function VstSectionV2(props: VstSectionV2Props) {
|
||||
{videoSet.map((video, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<div className="ratio ratio-16x9 vst-carousel-panel">
|
||||
<video loop muted>
|
||||
<video loop muted playsInline controls={false}>
|
||||
<source src={video.src} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
@@ -214,6 +216,8 @@ export default function VstSectionV2(props: VstSectionV2Props) {
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
controls={false}
|
||||
key={styledVideo.src}
|
||||
onPlay={handleVideoPlay}
|
||||
onPause={handleVideoPause}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function FakeYouLandingBody(props: FakeYouLandingBodyProps) {
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-left-bottom me-3"
|
||||
className="dots-right-bottom me-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+20
-13
@@ -35,6 +35,8 @@ interface TtsInferencePanelProps {
|
||||
) => void;
|
||||
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
|
||||
enqueueTtsJob: (jobToken: string) => void;
|
||||
showHanashi?: boolean;
|
||||
autoFocusTextBox?: boolean;
|
||||
}
|
||||
|
||||
export default function LandingDemo({
|
||||
@@ -44,6 +46,8 @@ export default function LandingDemo({
|
||||
enqueueInferenceJob,
|
||||
enqueueTtsJob,
|
||||
inferenceJobsByCategory,
|
||||
showHanashi = true,
|
||||
autoFocusTextBox = true,
|
||||
}: TtsInferencePanelProps) {
|
||||
const [textBuffer, setTextBuffer] = useState("");
|
||||
const [maybeTtsError, setMaybeTtsError] = useState<
|
||||
@@ -221,18 +225,21 @@ export default function LandingDemo({
|
||||
|
||||
return (
|
||||
<div className="position-relative">
|
||||
<img
|
||||
src={
|
||||
isHanashiHovered
|
||||
? "/images/landing/hanashi-demo-2.webp"
|
||||
: "/images/landing/hanashi-demo-1.webp"
|
||||
}
|
||||
alt="Hanashi Demo"
|
||||
onMouseEnter={() => setIsHanashiHovered(true)}
|
||||
onMouseLeave={() => setIsHanashiHovered(false)}
|
||||
className="hanashi-demo-image"
|
||||
draggable="false"
|
||||
/>
|
||||
{showHanashi && (
|
||||
<img
|
||||
src={
|
||||
isHanashiHovered
|
||||
? "/images/landing/hanashi-demo-2.webp"
|
||||
: "/images/landing/hanashi-demo-1.webp"
|
||||
}
|
||||
alt="Hanashi Demo"
|
||||
onMouseEnter={() => setIsHanashiHovered(true)}
|
||||
onMouseLeave={() => setIsHanashiHovered(false)}
|
||||
className="hanashi-demo-image"
|
||||
draggable="false"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Panel padding={true}>
|
||||
<form className="d-flex flex-column">
|
||||
<div>
|
||||
@@ -265,7 +272,7 @@ export default function LandingDemo({
|
||||
onChange={handleChangeText}
|
||||
rows={4}
|
||||
resize={false}
|
||||
autoFocus={isMobile ? false : true}
|
||||
autoFocus={isMobile ? false : autoFocusTextBox ? true : false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { Panel } from "components/common";
|
||||
import Card from "components/common/Card";
|
||||
import React from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
interface OnboardingSelectionProps {}
|
||||
|
||||
export default function OnboardingSelection(props: OnboardingSelectionProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const cards = [
|
||||
{
|
||||
text: "Scene 1",
|
||||
backgroundImage: "",
|
||||
onClick: () => {
|
||||
history.push("/studio-intro/");
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Scene 2",
|
||||
backgroundImage: "",
|
||||
onClick: () => {
|
||||
history.push("/studio-intro/");
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Scene 3",
|
||||
backgroundImage: "",
|
||||
onClick: () => {
|
||||
history.push("/studio-intro/");
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Scene 4",
|
||||
backgroundImage: "",
|
||||
onClick: () => {
|
||||
history.push("/studio-intro/");
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Panel clear={true}>
|
||||
<div className="text-center my-5 pb-5">
|
||||
<h1 className="fw-bold">Explore scene examples</h1>
|
||||
<p className="opacity-75">
|
||||
Try generating a movie by clicking a scene below
|
||||
</p>
|
||||
<div className="row g-3 g-lg-4 mt-3 centered-row">
|
||||
{cards.map((card, index) => (
|
||||
<div className="col-6 col-lg-3" key={index}>
|
||||
<Card
|
||||
backgroundImage={card.backgroundImage}
|
||||
aspectRatio="16/9"
|
||||
canHover={true}
|
||||
borderWidth="2px"
|
||||
hoverPrimaryColor={true}
|
||||
onClick={card.onClick}
|
||||
bottomText={card.text}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { default } from "./OnboardingSelection";
|
||||
@@ -31,220 +31,5 @@ export default function StorytellerLanding({
|
||||
}: StorytellerLandingProps) {
|
||||
// const { t } = useLocalize("LandingPage");
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <Container type="panel" className="py-5">
|
||||
<FakeYouLandingHeader
|
||||
sessionWrapper={sessionWrapper}
|
||||
sessionSubscriptionsWrapper={sessionSubscriptionsWrapper}
|
||||
inferenceJobs={inferenceJobs}
|
||||
ttsInferenceJobs={ttsInferenceJobs}
|
||||
enqueueInferenceJob={enqueueInferenceJob}
|
||||
inferenceJobsByCategory={inferenceJobsByCategory}
|
||||
enqueueTtsJob={enqueueTtsJob}
|
||||
/>
|
||||
|
||||
{/* <Container type="panel" className="py-5">
|
||||
<Panel clear={true} className="py-lg-5">
|
||||
<div className="row g-5 g-lg-5 flex-row-reverse">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="position-relative">
|
||||
<div className="ratio ratio-16x9 video-container">
|
||||
<video
|
||||
autoPlay={true}
|
||||
playsInline={true}
|
||||
loop={true}
|
||||
muted={true}
|
||||
>
|
||||
<source src="/videos/tts-video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-right-bottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 d-flex flex-column justify-content-center text-center text-lg-start gap-2">
|
||||
<h1 className="fw-bold display-5">
|
||||
Enabling Anyone to Create Quality Movies with AI
|
||||
</h1>
|
||||
<p className="opacity-75">
|
||||
We are combining generative AI and User Generated Content to
|
||||
radically democratize both audio and video production.
|
||||
</p>
|
||||
<div className="d-flex mt-3 mt-lg-4 gap-3 justify-content-center justify-content-lg-start">
|
||||
<Button
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
label="Sign Up"
|
||||
to="/signup"
|
||||
/>
|
||||
<Button
|
||||
icon={faCompass}
|
||||
label="Explore"
|
||||
variant="secondary"
|
||||
to="/explore"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Container> */}
|
||||
|
||||
{/* <div className="panel rounded-0 py-5 my-5">
|
||||
<Container type="panel" className="py-lg-5">
|
||||
<Panel clear={true}>
|
||||
<div className="d-flex flex-column align-items-center rounded-0 mt-lg-4 pt-lg-4">
|
||||
<div className="d-flex flex-column align-items-center text-center cta-container">
|
||||
<h2 className="fw-bold">AI Audio Generation</h2>
|
||||
<p className="mw-300 opacity-75">{t("ctaText")}</p>
|
||||
<div className="d-flex d-lg-none align-items-center mt-2 cta-mobile">
|
||||
<img
|
||||
src="/images/landing/hanashi-before.webp"
|
||||
alt="hanashi before"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-red.webp"
|
||||
alt="red chevrons"
|
||||
width={95}
|
||||
height={80}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/hanashi-after.webp"
|
||||
alt="hanashi after"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/hanashi-before.webp"
|
||||
alt="hanashi before"
|
||||
className="hanashi-before d-none d-lg-block"
|
||||
width={311}
|
||||
height={311}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-grey.webp"
|
||||
alt="grey chevrons"
|
||||
className="chevrons-grey d-none d-lg-block"
|
||||
width={127}
|
||||
height={108}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-red.webp"
|
||||
alt="red chevrons"
|
||||
className="chevrons-red d-none d-lg-block"
|
||||
width={127}
|
||||
height={108}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/hanashi-after.webp"
|
||||
alt="hanashi after"
|
||||
className="hanashi-after d-none d-lg-block"
|
||||
width={311}
|
||||
height={311}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row gy-4 mt-lg-3">
|
||||
<div className="col-12 col-md-6">
|
||||
<Panel padding={true} className="panel-inner rounded h-100">
|
||||
<div className="d-flex gap-2">
|
||||
<h4 className="fw-semibold mb-4 flex-grow-1">
|
||||
<FontAwesomeIcon icon={faMessageDots} className="me-3" />
|
||||
Text to Speech
|
||||
</h4>
|
||||
<Link to="/tts">
|
||||
Try more voices{" "}
|
||||
<FontAwesomeIcon icon={faArrowRight} className="ms-1" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<Select label="Select a Voice" />
|
||||
<Input
|
||||
label="Your Text"
|
||||
placeholder="Type what you want your character to say"
|
||||
/>
|
||||
<div className="d-flex gap-2 justify-content-end">
|
||||
<Button
|
||||
label="Generate"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<Panel padding={true} className="panel-inner rounded h-100">
|
||||
<div className="d-flex gap-2">
|
||||
<h4 className="fw-semibold mb-4 flex-grow-1">
|
||||
<FontAwesomeIcon
|
||||
icon={faWaveformLines}
|
||||
className="me-3"
|
||||
/>
|
||||
Voice to Voice
|
||||
</h4>
|
||||
<Link to="/voice-conversion">
|
||||
Try more voices{" "}
|
||||
<FontAwesomeIcon icon={faArrowRight} className="ms-1" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<Select label="Select a Voice" />
|
||||
<div className="d-flex gap-2 justify-content-end">
|
||||
<Button
|
||||
label="Generate"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Container>
|
||||
</div> */}
|
||||
|
||||
{/* <Container type="panel" className="pt-5 my-5">
|
||||
<Panel padding={true}>
|
||||
<div className="d-flex flex-column align-items-center py-5">
|
||||
<h2 className="fw-bold mb-2">{t("communityTitle")}</h2>
|
||||
<p className="opacity-75">{t("communityText")}</p>
|
||||
<div className="d-flex mt-4 gap-3">
|
||||
<a
|
||||
href="https://discord.gg/fakeyou"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-discord"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} className="me-2" />
|
||||
{t("communityButtonDiscord")}
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/intent/follow?screen_name=FakeYouApp"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-twitter"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTwitter} className="me-2" />
|
||||
{t("communityButtonTwitter")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-left-bottom me-3"
|
||||
/>
|
||||
</Panel>
|
||||
</Container> */}
|
||||
</>
|
||||
);
|
||||
return <>Test</>;
|
||||
}
|
||||
+2
@@ -126,6 +126,8 @@ export default function PageVSTApp() {
|
||||
<div className="col-12 col-md-6">
|
||||
{job.isSuccessful && job.maybe_result ? (
|
||||
<VideoQuickTrim
|
||||
trimStartSeconds={0}
|
||||
trimEndSeconds={0}
|
||||
mediaToken={job.maybe_result.entity_token}
|
||||
onSelect={(val: QuickTrimData) =>
|
||||
handleOnChange({
|
||||
|
||||
+2
@@ -70,6 +70,8 @@ export default function PageVSTApp({
|
||||
<div className="row g-5 mb-4">
|
||||
<div className="col-12 col-md-6">
|
||||
<VideoQuickTrim
|
||||
trimStartSeconds={vstValues.trimStart}
|
||||
trimEndSeconds={vstValues.trimEnd}
|
||||
mediaToken={vstValues.fileToken}
|
||||
onSelect={(val: QuickTrimData) =>
|
||||
handleOnChange({
|
||||
|
||||
Reference in new issue
Block a user