mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
upload component update
This commit is contained in:
1 parent
74eccc3d50
commit
fbbdd03ebf
7 files changed
+258
-21
No files matched your search
@@ -1158,6 +1158,14 @@ styles with important tags:
|
||||
}
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 14px;
|
||||
color: rgba($white, 0.75);
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
background-color: $secondary;
|
||||
}
|
||||
|
||||
.nav-vc {
|
||||
flex-wrap: nowrap;
|
||||
height: auto;
|
||||
@@ -1311,6 +1319,10 @@ styles with important tags:
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.bg-stepper {
|
||||
background-color: lighten($panel, 4%);
|
||||
}
|
||||
|
||||
.stepper-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -1318,14 +1330,14 @@ styles with important tags:
|
||||
align-items: center;
|
||||
|
||||
.stepper-number {
|
||||
font-size: 16px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
background-color: $secondary;
|
||||
border-radius: 48px;
|
||||
opacity: 0.4;
|
||||
@@ -1354,6 +1366,7 @@ styles with important tags:
|
||||
height: 3px;
|
||||
background: $secondary;
|
||||
transition: all 0.2s;
|
||||
min-width: 80px;
|
||||
|
||||
&.active {
|
||||
background-color: $primary;
|
||||
@@ -1484,6 +1497,11 @@ textarea {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
|
||||
.px-lg-4 {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 2rem !important;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ import { panel } from "data/animation";
|
||||
|
||||
interface PanelProps {
|
||||
children: React.ReactNode;
|
||||
padding?: boolean;
|
||||
}
|
||||
|
||||
function Panel(props: PanelProps) {
|
||||
return (
|
||||
<motion.div className="container-panel pb-5 mb-4" variants={panel}>
|
||||
<div className="panel p-3 py-4 p-md-4">{props.children}</div>
|
||||
<motion.div className="container-panel" variants={panel}>
|
||||
<div className={`panel ${props.padding ? "p-3 py-4 p-md-4" : ""}`}>
|
||||
{props.children}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,12 +54,20 @@ function VoiceDesignerPage(this: any) {
|
||||
|
||||
<Panel>
|
||||
{/* Stepper */}
|
||||
<Stepper steps={steps} currentStep={currentStep} />
|
||||
<div className="p-3 px-lg-4 bg-stepper">
|
||||
<Stepper steps={steps} currentStep={currentStep} />
|
||||
</div>
|
||||
|
||||
<div className="my-4">{displayStep(currentStep)}</div>
|
||||
<div className="p-3 py-4 p-md-4">{displayStep(currentStep)}</div>
|
||||
|
||||
{/* Navigation Controls */}
|
||||
<StepperControls onBack={handleBack} onNext={handleNext} />
|
||||
|
||||
<StepperControls
|
||||
steps={steps}
|
||||
currentStep={currentStep}
|
||||
onBack={handleBack}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
</Panel>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ const Stepper: React.FC<StepperProps> = ({ steps, currentStep }) => {
|
||||
</div>
|
||||
{index !== steps.length - 1 && (
|
||||
<div
|
||||
className={`stepper-line ${index < currentStep ? "active" : ""}`}
|
||||
className={`stepper-line ${index <= currentStep ? "active" : ""}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+21
-8
@@ -3,21 +3,34 @@ import React from "react";
|
||||
interface StepperControlsProps {
|
||||
onBack: () => void;
|
||||
onNext: () => void;
|
||||
steps: string[];
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
const StepperControls: React.FC<StepperControlsProps> = ({
|
||||
onBack,
|
||||
onNext,
|
||||
steps,
|
||||
currentStep,
|
||||
}) => {
|
||||
return (
|
||||
<div className="d-flex gap-3">
|
||||
<button className="btn btn-secondary w-100" onClick={onBack}>
|
||||
Back
|
||||
</button>
|
||||
<button className="btn btn-primary w-100" onClick={onNext}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
{currentStep !== steps.length - 1 && (
|
||||
<div className="p-3 pb-4 px-lg-4 pt-0 d-flex gap-3">
|
||||
{currentStep !== 0 && (
|
||||
<button className="btn btn-secondary w-100" onClick={onBack}>
|
||||
Back
|
||||
</button>
|
||||
)}
|
||||
|
||||
{currentStep !== steps.length - 1 && (
|
||||
<button className="btn btn-primary w-100" onClick={onNext}>
|
||||
{currentStep !== steps.length - 2 ? "Continue" : "Create Voice"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
faFileArrowUp,
|
||||
faFileAudio,
|
||||
faTrash,
|
||||
faWaveform,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useState } from "react";
|
||||
import { FileUploader } from "react-drag-drop-files";
|
||||
import { InputVcAudioPlayer } from "v2/view/_common/InputVcAudioPlayer";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
UploadAudio,
|
||||
UploadAudioIsOk,
|
||||
UploadAudioRequest,
|
||||
} from "@storyteller/components/src/api/upload/UploadAudio";
|
||||
|
||||
const FILE_TYPES = ["MP3", "WAV", "FLAC", "OGG"];
|
||||
|
||||
interface Props {
|
||||
setMediaUploadToken: (token?: string) => void;
|
||||
formIsCleared: boolean;
|
||||
setFormIsCleared: (cleared: boolean) => void;
|
||||
setCanConvert: (canConvert: boolean) => void;
|
||||
changeConvertIdempotencyToken: () => void;
|
||||
}
|
||||
|
||||
function VoiceDesignerUploadComponent(props: Props) {
|
||||
const [files, setFiles] = useState<any[]>([]);
|
||||
const [audioLinks, setAudioLinks] = useState<string[]>([]);
|
||||
const [isUploadDisabled, setIsUploadDisabled] = useState<boolean>(false);
|
||||
|
||||
// Auto generated
|
||||
const [idempotencyToken, setIdempotencyToken] = useState(uuidv4());
|
||||
|
||||
const handleChange = (file: any) => {
|
||||
if (files.length < 20) {
|
||||
setFiles([...files, file]);
|
||||
setIdempotencyToken(uuidv4());
|
||||
const audioUrl = URL.createObjectURL(file);
|
||||
setAudioLinks([...audioLinks, audioUrl ?? ""]);
|
||||
props.setFormIsCleared(false);
|
||||
props.setCanConvert(false);
|
||||
props.changeConvertIdempotencyToken();
|
||||
setIsUploadDisabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.currentTarget.classList.add("upload-zone-drag");
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.currentTarget.classList.remove("upload-zone-drag");
|
||||
};
|
||||
|
||||
const getFileSize = (file: any) =>
|
||||
file && file.size >= 1024 * 1024
|
||||
? (file.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
: file
|
||||
? `${Math.floor(file.size / 1024)} KB`
|
||||
: null;
|
||||
|
||||
const uploadBtnClass = isUploadDisabled
|
||||
? "btn btn-uploaded w-100 disabled"
|
||||
: "btn btn-primary w-100";
|
||||
|
||||
const handleRemoveFile = (index: number) => {
|
||||
const updatedFiles = [...files];
|
||||
updatedFiles.splice(index, 1);
|
||||
setFiles(updatedFiles);
|
||||
|
||||
const updatedAudioLinks = [...audioLinks];
|
||||
updatedAudioLinks.splice(index, 1);
|
||||
setAudioLinks(updatedAudioLinks);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column gap-3">
|
||||
{/* Usage refer to https://github.com/KarimMokhtar/react-drag-drop-files */}
|
||||
<FileUploader
|
||||
handleChange={handleChange}
|
||||
name="file"
|
||||
types={FILE_TYPES}
|
||||
maxSize={50}
|
||||
children={
|
||||
<div
|
||||
className="panel panel-inner upload-zone d-flex align-items-center justify-content-center p-4"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<div className="me-4">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="upload-icon" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="pb-0">
|
||||
<u className="fw-medium">Upload files</u> or drop them here...
|
||||
</div>
|
||||
<div className="d-flex gap-1">
|
||||
<div>
|
||||
<p className="opacity-50">
|
||||
{FILE_TYPES.join(", ").toString()} supported
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<h4 className="m-0 mt-3">
|
||||
Voice Samples <span className="opacity-75">({files.length}/20)</span>
|
||||
</h4>
|
||||
{files.length > 0 ? (
|
||||
<div className="d-flex flex-column gap-3">
|
||||
{files.map((file, index) => (
|
||||
<div key={index} className="panel panel-inner rounded p-3">
|
||||
<div className="d-flex flex-column gap-2">
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="flex-grow-1 mb-1">
|
||||
{file.name}
|
||||
<span className="file-size ms-2">{getFileSize(file)}</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-link p-0"
|
||||
onClick={() => handleRemoveFile(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="me-2" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<InputVcAudioPlayer filename={audioLinks[index]} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="panel panel-inner text-center p-5 rounded-5 h-100">
|
||||
<div className="d-flex flex-column opacity-75 h-100 justify-content-center">
|
||||
<FontAwesomeIcon icon={faWaveform} className="fs-3 mb-3" />
|
||||
<h5>No voice samples yet</h5>
|
||||
<p>Selected samples will appear here.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const LoadingIcon: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm ms-3"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceDesignerUploadComponent;
|
||||
+30
-3
@@ -1,9 +1,36 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import VoiceDesignerUploadComponent from "../VoiceDesignerUploadComponent";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
function UploadSamples() {
|
||||
const [convertLoading, setConvertLoading] = useState(false);
|
||||
const [canConvert, setCanConvert] = useState(false);
|
||||
const [mediaUploadToken, setMediaUploadToken] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [convertIdempotencyToken, setConvertIdempotencyToken] = useState(
|
||||
uuidv4()
|
||||
);
|
||||
const [formIsCleared, setFormIsCleared] = useState(false);
|
||||
|
||||
const changeConvertIdempotencyToken = () => {
|
||||
setConvertIdempotencyToken(uuidv4());
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input className="form-control" type="text" />
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div>
|
||||
<label className="sub-title">Upload Audio</label>
|
||||
<div className="d-flex flex-column gap-3 upload-component">
|
||||
<VoiceDesignerUploadComponent
|
||||
setMediaUploadToken={setMediaUploadToken}
|
||||
formIsCleared={formIsCleared}
|
||||
setFormIsCleared={setFormIsCleared}
|
||||
setCanConvert={setCanConvert}
|
||||
changeConvertIdempotencyToken={changeConvertIdempotencyToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user