diff --git a/packages/components/src/api/ApiConfig.ts b/packages/components/src/api/ApiConfig.ts index 459efd25f2..6c55b60b55 100644 --- a/packages/components/src/api/ApiConfig.ts +++ b/packages/components/src/api/ApiConfig.ts @@ -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 { diff --git a/packages/components/src/api/_common/UserDetailsLight.ts b/packages/components/src/api/_common/UserDetailsLight.ts new file mode 100644 index 0000000000..a913fe44df --- /dev/null +++ b/packages/components/src/api/_common/UserDetailsLight.ts @@ -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, +} diff --git a/packages/components/src/api/_common/enums/MediaFileType.ts b/packages/components/src/api/_common/enums/MediaFileType.ts new file mode 100644 index 0000000000..b104aeaa7b --- /dev/null +++ b/packages/components/src/api/_common/enums/MediaFileType.ts @@ -0,0 +1,6 @@ + +export enum MediaFileType { + Audio = "audio", + Video = "video", + Image = "image", +} diff --git a/packages/components/src/api/media_files/GetMediaFile.ts b/packages/components/src/api/media_files/GetMediaFile.ts new file mode 100644 index 0000000000..29bdc1a7fc --- /dev/null +++ b/packages/components/src/api/media_files/GetMediaFile.ts @@ -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 { + 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 }; + }); +} diff --git a/packages/fakeyou.com/package.json b/packages/fakeyou.com/package.json index f89e903dca..3bd36b3497 100644 --- a/packages/fakeyou.com/package.json +++ b/packages/fakeyou.com/package.json @@ -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", diff --git a/packages/fakeyou.com/src/App.tsx b/packages/fakeyou.com/src/App.tsx index 4380c46c10..e9216c3325 100644 --- a/packages/fakeyou.com/src/App.tsx +++ b/packages/fakeyou.com/src/App.tsx @@ -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 { - 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() { diff --git a/packages/fakeyou.com/src/components/common/Accordion/Accordion.scss b/packages/fakeyou.com/src/components/common/Accordion/Accordion.scss new file mode 100644 index 0000000000..803aeb60d5 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Accordion/Accordion.scss @@ -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); +} diff --git a/packages/fakeyou.com/src/components/common/Accordion/Accordion.tsx b/packages/fakeyou.com/src/components/common/Accordion/Accordion.tsx new file mode 100644 index 0000000000..4eceaeee37 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Accordion/Accordion.tsx @@ -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( + undefined +); + +function AccordionItem({ + title, + defaultOpen = false, + children, +}: AccordionItemProps) { + const { openItems, toggleItem } = useContext( + AccordionContext + ) as AccordionContextType; + const isOpen = openItems.includes(title); + + const [contentHeight, setContentHeight] = useState( + isOpen ? undefined : 0 + ); + const contentRef = useRef(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 ( +
+
+ {title} + + + +
+ + + {children} + + +
+ ); +} + +function Accordion({ children }: AccordionProps) { + const [openItems, setOpenItems] = useState([]); + + 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 ( + +
{children}
+
+ ); +} + +Accordion.Item = AccordionItem; + +export default Accordion; diff --git a/packages/fakeyou.com/src/components/common/Accordion/index.ts b/packages/fakeyou.com/src/components/common/Accordion/index.ts new file mode 100644 index 0000000000..366853b1f2 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Accordion/index.ts @@ -0,0 +1 @@ +export { default } from "./Accordion"; diff --git a/packages/fakeyou.com/src/components/common/Button/Button.scss b/packages/fakeyou.com/src/components/common/Button/Button.scss index 2b77945926..17ba047172 100644 --- a/packages/fakeyou.com/src/components/common/Button/Button.scss +++ b/packages/fakeyou.com/src/components/common/Button/Button.scss @@ -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; diff --git a/packages/fakeyou.com/src/components/common/Button/Button.tsx b/packages/fakeyou.com/src/components/common/Button/Button.tsx index b67196aa13..cc68fe70a7 100644 --- a/packages/fakeyou.com/src/components/common/Button/Button.tsx +++ b/packages/fakeyou.com/src/components/common/Button/Button.tsx @@ -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 { - 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 ? ( + + ) : 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 ( - - {icon && } - {label} - - ); - } - if (href) { - return ( - - {icon && } - {label} - - ); - } + delete rest.className; - // If neither 'to' nor 'href' are provided (regular button) - return ( - ); + + return tooltip ? ( + + {WrappedButton} + + ) : ( + WrappedButton + ); } diff --git a/packages/fakeyou.com/src/components/common/Container/Container.tsx b/packages/fakeyou.com/src/components/common/Container/Container.tsx index a8ae3ca28e..965c52dff4 100644 --- a/packages/fakeyou.com/src/components/common/Container/Container.tsx +++ b/packages/fakeyou.com/src/components/common/Container/Container.tsx @@ -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
{children}
; + // Merge the classNames + const mergedClassNames = `${containerClass} ${className}`; + + return
{children}
; } diff --git a/packages/fakeyou.com/src/components/common/DataTable/DataTable.scss b/packages/fakeyou.com/src/components/common/DataTable/DataTable.scss new file mode 100644 index 0000000000..e5299b7c8e --- /dev/null +++ b/packages/fakeyou.com/src/components/common/DataTable/DataTable.scss @@ -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; +} diff --git a/packages/fakeyou.com/src/components/common/DataTable/DataTable.tsx b/packages/fakeyou.com/src/components/common/DataTable/DataTable.tsx new file mode 100644 index 0000000000..539e82317e --- /dev/null +++ b/packages/fakeyou.com/src/components/common/DataTable/DataTable.tsx @@ -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 ( + + + {data.map((row, index) => ( + + + {row.link ? ( + + ) : ( + + )} + + ))} + +
{row.property} + {row.value} + {row.value}
+ ); +} diff --git a/packages/fakeyou.com/src/components/common/DataTable/index.tsx b/packages/fakeyou.com/src/components/common/DataTable/index.tsx new file mode 100644 index 0000000000..e8a1b251a1 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/DataTable/index.tsx @@ -0,0 +1 @@ +export { default } from "./DataTable"; diff --git a/packages/fakeyou.com/src/components/common/Panel/Panel.tsx b/packages/fakeyou.com/src/components/common/Panel/Panel.tsx index 25e1d4281e..f86e65d2f9 100644 --- a/packages/fakeyou.com/src/components/common/Panel/Panel.tsx +++ b/packages/fakeyou.com/src/components/common/Panel/Panel.tsx @@ -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 ( -
-
- {children} -
+ > + {children}
); } diff --git a/packages/fakeyou.com/src/components/common/Skeleton/Skeleton.scss b/packages/fakeyou.com/src/components/common/Skeleton/Skeleton.scss new file mode 100644 index 0000000000..a2f59e40b5 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Skeleton/Skeleton.scss @@ -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%; +} diff --git a/packages/fakeyou.com/src/components/common/Skeleton/Skeleton.tsx b/packages/fakeyou.com/src/components/common/Skeleton/Skeleton.tsx new file mode 100644 index 0000000000..1ca5a22671 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Skeleton/Skeleton.tsx @@ -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
; +} diff --git a/packages/fakeyou.com/src/components/common/Skeleton/index.ts b/packages/fakeyou.com/src/components/common/Skeleton/index.ts new file mode 100644 index 0000000000..2508b14587 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Skeleton/index.ts @@ -0,0 +1 @@ +export { default } from "./Skeleton"; diff --git a/packages/fakeyou.com/src/components/layout/PageHeader/PageHeader.tsx b/packages/fakeyou.com/src/components/layout/PageHeader/PageHeader.tsx index 62682ba82d..2dfedb1d2c 100644 --- a/packages/fakeyou.com/src/components/layout/PageHeader/PageHeader.tsx +++ b/packages/fakeyou.com/src/components/layout/PageHeader/PageHeader.tsx @@ -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 && } @@ -33,7 +37,7 @@ export default function PageHeader({ return (
- +
@@ -44,9 +48,11 @@ export default function PageHeader({
{showButton && (
diff --git a/packages/fakeyou.com/src/components/layout/PageHeaderModelView/PageHeaderModelView.tsx b/packages/fakeyou.com/src/components/layout/PageHeaderModelView/PageHeaderModelView.tsx index 331e477565..871b610374 100644 --- a/packages/fakeyou.com/src/components/layout/PageHeaderModelView/PageHeaderModelView.tsx +++ b/packages/fakeyou.com/src/components/layout/PageHeaderModelView/PageHeaderModelView.tsx @@ -35,7 +35,7 @@ export default function PageHeaderModelView({ return (
{view === "regular" && ( - +

diff --git a/packages/fakeyou.com/src/hooks/useTimeAgo/index.ts b/packages/fakeyou.com/src/hooks/useTimeAgo/index.ts new file mode 100644 index 0000000000..039f2a67f4 --- /dev/null +++ b/packages/fakeyou.com/src/hooks/useTimeAgo/index.ts @@ -0,0 +1 @@ +export { default } from "./useTimeAgo"; diff --git a/packages/fakeyou.com/src/hooks/useTimeAgo/useTimeAgo.tsx b/packages/fakeyou.com/src/hooks/useTimeAgo/useTimeAgo.tsx new file mode 100644 index 0000000000..4137c46b27 --- /dev/null +++ b/packages/fakeyou.com/src/hooks/useTimeAgo/useTimeAgo.tsx @@ -0,0 +1,44 @@ +import { useEffect, useState } from "react"; + +function useTimeAgo(timestamp: string) { + const [timeAgo, setTimeAgo] = useState(""); + + 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; diff --git a/packages/fakeyou.com/src/index.tsx b/packages/fakeyou.com/src/index.tsx index 8741671386..94946a02b5 100644 --- a/packages/fakeyou.com/src/index.tsx +++ b/packages/fakeyou.com/src/index.tsx @@ -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( + enableSpectrograms={enableSpectrograms} + flashVocodesNotice={flashVocodesNotice} + /> , - document.getElementById('root') + document.getElementById("root") ); // If you want your app to work offline and load faster, you can change diff --git a/packages/fakeyou.com/src/scss/custom-bootstrap.scss b/packages/fakeyou.com/src/scss/custom-bootstrap.scss new file mode 100644 index 0000000000..af52e438e1 --- /dev/null +++ b/packages/fakeyou.com/src/scss/custom-bootstrap.scss @@ -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"; diff --git a/packages/fakeyou.com/src/scss/site-refresh.scss b/packages/fakeyou.com/src/scss/site-refresh.scss index f72841e452..da4780ee4c 100644 --- a/packages/fakeyou.com/src/scss/site-refresh.scss +++ b/packages/fakeyou.com/src/scss/site-refresh.scss @@ -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; - } } } diff --git a/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/packages/fakeyou.com/src/v2/view/PageContainer.tsx index 6c1298ee8e..54d82b97d6 100644 --- a/packages/fakeyou.com/src/v2/view/PageContainer.tsx +++ b/packages/fakeyou.com/src/v2/view/PageContainer.tsx @@ -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< /> + + + + @@ -463,10 +467,6 @@ class PageContainer extends React.Component< - - - - + +

+ ); +} diff --git a/packages/fakeyou.com/src/v2/view/pages/media/MediaAudioPlayer.tsx b/packages/fakeyou.com/src/v2/view/pages/media/MediaAudioPlayer.tsx new file mode 100644 index 0000000000..4b5cd6481b --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/media/MediaAudioPlayer.tsx @@ -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(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 = ( + <> + + + ); + if (isPlaying) { + playButtonText = ( + <> + + + ); + } + + 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 ( +
+
+
+
+ + + + + +
+
+
+ ); +} diff --git a/packages/fakeyou.com/src/v2/view/pages/media/MediaImageComponent.tsx b/packages/fakeyou.com/src/v2/view/pages/media/MediaImageComponent.tsx new file mode 100644 index 0000000000..b09ee77ac3 --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/media/MediaImageComponent.tsx @@ -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 ( +
+ test +
+ ); +} diff --git a/packages/fakeyou.com/src/v2/view/pages/media/MediaPage.tsx b/packages/fakeyou.com/src/v2/view/pages/media/MediaPage.tsx new file mode 100644 index 0000000000..6cd7a1b11c --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/media/MediaPage.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+ +
+
+ + Audio Text +
+ {/*

{mediaFile?.audio_text && mediaFile.audio_text}

*/} +
+
+ ); + case MediaFileType.Video: + return ( +
+
+ +
+
+ ); + + case MediaFileType.Image: + return ; + default: + return
Unsupported media type
; + } + } + + if (isLoading) + return ( + <> + +
+

+ +

+

+ +

+
+
+ + +
+
+
+

+ +

+
+ +
+

+ +

+

+ +

+
+
+
+

+ +

+

+ +

+

+ +

+
+
+
+ +
+ +

+ +

+

+ +

+
+
+ + ); + + if (error || !mediaFile) + return ( + + +
+ } + /> + + ); + + 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 = ( + + + + ); + break; + case MediaFileType.Video: + pageTitle = "Video Result"; + pageSubText = mediaFile.token; + mediaDetails = ( + + + + ); + 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 = ( + + + + ); + } + + return ( + <> + +

{pageTitle}

+

{pageSubText}

+
+ + +
+
+
+ {renderMediaComponent(mediaFile)} +
+ +
+

Comments

+ +
+
+
+
+
+ +
+ + {mediaFile.maybe_creator_user?.display_name} + + {timeCreated} +
+
+ +
+
+
+ + + {mediaDetails} + {modMediaDetails} + +
+
+
+ + +
+ + +

Comments

+ +
+
+
+ + ); +} diff --git a/packages/fakeyou.com/src/v2/view/pages/media/MediaVideoComponent.tsx b/packages/fakeyou.com/src/v2/view/pages/media/MediaVideoComponent.tsx new file mode 100644 index 0000000000..786de72de2 --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/media/MediaVideoComponent.tsx @@ -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 ( + + ); +} diff --git a/packages/fakeyou.com/src/v2/view/pages/media/index.tsx b/packages/fakeyou.com/src/v2/view/pages/media/index.tsx deleted file mode 100644 index f7c67981af..0000000000 --- a/packages/fakeyou.com/src/v2/view/pages/media/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import media from './media'; - -export { media as Media }; diff --git a/packages/fakeyou.com/src/v2/view/pages/media/media.tsx b/packages/fakeyou.com/src/v2/view/pages/media/media.tsx deleted file mode 100644 index ddc4db1725..0000000000 --- a/packages/fakeyou.com/src/v2/view/pages/media/media.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -interface Props { - value?: any; -} - -export default function media({ value }: Props) { - return
-

Media

-
; -}; \ No newline at end of file diff --git a/packages/fakeyou.com/src/v2/view/pages/tts/tts_result_view/TtsResultViewPage.tsx b/packages/fakeyou.com/src/v2/view/pages/tts/tts_result_view/TtsResultViewPage.tsx index 8acbb7384a..39e36ceaba 100644 --- a/packages/fakeyou.com/src/v2/view/pages/tts/tts_result_view/TtsResultViewPage.tsx +++ b/packages/fakeyou.com/src/v2/view/pages/tts/tts_result_view/TtsResultViewPage.tsx @@ -434,46 +434,6 @@ function TtsResultViewPage(props: Props) {

{headingTitle}

- {/* Rate Result Buttons */} - {/* Commented out because not ready yet. */} - {/*
-
- - - - -
- - - - -
-
- -

- 1,518 likes -

-
-
*/}

- {/* Without wavesurfer, - */} -
{socialSharing}
@@ -517,12 +469,6 @@ function TtsResultViewPage(props: Props) {
- {/* - - - */} diff --git a/yarn.lock b/yarn.lock index b6f99c3d43..38cea68bf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"
Original text - {ttsInferenceResult.raw_inference_text} -
Audio creator {creatorDetails}