mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
Merge pull request #309 from storytold/bt-bflat-fy-media-page
media file pages (BFlat's work)
This commit is contained in:
36 files changed
+1170
-185
No files matched your search
@@ -410,6 +410,12 @@ class ApiConfig {
|
||||
return `${this.getApiOrigin()}/voice_clone_requests/check`;
|
||||
}
|
||||
|
||||
// =============== Media Files ===============
|
||||
|
||||
getMediaFile(mediaFileToken: string): string {
|
||||
return `${this.getApiOrigin()}/v1/media_files/file/${mediaFileToken}`;
|
||||
}
|
||||
|
||||
// =============== File Uploads ===============
|
||||
|
||||
uploadAudio(): string {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/// A common type returned by several endpoints.
|
||||
/// Basic information to display a user and their avatar.
|
||||
export interface UserDetailsLight {
|
||||
user_token: string,
|
||||
/// Username (lowercase)
|
||||
username: string,
|
||||
/// Username with user-specified capitalization
|
||||
display_name: string,
|
||||
gravatar_hash: string,
|
||||
default_avatar: DefaultAvatarInfo,
|
||||
|
||||
}
|
||||
|
||||
export interface DefaultAvatarInfo {
|
||||
image_index: number,
|
||||
color_index: number,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
export enum MediaFileType {
|
||||
Audio = "audio",
|
||||
Video = "video",
|
||||
Image = "image",
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ApiConfig } from "../ApiConfig";
|
||||
import { UserDetailsLight } from "../_common/UserDetailsLight";
|
||||
import { MediaFileType } from "../_common/enums/MediaFileType";
|
||||
|
||||
export interface MediaFile {
|
||||
token: string,
|
||||
media_type: MediaFileType,
|
||||
public_bucket_path: string,
|
||||
maybe_creator_user: UserDetailsLight | null,
|
||||
creator_set_visibility: string,
|
||||
created_at: Date,
|
||||
updated_at: Date,
|
||||
}
|
||||
|
||||
export enum MediaFileLookupError {
|
||||
NotFound,
|
||||
ServerError,
|
||||
FrontendError,
|
||||
}
|
||||
|
||||
export interface GetMediaFileResponse {
|
||||
success: boolean,
|
||||
media_file?: MediaFile,
|
||||
}
|
||||
|
||||
export async function GetMediaFile(mediaFileToken: string) : Promise<GetMediaFileResponse> {
|
||||
const endpoint = new ApiConfig().getMediaFile(mediaFileToken);
|
||||
|
||||
return await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
const response : GetMediaFileResponse = res;
|
||||
|
||||
if (response && response.success && response.media_file) {
|
||||
// NB: Timestamps aren't converted to Date objects on their own!
|
||||
response.media_file.created_at = new Date(response.media_file.created_at);
|
||||
response.media_file.updated_at = new Date(response.media_file.updated_at);
|
||||
return response;
|
||||
} else {
|
||||
return { success : false };
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
return { success : false };
|
||||
});
|
||||
}
|
||||
@@ -31,9 +31,10 @@
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"axios": "^0.21.0",
|
||||
"bootstrap": "5.1.3",
|
||||
"bulma": "^0.9.0",
|
||||
"craco": "^0.0.3",
|
||||
"date-fns": "^2.22.1",
|
||||
"date-fns": "2.22.1",
|
||||
"framer-motion": "^6.5.1",
|
||||
"howler": "^2.2.0",
|
||||
"i18next": "^21.6.11",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./AppNew.scss";
|
||||
import "scss/custom-bootstrap.scss";
|
||||
|
||||
import React from "react";
|
||||
import { ApiConfig } from "@storyteller/components";
|
||||
@@ -184,12 +185,15 @@ function newVocodes() {
|
||||
return discord || twitter || alphaCookie;
|
||||
}
|
||||
|
||||
function initInferenceJobsByCategoryMap() : Map<FrontendInferenceJobType, InferenceJob[]> {
|
||||
let inferenceJobsByCategory = new Map();
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.FaceAnimation, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.TextToSpeech, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.VoiceConversion, []);
|
||||
return inferenceJobsByCategory;
|
||||
function initInferenceJobsByCategoryMap(): Map<
|
||||
FrontendInferenceJobType,
|
||||
InferenceJob[]
|
||||
> {
|
||||
let inferenceJobsByCategory = new Map();
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.FaceAnimation, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.TextToSpeech, []);
|
||||
inferenceJobsByCategory.set(FrontendInferenceJobType.VoiceConversion, []);
|
||||
return inferenceJobsByCategory;
|
||||
}
|
||||
|
||||
function isMacOs() {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.fy-accordion-item {
|
||||
border: 1px solid rgba($white, 0.12);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fy-accordion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: $secondary;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&.open {
|
||||
background-color: lighten($secondary, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.fy-accordion-content {
|
||||
overflow: hidden;
|
||||
background-color: lighten($panel, 4%);
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
max-height: 100%;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-accordion-content table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.caret-icon {
|
||||
transition: transform 0.2s ease;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.caret-rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
createContext,
|
||||
useContext,
|
||||
} from "react";
|
||||
import "./Accordion.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { animated, useSpring } from "@react-spring/web";
|
||||
|
||||
interface AccordionProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AccordionItemProps {
|
||||
title: string;
|
||||
defaultOpen?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AccordionContextType {
|
||||
openItems: string[];
|
||||
toggleItem: (title: string) => void;
|
||||
}
|
||||
|
||||
const AccordionContext = createContext<AccordionContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
function AccordionItem({
|
||||
title,
|
||||
defaultOpen = false,
|
||||
children,
|
||||
}: AccordionItemProps) {
|
||||
const { openItems, toggleItem } = useContext(
|
||||
AccordionContext
|
||||
) as AccordionContextType;
|
||||
const isOpen = openItems.includes(title);
|
||||
|
||||
const [contentHeight, setContentHeight] = useState<number | undefined>(
|
||||
isOpen ? undefined : 0
|
||||
);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
setContentHeight(isOpen ? contentRef.current.scrollHeight : 0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const heightProps = useSpring({
|
||||
height: isOpen ? `${contentHeight}px` : "0px",
|
||||
config: { tension: 300, friction: 25, clamp: true },
|
||||
});
|
||||
|
||||
const contentOpacityProps = useSpring({
|
||||
opacity: isOpen ? 1 : 0,
|
||||
config: { tension: 300, friction: 25, clamp: true },
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
toggleItem(title);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fy-accordion-item">
|
||||
<div
|
||||
className={`fy-accordion-header p-3 ${isOpen ? "open" : ""}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{title}
|
||||
<span className={`caret-icon ${isOpen ? "caret-rotated" : ""}`}>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</span>
|
||||
</div>
|
||||
<animated.div style={{ ...heightProps, overflow: "hidden" }}>
|
||||
<animated.div
|
||||
ref={contentRef}
|
||||
style={contentOpacityProps}
|
||||
className="fy-accordion-content"
|
||||
>
|
||||
{children}
|
||||
</animated.div>
|
||||
</animated.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Accordion({ children }: AccordionProps) {
|
||||
const [openItems, setOpenItems] = useState<string[]>([]);
|
||||
|
||||
const toggleItem = (title: string) => {
|
||||
if (openItems.includes(title)) {
|
||||
setOpenItems(openItems.filter((item) => item !== title));
|
||||
} else {
|
||||
setOpenItems([...openItems, title]);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize openItems based on defaultOpen props of AccordionItem children
|
||||
useEffect(() => {
|
||||
const defaultOpenItems = React.Children.toArray(children)
|
||||
.filter((child: any) => child.props.defaultOpen)
|
||||
.map((child: any) => child.props.title);
|
||||
setOpenItems(defaultOpenItems);
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openItems, toggleItem }}>
|
||||
<div className="d-flex flex-column gap-3">{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Accordion.Item = AccordionItem;
|
||||
|
||||
export default Accordion;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Accordion";
|
||||
@@ -11,6 +11,16 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $white !important;
|
||||
|
||||
&-square {
|
||||
width: 46.5px;
|
||||
height: 46.5px;
|
||||
}
|
||||
|
||||
&-square-small {
|
||||
width: 37.5px;
|
||||
height: 37.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.button-primary,
|
||||
@@ -45,9 +55,9 @@
|
||||
border-color: lighten($secondary, 5%);
|
||||
}
|
||||
|
||||
.button-destructive,
|
||||
.button-destructive:focus,
|
||||
.button-destructive:active {
|
||||
.button-danger,
|
||||
.button-danger:focus,
|
||||
.button-danger:active {
|
||||
background-color: $danger;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
@@ -55,7 +65,7 @@
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.button-destructive:hover {
|
||||
.button-danger:hover {
|
||||
background-color: lighten($danger, 5%);
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
|
||||
@@ -3,15 +3,20 @@ import { Link } from "react-router-dom";
|
||||
import "./Button.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
label?: string;
|
||||
label?: string | null;
|
||||
icon?: IconDefinition;
|
||||
small?: boolean;
|
||||
variant?: "primary" | "secondary" | "danger";
|
||||
to?: string; // for internal links
|
||||
href?: string; // for external links
|
||||
to?: string;
|
||||
href?: string;
|
||||
target?: "_blank" | "_self";
|
||||
square?: boolean;
|
||||
tooltip?: string;
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
@@ -22,56 +27,52 @@ export default function Button({
|
||||
to,
|
||||
href,
|
||||
target = "_self",
|
||||
square,
|
||||
tooltip,
|
||||
full = false,
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
let buttonClass = `button`;
|
||||
let IconComponent = icon ? (
|
||||
<FontAwesomeIcon icon={icon} className={!square && label ? "me-2" : ""} />
|
||||
) : null;
|
||||
let LabelComponent = !square ? label : null;
|
||||
|
||||
if (small) {
|
||||
buttonClass += ` button-small`;
|
||||
}
|
||||
const externalClass = rest.className || "";
|
||||
|
||||
switch (variant) {
|
||||
case "primary":
|
||||
buttonClass += " button-primary";
|
||||
break;
|
||||
case "secondary":
|
||||
buttonClass += " button-secondary";
|
||||
break;
|
||||
case "danger":
|
||||
buttonClass += " button-destructive";
|
||||
break;
|
||||
default:
|
||||
buttonClass += " button-primary";
|
||||
break;
|
||||
}
|
||||
const commonProps = {
|
||||
className: `${externalClass} button ${small ? "button-small" : ""} ${
|
||||
square ? (small ? "button-square-small" : "button-square") : ""
|
||||
} button-${variant} ${full ? "w-100" : ""}`,
|
||||
};
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
<Link to={to} className={buttonClass}>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
if (href) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
rel="noopener noreferrer"
|
||||
className={buttonClass}
|
||||
>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
delete rest.className;
|
||||
|
||||
// If neither 'to' nor 'href' are provided (regular button)
|
||||
return (
|
||||
<button className={buttonClass} type="button" {...rest}>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
|
||||
{label}
|
||||
const ButtonContent = (
|
||||
<>
|
||||
{IconComponent}
|
||||
{LabelComponent}
|
||||
</>
|
||||
);
|
||||
|
||||
const WrappedButton = to ? (
|
||||
<Link to={to} {...commonProps}>
|
||||
{ButtonContent}
|
||||
</Link>
|
||||
) : href ? (
|
||||
<a href={href} target={target} rel="noopener noreferrer" {...commonProps}>
|
||||
{ButtonContent}
|
||||
</a>
|
||||
) : (
|
||||
<button type="button" {...commonProps} {...rest}>
|
||||
{ButtonContent}
|
||||
</button>
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tippy theme="fakeyou" content={tooltip}>
|
||||
{WrappedButton}
|
||||
</Tippy>
|
||||
) : (
|
||||
WrappedButton
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,13 @@ import React from "react";
|
||||
interface ContainerProps {
|
||||
children: React.ReactNode;
|
||||
type?: "full" | "padded" | "panel";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function PageContainer({
|
||||
children,
|
||||
type = "panel",
|
||||
type = "full",
|
||||
className = "",
|
||||
}: ContainerProps) {
|
||||
let containerClass = "";
|
||||
|
||||
@@ -19,12 +21,15 @@ export default function PageContainer({
|
||||
containerClass = "container";
|
||||
break;
|
||||
case "panel":
|
||||
containerClass = "";
|
||||
containerClass = "container-panel";
|
||||
break;
|
||||
default:
|
||||
containerClass = "container-fluid";
|
||||
break;
|
||||
}
|
||||
|
||||
return <div className={containerClass}>{children}</div>;
|
||||
// Merge the classNames
|
||||
const mergedClassNames = `${containerClass} ${className}`;
|
||||
|
||||
return <div className={mergedClassNames}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
table {
|
||||
font-size: 14px;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 16px !important;
|
||||
min-width: 124px !important;
|
||||
}
|
||||
|
||||
.no-outer-border {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > thead > tr > th:first-child,
|
||||
.no-outer-border > tbody > tr > td:first-child {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > thead > tr > th:last-child,
|
||||
.no-outer-border > tbody > tr > td:last-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > tbody > tr:last-child > td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.data-table-property {
|
||||
font-weight: 500 !important;
|
||||
background-color: lighten($panel, 7%) !important;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import "./DataTable.scss";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type Data = {
|
||||
property: string;
|
||||
value: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
interface DataTableProps {
|
||||
data: Data[];
|
||||
}
|
||||
|
||||
export default function DataTable({ data }: DataTableProps) {
|
||||
return (
|
||||
<table className="table no-outer-border">
|
||||
<tbody>
|
||||
{data.map((row, index) => (
|
||||
<tr key={index}>
|
||||
<td className="data-table-property">{row.property}</td>
|
||||
{row.link ? (
|
||||
<td>
|
||||
<Link to={row.link}>{row.value}</Link>
|
||||
</td>
|
||||
) : (
|
||||
<td>{row.value}</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DataTable";
|
||||
@@ -5,18 +5,15 @@ interface PanelProps {
|
||||
padding?: boolean;
|
||||
mb?: boolean;
|
||||
mt?: boolean;
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export default function Panel({ children, padding, mb, mt, full }: PanelProps) {
|
||||
export default function Panel({ children, padding, mb, mt }: PanelProps) {
|
||||
return (
|
||||
<div className={full ? "container-panel mw-100" : "container-panel"}>
|
||||
<div
|
||||
className={`panel ${padding ? "p-3 py-4 p-md-4" : ""}
|
||||
<div
|
||||
className={`panel ${padding ? "p-3 py-4 p-md-4" : ""}
|
||||
${mb ? "mb-4" : ""} ${mt ? "mt-4" : ""}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
-90deg,
|
||||
rgba($white, 10%) 25%,
|
||||
rgba(darken($white, 20%), 10%) 50%,
|
||||
rgba($white, 10%) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: 0.25rem;
|
||||
width: 100%;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton.rounded {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.skeleton.medium {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.skeleton.short {
|
||||
width: 20%;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import "./Skeleton.scss";
|
||||
|
||||
interface SkeletonProps {
|
||||
type?: "full" | "medium" | "short";
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
export default function Skeleton({ type = "full", rounded }: SkeletonProps) {
|
||||
const skeletonClass = `skeleton ${type} ${rounded && "rounded"}`;
|
||||
|
||||
return <div className={skeletonClass}></div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Skeleton";
|
||||
@@ -6,7 +6,7 @@ import Button from "components/common/Button";
|
||||
|
||||
interface PageHeaderProps {
|
||||
titleIcon?: IconDefinition;
|
||||
title: string;
|
||||
title: string | React.ReactNode;
|
||||
subText: string | React.ReactNode;
|
||||
full?: boolean;
|
||||
showButton?: boolean;
|
||||
@@ -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" />}</>
|
||||
@@ -33,7 +37,7 @@ export default function PageHeader({
|
||||
|
||||
return (
|
||||
<div className="pt-3 pb-4 pt-lg-4">
|
||||
<Panel padding full={full}>
|
||||
<Panel padding>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div>
|
||||
<div className="d-flex">
|
||||
@@ -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
-1
@@ -35,7 +35,7 @@ export default function PageHeaderModelView({
|
||||
return (
|
||||
<div className="pt-3 pb-4 pt-lg-4">
|
||||
{view === "regular" && (
|
||||
<Panel padding full={full}>
|
||||
<Panel padding>
|
||||
<div className="row gy-3">
|
||||
<div className="col-12 col-lg-8">
|
||||
<h2 className="fw-bold">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./useTimeAgo";
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function useTimeAgo(timestamp: string) {
|
||||
const [timeAgo, setTimeAgo] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const calculateTimeAgo = () => {
|
||||
const now = new Date();
|
||||
const then = new Date(timestamp);
|
||||
|
||||
const seconds = Math.floor((now.getTime() - then.getTime()) / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
const months = Math.floor(days / 30);
|
||||
const years = Math.floor(months / 12);
|
||||
|
||||
if (years > 0) {
|
||||
setTimeAgo(`${years} ${years === 1 ? "year" : "years"} ago`);
|
||||
} else if (months > 0) {
|
||||
setTimeAgo(`${months} ${months === 1 ? "month" : "months"} ago`);
|
||||
} else if (days > 0) {
|
||||
setTimeAgo(`${days} ${days === 1 ? "day" : "days"} ago`);
|
||||
} else if (hours > 0) {
|
||||
setTimeAgo(`${hours} ${hours === 1 ? "hour" : "hours"} ago`);
|
||||
} else if (minutes > 0) {
|
||||
setTimeAgo(`${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`);
|
||||
} else {
|
||||
setTimeAgo(`${seconds} ${seconds === 1 ? "second" : "seconds"} ago`);
|
||||
}
|
||||
};
|
||||
|
||||
calculateTimeAgo();
|
||||
|
||||
// Update the time difference every minute
|
||||
const intervalId = setInterval(calculateTimeAgo, 60000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [timestamp]);
|
||||
|
||||
return timeAgo;
|
||||
}
|
||||
|
||||
export default useTimeAgo;
|
||||
@@ -1,32 +1,40 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import AppTranslated from './AppTranslated';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import AppTranslated from "./AppTranslated";
|
||||
|
||||
const IS_IOS : boolean = /iPad|iPhone|iPod/.test(navigator.platform || "");
|
||||
const IS_IOS: boolean = /iPad|iPhone|iPod/.test(navigator.platform || "");
|
||||
|
||||
const enableSpectrograms = !IS_IOS;
|
||||
|
||||
const flashVocodesNotice = new URLSearchParams(window.location.search).has('vocodes');
|
||||
const flashVocodesNotice = new URLSearchParams(window.location.search).has(
|
||||
"vocodes"
|
||||
);
|
||||
|
||||
const designSystemClass = 'fakeyou-refresh';
|
||||
const designSystemClass = "fakeyou-refresh";
|
||||
|
||||
document.getElementsByTagName('html')[0].classList.add(designSystemClass);
|
||||
document.getElementsByTagName("html")[0].classList.add(designSystemClass);
|
||||
|
||||
// We can't include Bootstrap CSS along with Bulma since some of the class names conflict.
|
||||
// TODO(echelon): Once ported, statically move CSS to "index.html".
|
||||
const bootstrapCss = document.createElement("link");
|
||||
bootstrapCss.setAttribute("rel", "stylesheet");
|
||||
bootstrapCss.setAttribute("crossorigin", "anonymous");
|
||||
bootstrapCss.setAttribute("integrity", "sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3");
|
||||
bootstrapCss.setAttribute("href", "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css");
|
||||
document.getElementsByTagName("head")[0].appendChild(bootstrapCss);
|
||||
// const bootstrapCss = document.createElement("link");
|
||||
// bootstrapCss.setAttribute("rel", "stylesheet");
|
||||
// bootstrapCss.setAttribute("crossorigin", "anonymous");
|
||||
// bootstrapCss.setAttribute("integrity", "sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3");
|
||||
// bootstrapCss.setAttribute("href", "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css");
|
||||
// document.getElementsByTagName("head")[0].appendChild(bootstrapCss);
|
||||
|
||||
const bootstrapJs = document.createElement("script");
|
||||
bootstrapJs.setAttribute("rel", "stylesheet");
|
||||
bootstrapJs.setAttribute("crossorigin", "anonymous");
|
||||
bootstrapJs.setAttribute("integrity", "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p");
|
||||
bootstrapJs.setAttribute("src", "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js");
|
||||
bootstrapJs.setAttribute(
|
||||
"integrity",
|
||||
"sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
);
|
||||
bootstrapJs.setAttribute(
|
||||
"src",
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
);
|
||||
document.getElementsByTagName("body")[0].appendChild(bootstrapJs);
|
||||
|
||||
// NB: Posthog turned off for most users (anonymous users) due to cost.
|
||||
@@ -37,11 +45,11 @@ document.getElementsByTagName("body")[0].appendChild(bootstrapJs);
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<AppTranslated
|
||||
enableSpectrograms={enableSpectrograms}
|
||||
flashVocodesNotice={flashVocodesNotice}
|
||||
/>
|
||||
enableSpectrograms={enableSpectrograms}
|
||||
flashVocodesNotice={flashVocodesNotice}
|
||||
/>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Override the $spacers variable
|
||||
$spacers: (
|
||||
0: 0,
|
||||
1: 0.25rem,
|
||||
2: 0.5rem,
|
||||
3: 1rem,
|
||||
4: 1.75rem,
|
||||
5: 2rem,
|
||||
);
|
||||
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@@ -148,7 +148,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1080px !important;
|
||||
max-width: 1200px !important;
|
||||
}
|
||||
|
||||
.container,
|
||||
@@ -168,11 +168,6 @@ styles with important tags:
|
||||
padding-bottom: 0.4rem !important;
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 2rem !important;
|
||||
padding-bottom: 2rem !important;
|
||||
}
|
||||
|
||||
.lead {
|
||||
color: rgba($white, 0.8);
|
||||
font-size: 17px;
|
||||
@@ -288,18 +283,33 @@ styles with important tags:
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.bg-panel {
|
||||
background-color: lighten($panel, 1%);
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: $panel;
|
||||
border-color: #ffffff12 !important;
|
||||
border: 1px solid;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 0 0 1px rgba(0 0 0 / 16%), 0 70px 65px rgb(0 0 0 / 8%),
|
||||
0 30px 30px rgb(0 0 0 / 7%), 0 15px 15px rgb(0 0 0 / 5%),
|
||||
0 10px 8px rgb(0 0 0 / 3%), 0 4px 4px rgb(0 0 0 / 2%),
|
||||
0 2px 2px rgb(0 0 0 / 1%) !important;
|
||||
|
||||
&.panel-clear {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.panel-inner {
|
||||
background-color: lighten($panel, 4%);
|
||||
}
|
||||
@@ -926,10 +936,6 @@ styles with important tags:
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="fakeyou"] > .tippy-svg-arrow {
|
||||
fill: lighten($secondary, 5%);
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="range-slider"] {
|
||||
margin-top: -5px;
|
||||
background-color: lighten($panel, 10%);
|
||||
@@ -937,6 +943,23 @@ styles with important tags:
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="fakeyou"][data-placement^="top"]
|
||||
> .tippy-arrow::before {
|
||||
border-top-color: lighten($secondary, 5%);
|
||||
}
|
||||
.tippy-box[data-theme~="fakeyou"][data-placement^="bottom"]
|
||||
> .tippy-arrow::before {
|
||||
border-bottom-color: lighten($secondary, 5%);
|
||||
}
|
||||
.tippy-box[data-theme~="fakeyou"][data-placement^="left"]
|
||||
> .tippy-arrow::before {
|
||||
border-left-color: lighten($secondary, 5%);
|
||||
}
|
||||
.tippy-box[data-theme~="fakeyou"][data-placement^="right"]
|
||||
> .tippy-arrow::before {
|
||||
border-right-color: lighten($secondary, 5%);
|
||||
}
|
||||
|
||||
.sliding-content {
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
@@ -1468,10 +1491,6 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.p-lg-4 {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -1517,12 +1536,18 @@ textarea {
|
||||
padding-left: var(--bs-gutter-x, 1rem);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
max-width: 1080px;
|
||||
max-width: 1200px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid;
|
||||
|
||||
&.panel-clear {
|
||||
padding-left: 0rem;
|
||||
padding-right: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul.patrons-list {
|
||||
@@ -1538,9 +1563,5 @@ textarea {
|
||||
height: 100%;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.p-md-4 {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ 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 { Media } from "./pages/media";
|
||||
import { LipsyncEditor } from "./pages/lipsync";
|
||||
import { ProfileEditFc } from "./pages/profile/profile_edit/ProfileEditFc";
|
||||
import { ProfilePage } from "./pages/profile/profile_view/ProfilePage";
|
||||
@@ -90,6 +89,7 @@ import MobileMenu from "components/layout/MobileMenu/MobileMenu";
|
||||
import { TopNav } from "./nav/TopNav";
|
||||
import { TestingPage } from "./pages/testing/TestingPage";
|
||||
import TtsModelSearchPage from "./pages/tts/tts_model_search/TtsModelSearchPage";
|
||||
import MediaPage from "./pages/media/MediaPage";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -318,6 +318,10 @@ class PageContainer extends React.Component<
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/media/:token">
|
||||
<MediaPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
|
||||
<Route path="/dev-tts">
|
||||
<TtsModelSearchPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
@@ -463,10 +467,6 @@ class PageContainer extends React.Component<
|
||||
<CreateCategoryPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
|
||||
<Route path="/media/:token">
|
||||
<Media />
|
||||
</Route>
|
||||
|
||||
<Route path="/moderation/user/list">
|
||||
<ModerationUserListFc
|
||||
sessionWrapper={this.props.sessionWrapper}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import MediaAudioPlayer from "./MediaAudioPlayer";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
|
||||
interface MediaAudioComponentProps {
|
||||
mediaFile: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaAudioComponent({
|
||||
mediaFile,
|
||||
}: MediaAudioComponentProps) {
|
||||
return (
|
||||
<div className="w-100">
|
||||
<MediaAudioPlayer mediaFile={mediaFile} />
|
||||
</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 { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
|
||||
enum PlaybackSpeed {
|
||||
HALF,
|
||||
NORMAL,
|
||||
DOUBLE,
|
||||
}
|
||||
|
||||
interface MediaAudioPlayerProps {
|
||||
mediaFile: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaAudioPlayer({ mediaFile }: 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(
|
||||
mediaFile.public_bucket_path
|
||||
);
|
||||
if (waveSurfer) {
|
||||
waveSurfer.load(audioLink);
|
||||
}
|
||||
}, [waveSurfer, mediaFile]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
import React from "react";
|
||||
|
||||
interface MediaImageComponentProps {
|
||||
mediaFile: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaImageComponent({
|
||||
mediaFile,
|
||||
}: MediaImageComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={mediaFile.public_bucket_path}
|
||||
alt="test"
|
||||
width="400"
|
||||
height="300"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import MediaAudioComponent from "./MediaAudioComponent";
|
||||
import MediaVideoComponent from "./MediaVideoComponent";
|
||||
import MediaImageComponent from "./MediaImageComponent";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { GetMediaFile, MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
import Container from "components/common/Container";
|
||||
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCircleExclamation,
|
||||
faArrowDownToLine,
|
||||
faCirclePlay,
|
||||
faShare,
|
||||
faSquareQuote,
|
||||
} 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 { CommentComponent } from "v2/view/_common/comments/CommentComponent";
|
||||
import { MediaFileType } from "@storyteller/components/src/api/_common/enums/MediaFileType";
|
||||
|
||||
interface MediaPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
export default function MediaPage({ sessionWrapper }: MediaPageProps) {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const [mediaFile, setMediaFile] = useState<MediaFile | undefined | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const timeCreated = useTimeAgo(mediaFile?.created_at.toISOString() || "");
|
||||
|
||||
const getMediaFile = useCallback(async (mediaFileToken: string) => {
|
||||
let result = await GetMediaFile(mediaFileToken);
|
||||
if (result.media_file) {
|
||||
setMediaFile(result.media_file);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setError(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getMediaFile(token);
|
||||
}, [token, getMediaFile]);
|
||||
|
||||
function renderMediaComponent(mediaFile: MediaFile) {
|
||||
switch (mediaFile.media_type) {
|
||||
case MediaFileType.Audio:
|
||||
return (
|
||||
<div className="panel p-3 p-lg-4 d-flex flex-column gap-4 rounded">
|
||||
<MediaAudioComponent mediaFile={mediaFile} />
|
||||
<div>
|
||||
<h5 className="fw-semibold">
|
||||
<FontAwesomeIcon icon={faSquareQuote} className="me-2" />
|
||||
Audio Text
|
||||
</h5>
|
||||
{/*<p>{mediaFile?.audio_text && mediaFile.audio_text}</p>*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case MediaFileType.Video:
|
||||
return (
|
||||
<div className="panel panel-clear">
|
||||
<div className="ratio ratio-16x9">
|
||||
<MediaVideoComponent mediaFile={mediaFile} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MediaFileType.Image:
|
||||
return <MediaImageComponent mediaFile={mediaFile} />;
|
||||
default:
|
||||
return <div>Unsupported media type</div>;
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<>
|
||||
<Container type="padded">
|
||||
<div className="pt-4 pb-4">
|
||||
<h2 className="fw-bold mb-1 pt-0 pt-lg-1">
|
||||
<Skeleton type="short" />
|
||||
</h2>
|
||||
<p className="mb-0">
|
||||
<Skeleton type="medium" />
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<Container type="padded">
|
||||
<div className="row g-4">
|
||||
<div className="col-12 col-xl-8">
|
||||
<div className="panel p-3 py-4 p-md-4">
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="panel p-3 py-4 p-md-4 mt-4 d-none d-xl-block">
|
||||
<h4 className="fw-semibold mb-3">
|
||||
<Skeleton type="short" />
|
||||
</h4>
|
||||
<h1>
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-xl-4 d-flex flex-column gap-2">
|
||||
<h1 className="mb-0">
|
||||
<Skeleton type="medium" />
|
||||
</h1>
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<div className="d-xl-none mt-4">
|
||||
<Panel padding>
|
||||
<h4 className="fw-semibold mb-3">
|
||||
<Skeleton type="short" />
|
||||
</h4>
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (error || !mediaFile)
|
||||
return (
|
||||
<Container type="panel">
|
||||
<PageHeader
|
||||
titleIcon={faCircleExclamation}
|
||||
title="Media not found"
|
||||
subText="This media does not exist or is private."
|
||||
extension={
|
||||
<div className="d-flex">
|
||||
<Button label="Back to homepage" to="/" className="d-flex" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const audioDetails = [
|
||||
{ property: "Type", value: mediaFile.media_type },
|
||||
{ property: "Created at", value: mediaFile.created_at.toString() },
|
||||
{
|
||||
property: "Visibility",
|
||||
value: mediaFile.creator_set_visibility.toString(),
|
||||
},
|
||||
/*{
|
||||
property: "Model",
|
||||
value: mediaFile.model_used,
|
||||
link: mediaFile.model_link,
|
||||
},
|
||||
{
|
||||
property: "Vocoder",
|
||||
value: "Test",
|
||||
},
|
||||
{
|
||||
property: "Language",
|
||||
value: "English",
|
||||
},
|
||||
{
|
||||
property: "Model",
|
||||
value: mediaFile.model_used,
|
||||
},*/
|
||||
];
|
||||
|
||||
const videoDetails = [
|
||||
{ property: "Type", value: mediaFile.media_type },
|
||||
{ property: "Created at", value: mediaFile.created_at.toString() },
|
||||
{
|
||||
property: "Visibility",
|
||||
value: mediaFile.creator_set_visibility.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
let pageTitle = "Result";
|
||||
let pageSubText = "This is the result of your media.";
|
||||
let mediaDetails = undefined;
|
||||
|
||||
switch (mediaFile.media_type) {
|
||||
case MediaFileType.Audio:
|
||||
pageTitle = "Audio Result";
|
||||
pageSubText = mediaFile.token;
|
||||
mediaDetails = (
|
||||
<Accordion.Item title="Audio Details" defaultOpen={true}>
|
||||
<DataTable data={audioDetails} />
|
||||
</Accordion.Item>
|
||||
);
|
||||
break;
|
||||
case MediaFileType.Video:
|
||||
pageTitle = "Video Result";
|
||||
pageSubText = mediaFile.token;
|
||||
mediaDetails = (
|
||||
<Accordion.Item title="Video Details" defaultOpen={true}>
|
||||
<DataTable data={videoDetails} />
|
||||
</Accordion.Item>
|
||||
);
|
||||
break;
|
||||
case MediaFileType.Image:
|
||||
pageTitle = "Image Model";
|
||||
pageSubText = "Image Model SubText";
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
let modMediaDetails = undefined;
|
||||
|
||||
const modDetails = [
|
||||
{ property: "Model creator is banned", value: "good standing" },
|
||||
{
|
||||
property: "Result creator is banned (if user)",
|
||||
value: "good standing",
|
||||
},
|
||||
{
|
||||
property: "Result creator IP address",
|
||||
value: "0.0.0.0",
|
||||
},
|
||||
{
|
||||
property: "Mod deleted at (UTC)",
|
||||
value: "not deleted",
|
||||
},
|
||||
{
|
||||
property: "Result creator deleted at (UTC)",
|
||||
value: "not deleted",
|
||||
},
|
||||
];
|
||||
|
||||
if (sessionWrapper.canBanUsers()) {
|
||||
modMediaDetails = (
|
||||
<Accordion.Item title="Moderator Details" defaultOpen={false}>
|
||||
<DataTable data={modDetails} />
|
||||
</Accordion.Item>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container type="padded" className="py-5">
|
||||
<h2 className="fw-bold mb-1">{pageTitle}</h2>
|
||||
<p className="mb-0">{pageSubText}</p>
|
||||
</Container>
|
||||
|
||||
<Container type="panel">
|
||||
<div className="row g-4 mb-4">
|
||||
<div className="col-12 col-xl-8">
|
||||
<div className="media-wrapper">
|
||||
{renderMediaComponent(mediaFile)}
|
||||
</div>
|
||||
|
||||
<div className="panel p-3 py-4 p-md-4 mt-4 d-none d-xl-block">
|
||||
<h4 className="fw-semibold mb-3">Comments</h4>
|
||||
<CommentComponent
|
||||
entityType="user"
|
||||
entityToken={mediaFile.token}
|
||||
sessionWrapper={sessionWrapper}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-xl-4">
|
||||
<div className="panel panel-clear d-flex flex-column gap-4">
|
||||
<div className="d-flex gap-2">
|
||||
<Gravatar
|
||||
size={48}
|
||||
username={mediaFile.maybe_creator_user?.display_name}
|
||||
avatarIndex={
|
||||
mediaFile.maybe_creator_user?.default_avatar.image_index
|
||||
}
|
||||
backgroundIndex={
|
||||
mediaFile.maybe_creator_user?.default_avatar.color_index
|
||||
}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
<Link
|
||||
className="fw-medium"
|
||||
to={`/profile/${mediaFile.maybe_creator_user?.display_name}`}
|
||||
>
|
||||
{mediaFile.maybe_creator_user?.display_name}
|
||||
</Link>
|
||||
{timeCreated}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2 flex-wrap">
|
||||
<Button
|
||||
icon={faArrowDownToLine}
|
||||
label="Download"
|
||||
onClick={() => {}}
|
||||
className="flex-grow-1"
|
||||
/>
|
||||
<div className="d-flex gap-2">
|
||||
<Button
|
||||
square={true}
|
||||
variant="secondary"
|
||||
icon={faCirclePlay}
|
||||
onClick={() => {}}
|
||||
tooltip="Create"
|
||||
/>
|
||||
<Button
|
||||
square={true}
|
||||
variant="secondary"
|
||||
icon={faShare}
|
||||
onClick={() => {}}
|
||||
tooltip="Share"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Accordion>
|
||||
{mediaDetails}
|
||||
{modMediaDetails}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<div className="d-xl-none my-4">
|
||||
<Container type="panel">
|
||||
<Panel padding={true}>
|
||||
<h4 className="fw-semibold mb-3">Comments</h4>
|
||||
<CommentComponent
|
||||
entityType="user"
|
||||
entityToken={mediaFile.token}
|
||||
sessionWrapper={sessionWrapper}
|
||||
/>
|
||||
</Panel>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
import React from "react";
|
||||
|
||||
interface MediaVideoComponentProps {
|
||||
mediaFile: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaVideoComponent({
|
||||
mediaFile,
|
||||
}: MediaVideoComponentProps) {
|
||||
let mediaLink = new BucketConfig().getGcsUrl(
|
||||
mediaFile.public_bucket_path
|
||||
);
|
||||
|
||||
return (
|
||||
<video className="rounded" controls width="100%" height="auto">
|
||||
<source src={mediaLink} />
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import media from './media';
|
||||
|
||||
export { media as Media };
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export default function media({ value }: Props) {
|
||||
return <div>
|
||||
<h1>Media</h1>
|
||||
</div>;
|
||||
};
|
||||
@@ -434,46 +434,6 @@ function TtsResultViewPage(props: Props) {
|
||||
<div className="container py-5">
|
||||
<div className="d-flex flex-column">
|
||||
<h1 className=" fw-bold mb-2">{headingTitle}</h1>
|
||||
{/* Rate Result Buttons */}
|
||||
{/* Commented out because not ready yet. */}
|
||||
{/*<div className="d-flex gap-3">
|
||||
<div className="d-flex">
|
||||
<Tippy
|
||||
content="I like this audio"
|
||||
hideOnClick
|
||||
placement="bottom"
|
||||
theme="fakeyou"
|
||||
arrow={false}
|
||||
>
|
||||
<button className="btn-rate left rated">
|
||||
<FontAwesomeIcon icon={faThumbsUp} />
|
||||
</button>
|
||||
</Tippy>
|
||||
|
||||
<div className="vr"></div>
|
||||
|
||||
<Tippy
|
||||
content="I dislike this audio"
|
||||
hideOnClick
|
||||
placement="bottom"
|
||||
theme="fakeyou"
|
||||
arrow={false}
|
||||
>
|
||||
<button className="btn-rate right">
|
||||
<FontAwesomeIcon icon={faThumbsDown} />
|
||||
</button>
|
||||
</Tippy>
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faHeart}
|
||||
className="me-2 rating-icon heart"
|
||||
/>
|
||||
<p>
|
||||
<span className="fw-medium">1,518 likes</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>*/}
|
||||
|
||||
<p className="mb-3 result-text pt-2">
|
||||
<TextExpander
|
||||
@@ -494,14 +454,6 @@ function TtsResultViewPage(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Without wavesurfer,
|
||||
<audio
|
||||
controls
|
||||
src={audioLink}>
|
||||
Your browser does not support the
|
||||
<code>audio</code> element.
|
||||
</audio>*/}
|
||||
|
||||
<div className="container-panel pt-3 pb-5">{socialSharing}</div>
|
||||
|
||||
<div className="container-panel pt-3 pb-5">
|
||||
@@ -517,12 +469,6 @@ function TtsResultViewPage(props: Props) {
|
||||
<div className="py-6">
|
||||
<table className="table tts-result-table">
|
||||
<tbody>
|
||||
{/* <tr>
|
||||
<th scope="row">Original text</th>
|
||||
<td className="overflow-fix">
|
||||
{ttsInferenceResult.raw_inference_text}
|
||||
</td>
|
||||
</tr> */}
|
||||
<tr>
|
||||
<th scope="row">Audio creator</th>
|
||||
<td>{creatorDetails}</td>
|
||||
|
||||
@@ -4440,9 +4440,10 @@ __metadata:
|
||||
"@types/uuid": ^8.3.0
|
||||
"@types/wavesurfer.js": ^5.2.0
|
||||
axios: ^0.21.0
|
||||
bootstrap: 5.1.3
|
||||
bulma: ^0.9.0
|
||||
craco: ^0.0.3
|
||||
date-fns: ^2.22.1
|
||||
date-fns: 2.22.1
|
||||
framer-motion: ^6.5.1
|
||||
howler: ^2.2.0
|
||||
i18next: ^21.6.11
|
||||
@@ -7280,6 +7281,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bootstrap@npm:5.1.3":
|
||||
version: 5.1.3
|
||||
resolution: "bootstrap@npm:5.1.3"
|
||||
peerDependencies:
|
||||
"@popperjs/core": ^2.10.2
|
||||
checksum: 301b5ed872efba061104cf22ac93568e3837867fb5527ab9326a51510fb752bd4883e1d488225c8be72f86d9d3a55ef5b166aa7fa62c2fdd077c3f05b65752f8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.11
|
||||
resolution: "brace-expansion@npm:1.1.11"
|
||||
@@ -8966,6 +8976,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:2.22.1":
|
||||
version: 2.22.1
|
||||
resolution: "date-fns@npm:2.22.1"
|
||||
checksum: 7ff97cd605af50c02f341687c2cafd218839a1aace67965374989855a13f76dc4fe52e0e38c343c1ad1f8399787cb6839a0b14a669c44b30550c287300b1bb50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:^2.22.1":
|
||||
version: 2.28.0
|
||||
resolution: "date-fns@npm:2.28.0"
|
||||
|
||||
Reference in new issue
Block a user