mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 16:38:14 +00:00
Merge pull request #261 from storytold/olivicmic-fy-lipsync-revamp
Olivicmic fy lipsync revamp
This commit is contained in:
35 files changed
+623
No files matched your search
@@ -0,0 +1 @@
|
||||
.fy-audio-uploader > div { margin-bottom: 1rem }
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { InputVcAudioPlayer } from "v2/view/_common/InputVcAudioPlayer";
|
||||
import { FileActions, FileDetails, FileLabel, FileWrapper } from 'components/common';
|
||||
import { faFileAudio } from "@fortawesome/pro-solid-svg-icons";
|
||||
import './AudioInput.scss'
|
||||
|
||||
const fileTypes = ["MP3", "WAV", "FLAC", "OGG"];
|
||||
|
||||
interface Props {
|
||||
blob?: string;
|
||||
clear?: (file?: any) => void;
|
||||
file?: any;
|
||||
hideActions?: boolean;
|
||||
hideClearDetails?: boolean;
|
||||
inputProps?: any;
|
||||
success?: boolean;
|
||||
submit?: () => void;
|
||||
working?: boolean;
|
||||
[x:string]: any;
|
||||
}
|
||||
|
||||
const n = () => {};
|
||||
|
||||
export default function AudioInput({ blob = "", clear = n, file, hideActions, hideClearDetails, inputProps, success = false, submit = n, working = false, ...rest }: Props) {
|
||||
|
||||
return <div {...{ className: "fy-audio-uploader" }}>
|
||||
<FileWrapper {...{ fileTypes, ...inputProps, panelClass: "p-3", ...rest }}>
|
||||
{ file ? <FileDetails {...{ clear, hideClearDetails, icon: faFileAudio, file }}/> : <FileLabel {...{ fileTypes }}/> }
|
||||
</FileWrapper>
|
||||
{ file && <>
|
||||
<div {...{ className: "panel panel-inner rounded p-3" }}>
|
||||
<InputVcAudioPlayer {...{ filename: blob as string }}/>
|
||||
</div>
|
||||
{ !hideActions && <FileActions {...{ clear, success, submit, working }}/> }
|
||||
</> }
|
||||
</div>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './AudioInput';
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck, faFileArrowUp, faTrash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import LoadingIcon from "./LoadingIcon";
|
||||
|
||||
interface Props {
|
||||
clear?: (x?: any) => void;
|
||||
upload?: (x?: any) => void;
|
||||
successful?: boolean;
|
||||
working?: boolean;
|
||||
}
|
||||
|
||||
const n = () => {};
|
||||
|
||||
export default function FileActions({ clear = n, upload = n, successful, working }: Props) {
|
||||
const uploadBtnClass = successful ? "btn btn-uploaded w-100 disabled" : "btn btn-primary w-100";
|
||||
|
||||
return <div className="d-flex gap-3">
|
||||
<button {...{ className: uploadBtnClass, disabled: working || successful, onClick: () => upload(), type: "submit", }}>
|
||||
<Icon {...{ className: "me-2", icon: successful ? faCheck : faFileArrowUp, }}/>
|
||||
{ working && <LoadingIcon /> }
|
||||
</button>
|
||||
<button {...{ className: "btn btn-destructive w-100", onClick: clear }}>
|
||||
<Icon icon={faTrash} className="me-2" />
|
||||
Clear
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export default function LoadingIcon() {
|
||||
return <>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm ms-3"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './FileActions';
|
||||
@@ -0,0 +1,10 @@
|
||||
.upload-details {
|
||||
width: 100%;
|
||||
|
||||
.upload-details-clear {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
margin: auto 0 auto auto;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import "./FileDetails.scss";
|
||||
|
||||
interface Props {
|
||||
clear?: (file?: any) => void;
|
||||
file?: any;
|
||||
hideClearDetails?: boolean;
|
||||
icon?: any;
|
||||
}
|
||||
|
||||
export default function FileDetails({ clear = () => {}, file, hideClearDetails, icon }: Props) {
|
||||
const fileSize =
|
||||
file && file.size >= 1024 * 1024
|
||||
? (file.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
: file
|
||||
? `${Math.floor(file.size / 1024)} KB`
|
||||
: null;
|
||||
|
||||
return <div {...{ className: "fy-uploader-layout upload-details" }}>
|
||||
<Icon {...{ className: "fy-uploader-layout-icon", icon }}/>
|
||||
<div>
|
||||
<div {...{ className: "filename" }}>
|
||||
{ file.name.slice(0, file.name.lastIndexOf(".")) }
|
||||
</div>
|
||||
<span className="opacity-50">
|
||||
{`${ file.name.split(".").pop().toUpperCase() } file size: ${ fileSize }`}
|
||||
</span>
|
||||
<u className="fw-medium opacity-100 ms-1">Change file</u>
|
||||
</div>
|
||||
{ !hideClearDetails && <button {...{ className: "upload-details-clear btn btn-destructive align-items-center justify-content-center", onClick: e => { e.preventDefault(); clear() } }}>
|
||||
<Icon {...{ icon: faTrash }}/>
|
||||
</button> }
|
||||
</div>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './FileDetails';
|
||||
Whitespace-only changes.
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
|
||||
import { faFileArrowUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import "./FileLabel.scss";
|
||||
|
||||
interface Props {
|
||||
fileTypes?: string[];
|
||||
}
|
||||
|
||||
export default function FileLabel({ fileTypes = [] }: Props) {
|
||||
return <div {...{ className: "fy-uploader-layout fy-upload-label" }}>
|
||||
<Icon {...{ className: "fy-uploader-layout-icon", icon: faFileArrowUp }}/>
|
||||
<div className="pb-0">
|
||||
<u className="fw-medium">Upload a file</u> or drop it here...
|
||||
<p className="opacity-50">
|
||||
{ fileTypes.join(", ").toString() } supported
|
||||
</p>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './FileLabel';
|
||||
@@ -0,0 +1,33 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.fy-uploader {
|
||||
[type="file"] {
|
||||
border: 0;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute !important;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
|
||||
+ label {
|
||||
cursor: pointer;
|
||||
border: dashed 2px rgba(255, 255, 255, 0.12) !important;
|
||||
border-radius: 0.5rem;
|
||||
transition: 0.1s all;
|
||||
&:hover {
|
||||
background-color: lighten($panel, 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.fy-uploader-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.fy-uploader-layout-icon {
|
||||
font-size: 2.625rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { useId } from 'hooks'; // replace with react v18
|
||||
import './FileWrapper.scss'
|
||||
|
||||
interface Props {
|
||||
children?: JSX.Element|JSX.Element[];
|
||||
containerClass?: string;
|
||||
fileTypes?: string[];
|
||||
inputRef: any;
|
||||
onChange: (file?: any) => void;
|
||||
panelClass?: string;
|
||||
[x:string]: any;
|
||||
};
|
||||
|
||||
export default function FileWrapper({ children, containerClass, fileTypes = [], inputRef, onChange, panelClass, ...rest }: Props) {
|
||||
const id = 'file-input-' + useId();
|
||||
const accept = fileTypes.map((type,i) => `.${ type.toLowerCase() }`).join(",");
|
||||
|
||||
const fileChange = (e: any) => {
|
||||
e.preventDefault();
|
||||
onChange({ target: { name: e.target?.name || "file-input", value: e.target.files[0] }});
|
||||
};
|
||||
const onDragDrop = (e: any) => { e.preventDefault(); e.stopPropagation(); };
|
||||
const onDragEvent = (onOff: number) => (e: React.DragEvent<HTMLDivElement>): void => {
|
||||
onDragDrop(e);
|
||||
e.currentTarget.classList[onOff ? "add" : "remove" ]("upload-zone-drag");
|
||||
};
|
||||
const onDrop = (e: any): void => {
|
||||
onDragDrop(e);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
onChange({ target: { name: e.target?.name || "file-input", value: e.dataTransfer.files[0] }});
|
||||
}
|
||||
};
|
||||
|
||||
return <div {...{ className: `fy-uploader${ containerClass ? " " + containerClass : "" }`, onDragLeave: onDragEvent(1), onDragOver: onDragEvent(0), onDrop }}>
|
||||
<input { ...{ accept, onChange: fileChange, type: "file", id, ref: inputRef, ...rest }} />
|
||||
<label {...{ className: `panel panel-inner d-flex align-items-center${ panelClass ? " " + panelClass : "" }`, htmlFor: id }} >
|
||||
{ children }
|
||||
</label>
|
||||
</div>;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export default function LoadingIcon() {
|
||||
return <>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm ms-3"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './FileWrapper';
|
||||
@@ -0,0 +1,39 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.fy-image-uploader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.file-preview {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: .5rem;
|
||||
object-fit: cover;
|
||||
}
|
||||
.image-upload-label, .upload-details {
|
||||
background-color: rgba($panel, .85);
|
||||
position: absolute;
|
||||
}
|
||||
.image-upload-label {
|
||||
border-radius: .5rem .5rem 0 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.upload-details {
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
// background-color: red;
|
||||
fill: $secondary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import { FileDetails, FileWrapper, FileLabel } from "components/common";
|
||||
import "./ImageInput.scss";
|
||||
|
||||
const fileTypes = ["JPG", "GIF", "PNG"];
|
||||
|
||||
interface Props {
|
||||
blob?: string;
|
||||
clear?: (file?: any) => void;
|
||||
file?: any;
|
||||
hideActions?: boolean;
|
||||
hideClearDetails?: boolean;
|
||||
inputProps?: any;
|
||||
success?: boolean;
|
||||
submit?: () => void;
|
||||
working?: boolean;
|
||||
[x:string]: any;
|
||||
}
|
||||
|
||||
const n = () => {};
|
||||
|
||||
export default function ImageInput({ blob = "", clear = n, file, hideActions, hideClearDetails, inputProps, success = false, submit = n, working = false, ...rest }: Props) {
|
||||
return <FileWrapper {...{ fileTypes, ...inputProps, ...rest }}>
|
||||
<div {...{ className: "fy-image-uploader" }}>
|
||||
{ file ? <>
|
||||
<img {...{ alt: "file preview", className: "file-preview", src: blob }} />
|
||||
<FileDetails {...{ file, hideClearDetails }}/>
|
||||
</> : <>
|
||||
<svg {...{ className: "image-placeholder", height: 400, viewBox: "0 0 300 300", width: 400 }}>
|
||||
<path d="m152.42 226c41.8 0 69.49-43.57 73.21-90.87 3.78-47.97-26.06-87.13-75.69-87.13-49.64 0-79.28 39.15-75.69 87.13 3.72 49.79 36.37 90.87 78.17 90.87zm-29.19-50.93c1.62-1.51 4.15-1.42 5.65.2 4.1 4.41 15.31 6.97 21.88 6.97s17.77-2.56 21.88-6.97c1.5-1.62 4.04-1.71 5.65-.2 1.62 1.5 1.92 3.95.2 5.65-6.31 6.27-18.31 9.52-27.74 9.52s-22.16-3.54-27.74-9.52c-1.49-1.61-1.4-4.14.22-5.65zm115.25 112.93h-176.62c-37.67 0-38.24-17.41-27.56-26.36 21.49-18.01 57.18-23.64 114.81-23.64 60.01 0 96.24 4.84 117.41 24.06 10.09 9.16 7.67 25.94-28.04 25.94z"/>
|
||||
</svg>
|
||||
<div {...{ className: "image-upload-label" }}>
|
||||
{ !file && <FileLabel {...{ fileTypes }} /> }
|
||||
</div>
|
||||
</> }
|
||||
</div>
|
||||
</FileWrapper>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ImageInput';
|
||||
@@ -0,0 +1,15 @@
|
||||
import AudioInput from './AudioInput';
|
||||
import ImageInput from './ImageInput';
|
||||
import FileActions from './FileActions';
|
||||
import FileDetails from './FileDetails';
|
||||
import FileWrapper from './FileWrapper';
|
||||
import FileLabel from './FileLabel';
|
||||
|
||||
export {
|
||||
AudioInput,
|
||||
ImageInput,
|
||||
FileActions,
|
||||
FileDetails,
|
||||
FileWrapper,
|
||||
FileLabel
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
childProps?: any;
|
||||
titleIcon?: JSX.Element;
|
||||
titleComponent?: React.ComponentType;
|
||||
title?: string;
|
||||
subText: string;
|
||||
showButtons: boolean;
|
||||
actionButtons?: JSX.Element;
|
||||
}
|
||||
|
||||
|
||||
export default function PageHeader({ actionButtons, childProps, showButtons, subText, titleComponent: Title, title, titleIcon }: Props) {
|
||||
return <div className="container-panel hero-section py-4">
|
||||
<div className="panel">
|
||||
<div className="p-3 py-4 p-md-4">
|
||||
{ Title ? <Title {...childProps }/> : <h1 className="fw-bold text-center text-md-start">
|
||||
{ titleIcon }
|
||||
{ title }
|
||||
</h1> }
|
||||
<p className="text-center text-md-start pt-1">{ subText }</p>
|
||||
{ showButtons && (
|
||||
<div className="d-flex flex-column flex-md-row gap-3 justify-content-center justify-content-md-start mt-4">
|
||||
{ actionButtons }
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
// export default PageHeader;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './PageHeader';
|
||||
@@ -0,0 +1,5 @@
|
||||
import PageHeader from './PageHeader';
|
||||
|
||||
export {
|
||||
PageHeader,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import useFile from './useFile';
|
||||
import useId from './useId';
|
||||
import useIdempotency from './useIdempotency';
|
||||
|
||||
export { useFile, useId, useIdempotency };
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./useFile";
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
const n = (o: any) => o;
|
||||
|
||||
interface Props {
|
||||
debug?: any;
|
||||
onChange?: (file: any) => void;
|
||||
onClear?: (x?: any) => void;
|
||||
onSubmit?: (file: any) => boolean;
|
||||
}
|
||||
|
||||
export default function useFile({ debug, onChange = n, onClear = n, onSubmit = n }: Props) {
|
||||
const [file, fileSet] = useState<any>(undefined);
|
||||
const [blob, blobSet] = useState<string>();
|
||||
const [working, workingSet] = useState(false);
|
||||
const [success, successSet] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const submit = async () => {
|
||||
if (file === undefined) { return false; }
|
||||
const onUploadResult = await onSubmit(file);
|
||||
workingSet(true);
|
||||
if (onUploadResult) { successSet(true); }
|
||||
successSet(false);
|
||||
};
|
||||
const fileChange = (inputFile?: any) => {
|
||||
onChange(file);
|
||||
fileSet(inputFile || null);
|
||||
blobSet(inputFile ? URL.createObjectURL(inputFile) : "");
|
||||
successSet(false);
|
||||
};
|
||||
const inputChange = ({ target = {} }: { target: any }) => {
|
||||
fileChange(target.value);
|
||||
};
|
||||
const clear = () => {
|
||||
if (inputRef?.current?.value) inputRef.current.value = '';
|
||||
fileChange();
|
||||
onClear();
|
||||
};
|
||||
|
||||
return {
|
||||
blob,
|
||||
clear,
|
||||
file,
|
||||
inputProps: {
|
||||
onChange: inputChange,
|
||||
inputRef
|
||||
},
|
||||
submit,
|
||||
success,
|
||||
working,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './useId';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const S4 = () => (((1+Math.random())*0x10000)|0).toString(16).substring(1);
|
||||
|
||||
export default function useId() {
|
||||
const [id,idSet] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) idSet(S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
|
||||
},[id]);
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
// based on https://stackoverflow.com/questions/6860853/generate-random-string-for-div-id/6860916#6860916
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './useIdempotency';
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default function useIdempotency() {
|
||||
const [token, set] = useState(uuidv4());
|
||||
return { reset: () => set(uuidv4()), set, token };
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { LoginPage } from "./pages/login/LoginPage";
|
||||
import { ModerationFc } from "./pages/moderation/moderation_main/ModerationFc";
|
||||
import { ModerationIpBanListFc } from "./pages/moderation/moderation_ip_ban_list/ModerationIpBanListFc";
|
||||
import { ModerationViewIpBanFc } from "./pages/moderation/moderation_view_ip_ban/ModerationViewIpBanFc";
|
||||
import { LipsyncEditor } from "./pages/lipsync";
|
||||
import { FooterNav } from "./nav/FooterNav";
|
||||
import { TopNav } from "./nav/TopNav";
|
||||
import { ProfileEditFc } from "./pages/profile/profile_edit/ProfileEditFc";
|
||||
@@ -429,6 +430,10 @@ class PageContainer extends React.Component<Props, State> {
|
||||
<CreateCategoryPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
|
||||
<Route path="/dev-lipsync">
|
||||
<LipsyncEditor />
|
||||
</Route>
|
||||
|
||||
<Route path="/moderation/user/list">
|
||||
<ModerationUserListFc
|
||||
sessionWrapper={this.props.sessionWrapper}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
.lipsync-editor {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-gap: 1rem;
|
||||
|
||||
.grid-heading, .grid-square {
|
||||
&:nth-child(odd) {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// background-color: blue;
|
||||
// padding-left: 1rem;
|
||||
h5 {
|
||||
margin: 1rem 0 0;
|
||||
// background: red;
|
||||
line-height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-square {
|
||||
aspect-ratio: 1;
|
||||
padding-bottom: 1rem;
|
||||
// &:nth-child(odd) {
|
||||
// padding-left: 1rem;
|
||||
// }
|
||||
// &:nth-child(even) {
|
||||
// padding-right: 1rem;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// div.grid-square { aspect-ratio: 1 }
|
||||
.fy-image-uploader { height: 100%; }
|
||||
.lipsync-audio {
|
||||
label { height: 100% }
|
||||
}
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.progress-heading {
|
||||
display: inline-block;
|
||||
margin: auto 1rem auto 0;
|
||||
}
|
||||
|
||||
.async-progress-tracker {
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
flex-grow: 1;
|
||||
margin-right: 1rem;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
svg {
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
margin-right: .5rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
stroke: white;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 0 0 auto;
|
||||
background-color: #ccccff;
|
||||
// flex-grow: 1;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
import { useFile } from "hooks";
|
||||
import { AudioInput, ImageInput } from "components/common";
|
||||
import { PageHeader } from "components/layout";
|
||||
import './LipsyncEditor.scss';
|
||||
|
||||
// interface Props {
|
||||
// audioFile?: any;
|
||||
// imageFile?: any;
|
||||
// }
|
||||
|
||||
const ProgressCheck = ({ disabled = false }: {disabled?: boolean}) => <svg>
|
||||
{ !disabled && <polyline {...{
|
||||
fill: 'none',
|
||||
points: '7 16 12 20 20 10',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
strokeWidth: '4',
|
||||
}}/> }
|
||||
</svg>;
|
||||
|
||||
const Title = ({ ...rest }) => {
|
||||
const { audioFile, imageFile } = rest;
|
||||
|
||||
return <div {...{ className: 'progress-header' }}>
|
||||
<h1 {...{ className: "fw-bold text-center text-md-start progress-heading" }}>
|
||||
Generate a lipsync
|
||||
</h1>
|
||||
<ul {...{ className: 'async-progress-tracker' }}>
|
||||
<li>
|
||||
<ProgressCheck {...{ disabled: imageFile }}/>
|
||||
Image
|
||||
</li>
|
||||
<li>
|
||||
<ProgressCheck {...{ disabled: audioFile }}/>
|
||||
Audio
|
||||
</li>
|
||||
</ul>
|
||||
<button className="btn btn-primary">
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default function LipsyncEditor({ ...rest }) {
|
||||
const audioProps = useFile({ debug: 'audio useFile' });
|
||||
const imageProps = useFile({});
|
||||
|
||||
const subText = "Select an image with a clear face, or one of our existing templates, then choose either text to speech or uploaded audio(eg. music). Then you can generate a beautifully lipsynced video.";
|
||||
const headerProps = {
|
||||
childProps: { audioFile: !audioProps.file, imageFile: !imageProps.file },
|
||||
titleComponent: Title,
|
||||
subText,
|
||||
showButtons: false
|
||||
};
|
||||
|
||||
return <div>
|
||||
<PageHeader { ...headerProps }/>
|
||||
<div {...{ className: "container" }}>
|
||||
<div {...{ className: "lipsync-editor panel" }}>
|
||||
<div {...{ className: "grid-heading" }}>
|
||||
<h5>Image</h5>
|
||||
</div>
|
||||
<div {...{ className: "grid-heading" }}>
|
||||
<h5>Audio</h5>
|
||||
</div>
|
||||
<div {...{ className: "grid-square lipsync-audio" }}>
|
||||
<ImageInput {...imageProps}/>
|
||||
</div>
|
||||
<div {...{ className: "grid-square" }}>
|
||||
<AudioInput {...{ ...audioProps, hideActions: true } }/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LipsyncEditor';
|
||||
@@ -0,0 +1,3 @@
|
||||
import LipsyncEditor from './LipsyncEditor';
|
||||
|
||||
export { LipsyncEditor };
|
||||
Reference in new issue
Block a user