mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
updates and audio player
This commit is contained in:
1 parent
a7a946780e
commit
ff529ebf31
8 files changed
+277
-48
No files matched your search
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import "./DataTable.scss";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type Data = {
|
||||
property: string;
|
||||
value: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
interface DataTableProps {
|
||||
@@ -17,7 +19,13 @@ export default function DataTable({ data }: DataTableProps) {
|
||||
{data.map((row, index) => (
|
||||
<tr key={index}>
|
||||
<td className="data-table-property">{row.property}</td>
|
||||
<td>{row.value}</td>
|
||||
{row.link ? (
|
||||
<td>
|
||||
<Link to={row.link}>{row.value}</Link>
|
||||
</td>
|
||||
) : (
|
||||
<td>{row.value}</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -14,6 +14,8 @@ interface PageHeaderProps {
|
||||
buttonLabel?: string;
|
||||
buttonVariant?: "primary" | "secondary" | "danger";
|
||||
buttonTo?: string;
|
||||
buttonIcon?: IconDefinition;
|
||||
buttonOnClick?: () => void;
|
||||
}
|
||||
|
||||
export default function PageHeader({
|
||||
@@ -26,6 +28,8 @@ export default function PageHeader({
|
||||
buttonLabel,
|
||||
buttonVariant = "primary",
|
||||
buttonTo,
|
||||
buttonIcon,
|
||||
buttonOnClick,
|
||||
}: PageHeaderProps) {
|
||||
const icon = (
|
||||
<>{titleIcon && <FontAwesomeIcon icon={titleIcon} className="me-3" />}</>
|
||||
@@ -44,9 +48,11 @@ export default function PageHeader({
|
||||
<div className="d-none d-md-block">
|
||||
{showButton && (
|
||||
<Button
|
||||
icon={buttonIcon}
|
||||
variant={buttonVariant}
|
||||
label={buttonLabel}
|
||||
to={buttonTo}
|
||||
onClick={buttonOnClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import MediaData from "./MediaDataTypes";
|
||||
|
||||
import React from "react";
|
||||
|
||||
interface AudioComponentProps {
|
||||
mediaData: MediaData;
|
||||
}
|
||||
|
||||
const AudioComponent: React.FC<AudioComponentProps> = ({ mediaData }) => {
|
||||
return (
|
||||
<div className="w-100">
|
||||
<audio className="w-100" controls>
|
||||
<source src={mediaData.public_bucket_path} />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioComponent;
|
||||
@@ -0,0 +1,17 @@
|
||||
import MediaData from "./MediaDataTypes";
|
||||
import React from "react";
|
||||
import AudioPlayer from "./MediaAudioPlayer";
|
||||
|
||||
interface MediaAudioComponentProps {
|
||||
mediaData: MediaData;
|
||||
}
|
||||
|
||||
export default function MediaAudioComponent({
|
||||
mediaData,
|
||||
}: MediaAudioComponentProps) {
|
||||
return (
|
||||
<div className="w-100">
|
||||
<AudioPlayer mediaData={mediaData} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import React from "react";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faPlay, faPause } from "@fortawesome/pro-solid-svg-icons";
|
||||
import MediaData from "./MediaDataTypes";
|
||||
|
||||
enum PlaybackSpeed {
|
||||
HALF,
|
||||
NORMAL,
|
||||
DOUBLE,
|
||||
}
|
||||
|
||||
interface MediaAudioPlayerProps {
|
||||
mediaData: MediaData;
|
||||
}
|
||||
|
||||
export default function MediaAudioPlayer({ mediaData }: MediaAudioPlayerProps) {
|
||||
let [isPlaying, setIsPlaying] = useState(false);
|
||||
let [isRepeating, setIsRepeating] = useState(false);
|
||||
let [playbackSpeed, setPlaybackSpeed] = useState(PlaybackSpeed.NORMAL);
|
||||
let [waveSurfer, setWaveSurfer] = useState<WaveSurfer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const wavesurferInstance = WaveSurfer.create({
|
||||
container: "#waveform", // Previousy I used 'this.ref.current' and React.createRef()
|
||||
height: 200,
|
||||
|
||||
responsive: true,
|
||||
waveColor: "#cbcbcb",
|
||||
progressColor: "#fc8481",
|
||||
cursorColor: "#fc6b68",
|
||||
cursorWidth: 2,
|
||||
normalize: false,
|
||||
});
|
||||
|
||||
setWaveSurfer(wavesurferInstance);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const audioLink = new BucketConfig().getGcsUrl(
|
||||
mediaData.public_bucket_path
|
||||
);
|
||||
if (waveSurfer) {
|
||||
waveSurfer.load(audioLink);
|
||||
}
|
||||
}, [waveSurfer, mediaData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (waveSurfer) {
|
||||
waveSurfer.unAll(); // NB: Otherwise we keep reinstalling the hooks and cause chaos
|
||||
waveSurfer.on("pause", () => {
|
||||
setIsPlaying(waveSurfer!.isPlaying());
|
||||
});
|
||||
waveSurfer.on("play", () => {
|
||||
setIsPlaying(waveSurfer!.isPlaying());
|
||||
});
|
||||
waveSurfer.on("finish", () => {
|
||||
if (waveSurfer && isRepeating) {
|
||||
waveSurfer!.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [waveSurfer, isRepeating]);
|
||||
|
||||
const togglePlayPause = () => {
|
||||
if (waveSurfer) {
|
||||
waveSurfer.playPause();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleIsRepeating = () => {
|
||||
setIsRepeating(!isRepeating);
|
||||
};
|
||||
|
||||
const togglePlaybackSpeed = () => {
|
||||
let nextSpeed = PlaybackSpeed.NORMAL;
|
||||
switch (playbackSpeed) {
|
||||
case PlaybackSpeed.NORMAL:
|
||||
nextSpeed = PlaybackSpeed.DOUBLE;
|
||||
waveSurfer!.setPlaybackRate(1.5); // Okay, so a lie...
|
||||
break;
|
||||
case PlaybackSpeed.DOUBLE:
|
||||
nextSpeed = PlaybackSpeed.HALF;
|
||||
waveSurfer!.setPlaybackRate(0.5);
|
||||
break;
|
||||
case PlaybackSpeed.HALF:
|
||||
nextSpeed = PlaybackSpeed.NORMAL;
|
||||
waveSurfer!.setPlaybackRate(1.0);
|
||||
break;
|
||||
}
|
||||
setPlaybackSpeed(nextSpeed);
|
||||
};
|
||||
|
||||
let playButtonText = (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faPlay} />
|
||||
</>
|
||||
);
|
||||
if (isPlaying) {
|
||||
playButtonText = (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faPause} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let repeatButtonText = isRepeating ? "Repeat" : "NoRepeat";
|
||||
|
||||
let speedButtonText = "1x";
|
||||
switch (playbackSpeed) {
|
||||
case PlaybackSpeed.NORMAL:
|
||||
speedButtonText = "1x";
|
||||
break;
|
||||
case PlaybackSpeed.DOUBLE:
|
||||
speedButtonText = "2x";
|
||||
break;
|
||||
case PlaybackSpeed.HALF:
|
||||
speedButtonText = "1/2x";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="waveform"></div>
|
||||
<div className="d-flex gap-4 flex-column justify-content-center align-items-center mt-4">
|
||||
<div className="d-flex gap-2">
|
||||
<button className="btn btn-primary" onClick={() => togglePlayPause()}>
|
||||
{playButtonText}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => toggleIsRepeating()}
|
||||
>
|
||||
{repeatButtonText}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => togglePlaybackSpeed()}
|
||||
>
|
||||
{speedButtonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+6
-6
@@ -1,21 +1,21 @@
|
||||
import React from "react";
|
||||
import MediaData from "./MediaDataTypes";
|
||||
|
||||
interface ImageComponentProps {
|
||||
interface MediaImageComponentProps {
|
||||
mediaData: MediaData;
|
||||
}
|
||||
|
||||
const ImageComponent: React.FC<ImageComponentProps> = ({ mediaData }) => {
|
||||
export default function MediaImageComponent({
|
||||
mediaData,
|
||||
}: MediaImageComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={mediaData.public_bucket_path}
|
||||
alt="test" // Make sure to provide alt text
|
||||
alt="test"
|
||||
width="400"
|
||||
height="300"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageComponent;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import AudioComponent from "./AudioComponent";
|
||||
import VideoComponent from "./VideoComponent";
|
||||
import ImageComponent from "./ImageComponent";
|
||||
import MediaAudioComponent from "./MediaAudioComponent";
|
||||
import MediaVideoComponent from "./MediaVideoComponent";
|
||||
import MediaImageComponent from "./MediaImageComponent";
|
||||
import MediaData from "./MediaDataTypes";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import Container from "components/common/Container";
|
||||
@@ -10,11 +10,16 @@ import Panel from "components/common/Panel";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import Skeleton from "components/common/Skeleton";
|
||||
import Button from "components/common/Button";
|
||||
import { faCircleExclamation } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faCircleExclamation,
|
||||
faArrowDownToLine,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import Accordion from "components/common/Accordion";
|
||||
import DataTable from "components/common/DataTable";
|
||||
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import useTimeAgo from "hooks/useTimeAgo";
|
||||
// import { RatingButtons } from "v2/view/_common/ratings/RatingButtons";
|
||||
// import { RatingStats } from "v2/view/_common/ratings/RatingStats";
|
||||
|
||||
interface MediaPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -69,16 +74,16 @@ export default function MediaPage({ sessionWrapper }: MediaPageProps) {
|
||||
function renderMediaComponent(data: MediaData) {
|
||||
switch (data.media_type) {
|
||||
case "audio":
|
||||
return <AudioComponent mediaData={data} />;
|
||||
return <MediaAudioComponent mediaData={data} />;
|
||||
case "video":
|
||||
return (
|
||||
<div className="ratio ratio-16x9">
|
||||
<VideoComponent mediaData={data} />
|
||||
<MediaVideoComponent mediaData={data} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case "image":
|
||||
return <ImageComponent mediaData={data} />;
|
||||
return <MediaImageComponent mediaData={data} />;
|
||||
default:
|
||||
return <div>Unsupported media type</div>;
|
||||
}
|
||||
@@ -124,29 +129,94 @@ export default function MediaPage({ sessionWrapper }: MediaPageProps) {
|
||||
</Container>
|
||||
);
|
||||
|
||||
const mediaDetails = [
|
||||
const audioDetails = [
|
||||
{ property: "Type", value: mediaData.media_type },
|
||||
{ property: "Created at", value: mediaData.created_at },
|
||||
{ property: "Visibility", value: mediaData.creator_set_visibility },
|
||||
{
|
||||
property: "Model",
|
||||
value: mediaData.model_used,
|
||||
link: mediaData.model_link,
|
||||
},
|
||||
{
|
||||
property: "Vocoder",
|
||||
value: "Test",
|
||||
},
|
||||
{
|
||||
property: "Language",
|
||||
value: mediaData.model_used,
|
||||
},
|
||||
{
|
||||
property: "Model",
|
||||
value: mediaData.model_used,
|
||||
},
|
||||
];
|
||||
|
||||
const videoDetails = [
|
||||
{ property: "Type", value: mediaData.media_type },
|
||||
{ property: "Created at", value: mediaData.created_at },
|
||||
{ property: "Visibility", value: mediaData.creator_set_visibility },
|
||||
];
|
||||
|
||||
let pageTitle = "Result";
|
||||
let pageSubText = "This is the result of your media.";
|
||||
let mediaDetails = undefined;
|
||||
|
||||
switch (mediaData.media_type) {
|
||||
case "audio":
|
||||
pageTitle = "Audio Result";
|
||||
pageSubText = mediaData.token;
|
||||
mediaDetails = (
|
||||
<Accordion.Item title="Audio Details" defaultOpen={true}>
|
||||
<DataTable data={audioDetails} />
|
||||
</Accordion.Item>
|
||||
);
|
||||
break;
|
||||
case "video":
|
||||
pageTitle = "Video Result";
|
||||
pageSubText = mediaData.token;
|
||||
mediaDetails = (
|
||||
<Accordion.Item title="Video Details" defaultOpen={true}>
|
||||
<DataTable data={videoDetails} />
|
||||
</Accordion.Item>
|
||||
);
|
||||
break;
|
||||
case "image":
|
||||
pageTitle = "Image Model";
|
||||
pageSubText = "Image Model SubText";
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
// const resultRatings = (
|
||||
// <div className="d-flex flex-column flex-lg-row flex-column-reverse gap-3">
|
||||
// <div className="d-flex gap-3">
|
||||
// <RatingButtons entity_type="v2v_model" entity_token="test" />
|
||||
// </div>
|
||||
// <RatingStats positive_votes={100} negative_votes={0} total_votes={100} />
|
||||
// </div>
|
||||
// );
|
||||
|
||||
return (
|
||||
<Container type="panel">
|
||||
<PageHeader
|
||||
title="Model name"
|
||||
subText={mediaData.maybe_creator_user.display_name}
|
||||
/>
|
||||
<PageHeader title={pageTitle} subText={pageSubText} />
|
||||
<Panel padding={true}>
|
||||
<div className="row g-4">
|
||||
<div className="col-12 col-xl-8">
|
||||
{renderMediaComponent(mediaData)}
|
||||
<div className="media-wrapper">
|
||||
{renderMediaComponent(mediaData)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-xl-4">
|
||||
<div className="d-flex mb-3 gap-2">
|
||||
<div className="d-flex mb-4">
|
||||
<Button
|
||||
icon={faArrowDownToLine}
|
||||
label="Download Result"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<Accordion>{mediaDetails}</Accordion>
|
||||
<div className="d-flex mt-4 gap-2">
|
||||
<Gravatar
|
||||
size={48}
|
||||
username={mediaData.maybe_creator_user.display_name}
|
||||
@@ -167,11 +237,6 @@ export default function MediaPage({ sessionWrapper }: MediaPageProps) {
|
||||
{timeCreated}
|
||||
</div>
|
||||
</div>
|
||||
<Accordion>
|
||||
<Accordion.Item title="Details" defaultOpen={true}>
|
||||
<DataTable data={mediaDetails} />
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
+5
-2
@@ -1,10 +1,13 @@
|
||||
import React from "react";
|
||||
import MediaData from "./MediaDataTypes";
|
||||
interface VideoComponentProps {
|
||||
|
||||
interface MediaVideoComponentProps {
|
||||
mediaData: MediaData;
|
||||
}
|
||||
|
||||
export default function VideoComponent({ mediaData }: VideoComponentProps) {
|
||||
export default function MediaVideoComponent({
|
||||
mediaData,
|
||||
}: MediaVideoComponentProps) {
|
||||
return (
|
||||
<video className="rounded" controls width="100%" height="auto">
|
||||
<source src={mediaData.public_bucket_path} />
|
||||
Reference in new issue
Block a user