Merge branch 'test' into feature/style-tab
No files matched your search
@@ -28,66 +28,8 @@ Fast Prototyping with Better Netlify Pipeline:
|
||||
- [Netlify Functions Overview](https://docs.netlify.com/functions/overview)
|
||||
- [Tailwind Docs](https://tailwindcss.com/docs)
|
||||
|
||||
## Netlify Setup
|
||||
|
||||
1. Install the [Netlify CLI](https://docs.netlify.com/cli/get-started/):
|
||||
|
||||
```sh
|
||||
npm i -g netlify-cli
|
||||
Setup
|
||||
```
|
||||
|
||||
If you have previously installed the Netlify CLI, you should update it to the latest version:
|
||||
|
||||
```sh
|
||||
npm i -g netlify-cli@latest
|
||||
```
|
||||
|
||||
2. Sign up and log in to Netlify:
|
||||
|
||||
```sh
|
||||
netlify login
|
||||
```
|
||||
|
||||
3. Create a new site:
|
||||
|
||||
```sh
|
||||
netlify init
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Ensure all packages are installed by running:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
Run
|
||||
|
||||
```sh
|
||||
netlify dev
|
||||
```
|
||||
|
||||
Open up [http://localhost:8888](http://localhost:8888), and you're ready to go!
|
||||
|
||||
### Serve your site locally
|
||||
|
||||
To serve your site locally in a production-like environment, run
|
||||
|
||||
```sh
|
||||
netlify serve
|
||||
```
|
||||
|
||||
Your site will be available at [http://localhost:8888](http://localhost:8888). Note that it will not auto-reload when you make changes.
|
||||
|
||||
## Deployment
|
||||
|
||||
There are two ways to deploy your app to Netlify, you can either link your app to your git repo and have it auto deploy changes to Netlify, or you can deploy your app manually. If you've followed the setup instructions already, all you need to do is run this:
|
||||
|
||||
```sh
|
||||
# preview deployment
|
||||
netlify deploy --build
|
||||
|
||||
# production deployment
|
||||
netlify deploy --build --prod
|
||||
npm run dev
|
||||
```
|
||||
@@ -11,4 +11,7 @@ export const inferTts = `${apiHost}/tts/inference`;
|
||||
export const listTts = `${apiHost}/tts/list`;
|
||||
|
||||
// User Media
|
||||
export const listMediaByUser = (username:string) => `${apiHost}/v1/media_files/list/user/${username}`;
|
||||
export const listMediaByUser = (username:string) => `${apiHost}/v1/media_files/list/user/${username}`;
|
||||
|
||||
// Any Media
|
||||
export const getMediaFileByToken = (fileToken:string) => `${apiHost}/v1/media_files/file/${fileToken}`;
|
||||
@@ -0,0 +1,56 @@
|
||||
class BucketConfig {
|
||||
isLocalDev: boolean;
|
||||
|
||||
constructor() {
|
||||
this.isLocalDev =
|
||||
document.location.host.includes("localhost") ||
|
||||
document.location.host.includes("jungle.horse") ||
|
||||
document.location.host.startsWith("dev.");
|
||||
}
|
||||
|
||||
// TODO: Prevent callers with undefined/null paths
|
||||
getGcsUrl(bucketRelativePath: string | undefined | null): string {
|
||||
let bucket = this.getBucket();
|
||||
let path = bucketRelativePath;
|
||||
if (path !== undefined && path !== null && !path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
return `https://storage.googleapis.com/${bucket}${path}`;
|
||||
}
|
||||
|
||||
private getBucket(): string {
|
||||
return this.isLocalDev ? "dev-vocodes-public" : "vocodes-public";
|
||||
}
|
||||
|
||||
getCdnUrl(
|
||||
bucketRelativePath: string,
|
||||
width?: number,
|
||||
quality?: number
|
||||
): string {
|
||||
// const basePath = this.isLocalDev
|
||||
// ? "https://dev-cdn.fakeyou.com"
|
||||
// : "https://cdn.fakeyou.com";
|
||||
const basePath = "https://cdn.fakeyou.com";
|
||||
let path = bucketRelativePath?.startsWith("/")
|
||||
? bucketRelativePath
|
||||
: "/" + bucketRelativePath;
|
||||
let resizeParams = "";
|
||||
if (width || quality) {
|
||||
resizeParams = "cdn-cgi/image/";
|
||||
if (width) {
|
||||
resizeParams += `width=${width},`;
|
||||
}
|
||||
if (quality) {
|
||||
resizeParams += `quality=${quality},`;
|
||||
}
|
||||
resizeParams = resizeParams.slice(0, -1);
|
||||
}
|
||||
if (resizeParams) {
|
||||
return `${basePath}/${resizeParams}${path}`;
|
||||
} else {
|
||||
return `${basePath}${path}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { BucketConfig };
|
||||
@@ -0,0 +1,34 @@
|
||||
// interface ApiDetails { hostname: string, useSsl: boolean, }
|
||||
|
||||
const domainWithoutPort = document.location.host.split(":")[0];
|
||||
|
||||
const hostConfig = (host: string, useSsl = true) => ({
|
||||
formatUrl: (endpoint = "") => `${ useSsl ? "https" : "http" }://${ host + endpoint }`,
|
||||
host,
|
||||
useSsl,
|
||||
});
|
||||
|
||||
export default function GetApiHost() {
|
||||
switch (domainWithoutPort) {
|
||||
case "fakeyou.com":
|
||||
case "staging.fakeyou.com":
|
||||
return hostConfig("api.fakeyou.com");
|
||||
case "storyteller.ai":
|
||||
case "staging.storyteller.ai":
|
||||
//return hostConfig("api.storyteller.ai");
|
||||
return hostConfig("api.fakeyou.com");
|
||||
case "storyteller.stream": // Storyteller.stream is deprecated and will be decommissioned in the future.
|
||||
return hostConfig("api.storyteller.stream");
|
||||
case "devproxy.fakeyou.com":
|
||||
return hostConfig("api.fakeyou.com");
|
||||
case "devproxy.storyteller.ai":
|
||||
//return hostConfig("api.storyteller.ai");
|
||||
return hostConfig("api.fakeyou.com");
|
||||
case "dev.fakeyou.com":
|
||||
return hostConfig("api.dev.fakeyou.com:12345",false); // false disables SSL
|
||||
default:
|
||||
return document.location.host.includes("localhost") ?
|
||||
hostConfig("localhost:12345",document.location.protocol === "https:") :
|
||||
hostConfig("api.fakeyou.com"); // default
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import GetApiHost from "./GetApiHost";
|
||||
|
||||
// const { formatUrl } = GetApiHost();
|
||||
|
||||
type UrlRoutingFunction<UrlRouteArgs> = (urlRouteArgs: UrlRouteArgs) => string;
|
||||
|
||||
interface RequestHeaders {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
interface RouteSetup<UrlRouteArgs> {
|
||||
method: string;
|
||||
multipart?: boolean;
|
||||
routingFunction: UrlRoutingFunction<UrlRouteArgs>;
|
||||
}
|
||||
|
||||
const METHOD_OMITS_BODY: { [key: string]: boolean } = {
|
||||
DELETE: false,
|
||||
GET: true,
|
||||
OPTIONS: true,
|
||||
PATCH: false,
|
||||
POST: false,
|
||||
PUT: false,
|
||||
};
|
||||
|
||||
const MakeRequest = <UrlRouteArgs, Request, Response, UrlParams>(routeSetup: RouteSetup<UrlRouteArgs>) : (urlRouteArgs: UrlRouteArgs, request: Request, queries?: UrlParams) => Promise<Response> => {
|
||||
return async function(urlRouteArgs: UrlRouteArgs, request: Request, queries?: any) : Promise<Response> {
|
||||
const newQueries = queries ? Object.keys(queries).map((key, i) => {
|
||||
return `${ i ? "&" : "" }${ key }=${ Array.isArray(queries[key]) ? queries[key].join(`&${ key }=`) : queries[key] }`
|
||||
}).join("") : null;
|
||||
|
||||
const endpoint = `${ routeSetup.routingFunction(urlRouteArgs) }${ newQueries ? "?" + newQueries : "" }`;
|
||||
const method = routeSetup.method;
|
||||
const methodOmitsBody = METHOD_OMITS_BODY[method] || false;
|
||||
|
||||
// return fetch(formatUrl(endpoint), {
|
||||
return fetch(`https://api.fakeyou.com${ endpoint }`, {
|
||||
method,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
...methodOmitsBody? {} : { "Content-Type": "application/json" }
|
||||
},
|
||||
credentials: 'include',
|
||||
...methodOmitsBody ? {} : { body: JSON.stringify(request) },
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (!res.success && res && (res.voice_token || res.dataset_token)) {
|
||||
return res;
|
||||
}
|
||||
else if (res && "success" in res) {
|
||||
return res;
|
||||
} else Promise.reject();
|
||||
})
|
||||
.catch(e => ({ success : false }));
|
||||
}
|
||||
};
|
||||
|
||||
export default MakeRequest;
|
||||
@@ -0,0 +1,26 @@
|
||||
import MakeRequest from "../MakeRequest";
|
||||
import { MediaFile } from "./GetMedia";
|
||||
import { Pagination } from "../_common/SharedFetchTypes";
|
||||
|
||||
export interface GetMediaRequest {}
|
||||
|
||||
export interface GetMediaListResponse {
|
||||
pagination: Pagination;
|
||||
success: boolean;
|
||||
results: MediaFile[];
|
||||
}
|
||||
|
||||
export interface GetMediaParams {
|
||||
page_index: number;
|
||||
}
|
||||
|
||||
export const GetMediaByUser = MakeRequest<
|
||||
string,
|
||||
GetMediaRequest,
|
||||
GetMediaListResponse,
|
||||
GetMediaParams
|
||||
>({
|
||||
method: "GET",
|
||||
routingFunction: (userToken: string) =>
|
||||
`/v1/media_files/list/user/${userToken}`,
|
||||
});
|
||||
@@ -16,12 +16,8 @@ export const Button = ({
|
||||
disabled,
|
||||
...rest
|
||||
}: ButtonPropsI) => {
|
||||
const variant = disabled ? "disabled" : propsVariant;
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "disabled": {
|
||||
return "bg-brand-secondary-500 text-brand-secondary-300";
|
||||
}
|
||||
case "secondary": {
|
||||
return " bg-brand-secondary hover:bg-brand-secondary-900 text-white focus-visible:outline-brand-secondary";
|
||||
}
|
||||
@@ -30,20 +26,25 @@ export const Button = ({
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return " bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600";
|
||||
return "bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const disabledClass = twMerge(
|
||||
disabled ? "opacity-40 pointer-events-none" : "",
|
||||
);
|
||||
|
||||
const className = twMerge(
|
||||
"text-sm font-medium rounded-lg px-3 py-2 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150 flex gap-2 items-center justify-center",
|
||||
getVariantClassNames(variant),
|
||||
getVariantClassNames(propsVariant),
|
||||
propsClassName,
|
||||
disabledClass,
|
||||
);
|
||||
|
||||
return (
|
||||
<button className={className} disabled={disabled} {...rest}>
|
||||
{icon && <FontAwesomeIcon icon={icon} size="sm"/>}
|
||||
{icon && <FontAwesomeIcon icon={icon} size="sm" />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
import { useRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { disableHotkeyInput,enableHotkeyInput,DomLevels } from "~/pages/PageEnigma/store";
|
||||
import {
|
||||
disableHotkeyInput,
|
||||
enableHotkeyInput,
|
||||
DomLevels,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
|
||||
interface InputVectorProps {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
onChange: (v: { x: number; y: number; z: number }) => void;
|
||||
increment?: number;
|
||||
}
|
||||
|
||||
export const InputVector = ({ x, y, z, onChange }: InputVectorProps) => {
|
||||
export const InputVector = ({
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
onChange,
|
||||
increment = 0.1,
|
||||
}: InputVectorProps) => {
|
||||
const xRef = useRef<HTMLInputElement>(null);
|
||||
const yRef = useRef<HTMLInputElement>(null);
|
||||
const zRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const inputCommonClasses =
|
||||
"relative w-14 h-7 rounded-r-lg bg-brand-secondary p-3 text-sm text-white transition-all duration-150 ease-in-out outline-none -outline-offset-2";
|
||||
"relative h-6 rounded-r-lg bg-brand-secondary p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-brand-secondary-900";
|
||||
|
||||
const wrapperCommonClasses =
|
||||
"flex items-center before:inline-block before:w-10 before:h-10 before:bg-brand-primary before:text-white before:rounded-l-lg before:h-7 before:w-5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs";
|
||||
"relative flex items-center before:inline-block before:h-6 before:bg-brand-primary before:text-white before:rounded-l-lg before:w-1.5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs";
|
||||
|
||||
function handleOnChange() {
|
||||
const newVector = {
|
||||
@@ -28,14 +39,67 @@ export const InputVector = ({ x, y, z, onChange }: InputVectorProps) => {
|
||||
};
|
||||
onChange(newVector);
|
||||
}
|
||||
|
||||
const blurAllInputs = () => {
|
||||
xRef.current?.blur();
|
||||
yRef.current?.blur();
|
||||
zRef.current?.blur();
|
||||
};
|
||||
|
||||
// For dragging the input value to increment/decrement
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent,
|
||||
ref: React.RefObject<HTMLInputElement>,
|
||||
) => {
|
||||
e.preventDefault();
|
||||
blurAllInputs();
|
||||
let isDragging = false;
|
||||
let previousMousePosition = e.clientX;
|
||||
|
||||
const mouseMoveHandler = (e: MouseEvent) => {
|
||||
isDragging = true;
|
||||
const currentMousePosition = e.clientX;
|
||||
const direction = currentMousePosition > previousMousePosition ? 1 : -1;
|
||||
previousMousePosition = currentMousePosition;
|
||||
|
||||
const currentValue = Number(ref.current?.value) || 0;
|
||||
const newValue = currentValue + direction * increment;
|
||||
|
||||
if (ref.current) {
|
||||
ref.current.value = newValue.toFixed(1);
|
||||
ref.current.blur();
|
||||
}
|
||||
|
||||
onChange({
|
||||
x: Number(xRef.current?.value) || 0,
|
||||
y: Number(yRef.current?.value) || 0,
|
||||
z: Number(zRef.current?.value) || 0,
|
||||
});
|
||||
};
|
||||
|
||||
const mouseUpHandler = () => {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler);
|
||||
document.removeEventListener("mouseup", mouseUpHandler);
|
||||
if (!isDragging) {
|
||||
ref.current?.focus();
|
||||
ref.current?.select();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", mouseMoveHandler);
|
||||
document.addEventListener("mouseup", mouseUpHandler);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
(e.currentTarget as HTMLInputElement).blur(); // Unfocus the input field on pressing Enter
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-between gap-2">
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-x before:content-['X']",
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col justify-between gap-1.5">
|
||||
<span className={twMerge(wrapperCommonClasses, "before:bg-axis-x")}>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">X</div>
|
||||
<input
|
||||
className={twMerge(inputCommonClasses, "focus:outline-axis-x")}
|
||||
type="number"
|
||||
@@ -44,14 +108,12 @@ export const InputVector = ({ x, y, z, onChange }: InputVectorProps) => {
|
||||
value={x}
|
||||
onFocus={() => disableHotkeyInput(DomLevels.INPUT)}
|
||||
onBlur={() => enableHotkeyInput(DomLevels.INPUT)}
|
||||
onMouseDown={(e) => handleMouseDown(e, xRef)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-y before:content-['Y']",
|
||||
)}
|
||||
>
|
||||
<span className={twMerge(wrapperCommonClasses, "before:bg-axis-y")}>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">Y</div>
|
||||
<input
|
||||
className={twMerge(inputCommonClasses, "focus:outline-axis-y")}
|
||||
type="number"
|
||||
@@ -60,14 +122,12 @@ export const InputVector = ({ x, y, z, onChange }: InputVectorProps) => {
|
||||
value={y}
|
||||
onFocus={() => disableHotkeyInput(DomLevels.INPUT)}
|
||||
onBlur={() => enableHotkeyInput(DomLevels.INPUT)}
|
||||
onMouseDown={(e) => handleMouseDown(e, yRef)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-z before:content-['Z']",
|
||||
)}
|
||||
>
|
||||
<span className={twMerge(wrapperCommonClasses, "before:bg-axis-z")}>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">Z</div>
|
||||
<input
|
||||
className={twMerge(inputCommonClasses, "focus:outline-axis-z")}
|
||||
type="number"
|
||||
@@ -76,6 +136,8 @@ export const InputVector = ({ x, y, z, onChange }: InputVectorProps) => {
|
||||
value={z}
|
||||
onFocus={() => disableHotkeyInput(DomLevels.INPUT)}
|
||||
onBlur={() => enableHotkeyInput(DomLevels.INPUT)}
|
||||
onMouseDown={(e) => handleMouseDown(e, zRef)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export const Pill = ({
|
||||
className,
|
||||
children,
|
||||
}:{
|
||||
className?: string,
|
||||
children : React.ReactNode
|
||||
})=>{
|
||||
return (
|
||||
<div className={twMerge(
|
||||
"rounded-md text-xs font-bold bg-brand-primary py-0.5 px-1.5 w-fit",
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,17 +2,21 @@ import { useEffect } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment, ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { disableHotkeyInput, enableHotkeyInput, DomLevels } from "~/pages/PageEnigma/store";
|
||||
import {
|
||||
disableHotkeyInput,
|
||||
enableHotkeyInput,
|
||||
DomLevels,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
|
||||
const DialogBackdrop = ()=>{
|
||||
useEffect(()=>{
|
||||
const DialogBackdrop = () => {
|
||||
useEffect(() => {
|
||||
disableHotkeyInput(DomLevels.DIALOGUE);
|
||||
return()=>{
|
||||
return () => {
|
||||
enableHotkeyInput(DomLevels.DIALOGUE);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return(
|
||||
return (
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@@ -20,57 +24,57 @@ const DialogBackdrop = ()=>{
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black/60" />
|
||||
</Transition.Child>
|
||||
);
|
||||
};
|
||||
|
||||
export const TransitionDialogue = ({
|
||||
isOpen, title, onClose, className, children,
|
||||
}:{
|
||||
isOpen,
|
||||
title,
|
||||
onClose,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
title?: ReactNode;
|
||||
onClose:()=>void;
|
||||
onClose: () => void;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
|
||||
})=>{
|
||||
return(
|
||||
}) => {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-50 "
|
||||
onClose={onClose}
|
||||
>
|
||||
<DialogBackdrop />
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className={twMerge("w-full max-w-lg transform rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all", className)}>
|
||||
{title && (
|
||||
<Dialog.Title
|
||||
as="h4"
|
||||
className="mb-4 text-xl font-bold text-white"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
<Dialog as="div" className="relative z-50 " onClose={onClose}>
|
||||
<DialogBackdrop />
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95">
|
||||
<Dialog.Panel
|
||||
className={twMerge(
|
||||
"w-full max-w-lg transform rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all",
|
||||
className,
|
||||
)}>
|
||||
{title && (
|
||||
<Dialog.Title
|
||||
as="h4"
|
||||
className="mb-4 text-xl font-bold text-white">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { ListDropdown } from "./ListDropdown"
|
||||
import { ListSearchDropdown } from "./ListSearchDropdown"
|
||||
import { LoadingBar } from "./LoadingBar";
|
||||
import { LoadingDots, LoadingDotsBricks, LoadingDotsTyping } from "./LoadingDots";
|
||||
import { Pill } from "./Pill";
|
||||
|
||||
export {
|
||||
Button,
|
||||
@@ -30,4 +31,5 @@ export {
|
||||
LoadingDots,
|
||||
LoadingDotsBricks,
|
||||
LoadingDotsTyping,
|
||||
Pill
|
||||
};
|
||||
@@ -8,11 +8,12 @@ interface ButtonDialoguePropsI {
|
||||
buttonProps?: UnionedButtonProps;
|
||||
confirmButtonProps?: UnionedButtonProps;
|
||||
closeButtonProps?: UnionedButtonProps;
|
||||
dialogProps?:{
|
||||
dialogProps?: {
|
||||
className?: string;
|
||||
};
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
showClose?: boolean;
|
||||
}
|
||||
|
||||
export const ButtonDialogue = ({
|
||||
@@ -22,6 +23,7 @@ export const ButtonDialogue = ({
|
||||
closeButtonProps: unionedCloseButtonProps,
|
||||
title,
|
||||
children,
|
||||
showClose = true,
|
||||
}: ButtonDialoguePropsI) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -43,19 +45,20 @@ export const ButtonDialogue = ({
|
||||
title={title}
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
className={dialogProps.className}
|
||||
>
|
||||
<div className="mt-2">{children}</div>
|
||||
className={dialogProps.className}>
|
||||
<div className="mt-2 h-full">{children}</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
{...closeButtonProps}
|
||||
variant="secondary"
|
||||
>
|
||||
{closeButtonLabel}
|
||||
</Button>
|
||||
{showClose && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
{...closeButtonProps}
|
||||
variant="secondary">
|
||||
{closeButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{confirmButtonProps && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -65,15 +68,12 @@ export const ButtonDialogue = ({
|
||||
confirmButtonProps.onClick(e);
|
||||
}
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
{confirmButtonProps.label
|
||||
? confirmButtonProps.label
|
||||
: "Confirm"}
|
||||
}}>
|
||||
{confirmButtonProps.label ? confirmButtonProps.label : "Confirm"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
</TransitionDialogue>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
H1, H4, H6, P,
|
||||
Button,
|
||||
ButtonLink,
|
||||
ListDropdown,
|
||||
ListSearchDropdown,
|
||||
// LoadingBar,
|
||||
LoadingDotsTyping,
|
||||
LoadingDotsBricks,
|
||||
} from "~/components";
|
||||
|
||||
|
||||
export function SectionComponents () {
|
||||
|
||||
const [valueListDropdown, setValueListDropdown] = useState<string|undefined>(undefined);
|
||||
const [valueListSearchDropdown, setValueListSearchDropdown] = useState<string|undefined>(undefined);
|
||||
|
||||
const testdata = [
|
||||
{ name: 'Wade Cooper' },
|
||||
{ name: 'Arlene Mccoy' },
|
||||
{ name: 'Devon Webb' },
|
||||
{ name: 'Tom Cook' },
|
||||
{ name: 'Tanya Fox' },
|
||||
{ name: 'Hellen Schmidt' },
|
||||
]
|
||||
|
||||
// Progress Bar Usage States, Variables and Functions
|
||||
// const [progress, setProgress] = useState(0);
|
||||
// const loopRef = useRef<NodeJS.Timeout | null>(null);
|
||||
// const pushProgressBar: ()=>void = useCallback(()=>{
|
||||
// loopRef.current = setInterval(function timer(){
|
||||
// setProgress((curr)=>{
|
||||
// if (curr < 100){
|
||||
// return curr+10;
|
||||
// }else{
|
||||
// if(loopRef.current)
|
||||
// clearInterval(loopRef.current);
|
||||
// return curr;
|
||||
// }
|
||||
// })}
|
||||
// , 3000);
|
||||
// }, []);
|
||||
// useEffect(()=>{
|
||||
// pushProgressBar();
|
||||
// return ()=>{
|
||||
// if(loopRef.current)
|
||||
// clearInterval(loopRef.current)
|
||||
// }
|
||||
// },[progress]);
|
||||
|
||||
return(
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<H1>Components</H1>
|
||||
<H4>Buttons</H4>
|
||||
<div className="flex gap-2">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button className="bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary">
|
||||
Prelim Tertiary
|
||||
</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<H4>ButtonLink</H4>
|
||||
<ButtonLink to="/">Back to /</ButtonLink>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4 max-w-2xl">
|
||||
<H4>List Dropdowns</H4>
|
||||
<ListDropdown
|
||||
list={testdata}
|
||||
onSelect={(val)=>setValueListDropdown(val)}
|
||||
/>
|
||||
<P>Selected Item: <b>{valueListDropdown}</b></P>
|
||||
<ListSearchDropdown
|
||||
list={testdata}
|
||||
onSelect={(val)=>setValueListSearchDropdown(val)}
|
||||
/>
|
||||
<P>Selected Item: <b>{valueListSearchDropdown}</b></P>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<H4>Loading Dots</H4>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-60 h-40 rounded-lg overflow-hidden">
|
||||
<LoadingDotsTyping />
|
||||
</div>
|
||||
<div className="w-60 h-40 rounded-lg overflow-hidden">
|
||||
<LoadingDotsBricks />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4 max-w-2xl">
|
||||
<H4>Loading Bar</H4>
|
||||
<H6>Loading Bar is currently Broken and not reusable</H6>
|
||||
{/* <div className="flex gap-2 items-center">
|
||||
<LoadingBar
|
||||
progress={progress}
|
||||
isShowing={progress !== 100}
|
||||
wrapperClassName="rounded-lg"
|
||||
message="this takes progress from parent"
|
||||
/>
|
||||
<Button
|
||||
className="h-fit"
|
||||
onClick={()=>setProgress(0)}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<LoadingBar
|
||||
label="labeling the bar"
|
||||
message="displaying a message"
|
||||
/>
|
||||
<LoadingBar
|
||||
label="useFakeTimer = 30000 (30secs)"
|
||||
useFakeTimer={30000}
|
||||
/>
|
||||
<LoadingBar
|
||||
label="useFakeTimer = 3000 (3secs)"
|
||||
useFakeTimer={3000}
|
||||
/>*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
H1,H2,H3,H4,H5,H6,P,Label,
|
||||
} from "~/components";
|
||||
|
||||
|
||||
export const SectionTypography = () => {
|
||||
|
||||
return(
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<H1>Typography</H1>
|
||||
<H1>H1. This is an H1 Heading</H1>
|
||||
<H2>H2. This is an H2 Heading</H2>
|
||||
<H3>H3. This is an H3 Heading</H3>
|
||||
<H4>H4. This is an H4 Heading</H4>
|
||||
<Label>Label. Is essentially same as H4</Label>
|
||||
<P>P. Is an normal Paragraph</P>
|
||||
<H5>H5. This is an H5 Heading</H5>
|
||||
<H6>H6. This is an H6 Heading</H6>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SectionTypography } from "./SectionTypography";
|
||||
import { SectionComponents } from "./SectionComponents";
|
||||
|
||||
export const PageComponentLibrary = () => {
|
||||
return (
|
||||
<div
|
||||
className="fixed w-full overflow-scroll"
|
||||
style={{ height: "calc(100% - 72px)" }}
|
||||
>
|
||||
<div
|
||||
className='bg-ui-panel w-10/12 max-w-7xl mx-auto my-6 rounded-lg p-6'
|
||||
>
|
||||
<SectionTypography />
|
||||
<hr className="mt-2 mb-4" />
|
||||
<SectionComponents />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { ButtonLink } from "~/components/ButtonLink";
|
||||
import { TopBarHelmet } from "~/modules/TopBarHelmet";
|
||||
|
||||
import { SidePanel } from "~/modules/SidePanel";
|
||||
import { SceneWithCube } from "./SceneWithCube";
|
||||
|
||||
export const PageCube = () => {
|
||||
const mountRef = useRef<HTMLDivElement | null>(null);
|
||||
const sceneRef = useRef<SceneWithCube | null>(null);
|
||||
const [{ x: rotationX, y: rotationY }, setRotations] = useState<{
|
||||
x: number | undefined;
|
||||
y: number | undefined;
|
||||
}>({ x: undefined, y: undefined });
|
||||
|
||||
const toggleStopped = () => {
|
||||
if (sceneRef.current !== null) {
|
||||
sceneRef.current.stopped = !sceneRef.current.stopped;
|
||||
}
|
||||
};
|
||||
|
||||
const listeningXYRotation = useCallback(
|
||||
({ x, y }: { x: number; y: number }) => {
|
||||
setRotations({
|
||||
x: Math.round(x * 100) / 100,
|
||||
y: Math.round(y * 100) / 100,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
//componentDidMount
|
||||
sceneRef.current = new SceneWithCube(listeningXYRotation);
|
||||
mountRef.current?.appendChild(sceneRef.current.renderer.domElement);
|
||||
|
||||
sceneRef.current.renderer.setAnimationLoop(() => {
|
||||
sceneRef.current?.update();
|
||||
});
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
if (sceneRef.current)
|
||||
mountRef.current?.removeChild(sceneRef.current.renderer.domElement);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-2 ">
|
||||
<TopBarHelmet>
|
||||
<ButtonLink to={"/"} variant="secondary" icon={faChevronLeft}>
|
||||
Back to Dashboard
|
||||
</ButtonLink>
|
||||
</TopBarHelmet>
|
||||
<div className="col-span-6 ">
|
||||
<div className="MountingPoint" ref={mountRef} />
|
||||
</div>
|
||||
<div className="col-span-6 ">
|
||||
<Button onClick={toggleStopped}> STOP </Button>
|
||||
<p className="text-white">Ration X: {rotationX ? rotationX : ""}</p>
|
||||
<p className="text-white">Ration Y: {rotationY ? rotationY : ""}</p>
|
||||
</div>
|
||||
<SidePanel>
|
||||
<p className="text-white">Side Panel Context</p>
|
||||
</SidePanel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class SceneWithCube {
|
||||
scene: THREE.Scene;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
renderer: THREE.WebGLRenderer;
|
||||
cube: THREE.Mesh;
|
||||
stopped: boolean;
|
||||
callback: ({x,y}:{x:number;y:number})=>void;
|
||||
|
||||
update(){
|
||||
if(!this.stopped){
|
||||
this.cube.rotation.x += 0.01;
|
||||
this.cube.rotation.y += 0.01;
|
||||
if(this.callback && typeof this.callback === "function")
|
||||
this.callback({
|
||||
x: this.cube.rotation.x,
|
||||
y: this.cube.rotation.y
|
||||
})
|
||||
}
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
constructor(callback: ({x,y}:{x:number;y:number})=>void) {
|
||||
const width = 1000;
|
||||
const height = 750;
|
||||
|
||||
this.callback = callback;
|
||||
this.stopped = false;
|
||||
|
||||
this.scene = new THREE.Scene();
|
||||
this.camera = new THREE.PerspectiveCamera(75, width/height, 0.1, 1000);
|
||||
this.renderer = new THREE.WebGLRenderer();
|
||||
this.renderer.setSize(width, height);
|
||||
|
||||
const geometry = new THREE.BoxGeometry();
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
|
||||
this.cube = new THREE.Mesh(geometry, material);
|
||||
this.scene.add(this.cube);
|
||||
|
||||
this.camera.position.z = 5;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
export { PageCube } from "./PageCube";
|
||||
@@ -20,4 +20,4 @@ export const PageEnigma = () => {
|
||||
/>
|
||||
</TrackProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -7,8 +7,6 @@ export enum toEngineActions {
|
||||
|
||||
ADD_OBJECT = "add_object",
|
||||
|
||||
ADD_SHAPE = "add_shape",
|
||||
|
||||
// data= QueueKeyframe
|
||||
DELETE_CLIP = "delete_clip",
|
||||
// data=QueueClip
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import {
|
||||
faArrowRightArrowLeft,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faCube,
|
||||
faTrash,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
@@ -19,9 +20,30 @@ import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { toTimelineActions } from "../../Queue/toTimelineActions";
|
||||
import { QueueKeyframe } from "~/pages/PageEnigma/models";
|
||||
|
||||
// TODO this will be useful later to fix the bug on leading zeros
|
||||
const formatNumber = (input: string): number => {
|
||||
// Convert the input string to a number to remove leading zeros
|
||||
const num = parseFloat(input);
|
||||
// Convert the number back to a string with at least two decimal places
|
||||
const str = num.toFixed(2);
|
||||
return parseFloat(str);
|
||||
}
|
||||
|
||||
// console.log(formatNumber("000123.4567")); // Outputs: "123.46"
|
||||
// console.log(formatNumber("000123.4")); // Outputs: "123.40"
|
||||
// console.log(formatNumber("000123")); // Outputs: "123.00"
|
||||
// console.log(formatNumber("0000.00")); // Outputs: "0.00"
|
||||
// console.log(formatNumber("0000.000001")); // Outputs: "0.00"
|
||||
|
||||
export const ControlPanelSceneObject = () => {
|
||||
const editorEngine = useContext(EngineContext);
|
||||
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
const toggleCollapse = () => {
|
||||
setIsCollapsed(!isCollapsed);
|
||||
};
|
||||
|
||||
|
||||
const position =
|
||||
appUiState.controlPanel.currentSceneObject?.objectVectors?.position;
|
||||
@@ -31,6 +53,7 @@ export const ControlPanelSceneObject = () => {
|
||||
appUiState.controlPanel.currentSceneObject?.objectVectors?.scale;
|
||||
const currentSceneObject = appUiState.controlPanel.currentSceneObject;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// TODO this causes a subtle bug because it renders way too many times.
|
||||
if (!appUiState.controlPanel.currentSceneObject) {
|
||||
@@ -169,63 +192,73 @@ export const ControlPanelSceneObject = () => {
|
||||
show={appUiState?.controlPanel.isShowing}
|
||||
className={[
|
||||
"absolute bottom-0 right-0",
|
||||
"m-4 flex h-fit w-fit flex-col gap-2",
|
||||
"m-4 flex h-fit w-56 flex-col gap-2",
|
||||
"rounded-lg",
|
||||
"border border-ui-panel-border",
|
||||
"bg-ui-panel p-4 text-white",
|
||||
"bg-ui-panel p-3.5 text-white",
|
||||
].join(" ")}
|
||||
enter="transition-opacity duration-100"
|
||||
enter="transition-opacity duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-100"
|
||||
leave="transition-opacity duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="flex justify-between">
|
||||
<div className="mb-1 flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
<p className="font-semibold">
|
||||
<p className="max-w-36 truncate font-semibold">
|
||||
{appUiState.controlPanel.currentSceneObject.object_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs font-medium opacity-60">
|
||||
<FontAwesomeIcon icon={faArrowRightArrowLeft} />
|
||||
<p>Swap Object</p>
|
||||
<FontAwesomeIcon
|
||||
icon={isCollapsed ? faChevronUp : faChevronDown}
|
||||
onClick={toggleCollapse}
|
||||
className="cursor-pointer opacity-75 transition-opacity duration-100 ease-in-out hover:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
show={!isCollapsed}
|
||||
enter="transition-all duration-200 ease-in-out"
|
||||
enterFrom="opacity-0 max-h-0"
|
||||
enterTo="opacity-100 max-h-96"
|
||||
leave="transition-all duration-200 ease-in-out"
|
||||
leaveFrom="opacity-100 max-h-96"
|
||||
leaveTo="opacity-0 max-h-0"
|
||||
className={"flex flex-col gap-2 overflow-y-auto"}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<H5>Location</H5>
|
||||
<InputVector
|
||||
x={position?.x || 0}
|
||||
y={position?.y || 0}
|
||||
z={position?.z || 0}
|
||||
onChange={handlePositionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className="h-1" />
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<H5>Position</H5>
|
||||
<InputVector
|
||||
x={position?.x || 0}
|
||||
y={position?.y || 0}
|
||||
z={position?.z || 0}
|
||||
onChange={handlePositionChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<H5>Rotation</H5>
|
||||
<InputVector
|
||||
x={rotation?.x || 0}
|
||||
y={rotation?.y || 0}
|
||||
z={rotation?.z || 0}
|
||||
onChange={handleRotationChange}
|
||||
increment={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<H5>Rotation</H5>
|
||||
<InputVector
|
||||
x={rotation?.x || 0}
|
||||
y={rotation?.y || 0}
|
||||
z={rotation?.z || 0}
|
||||
onChange={handleRotationChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<H5>Scale</H5>
|
||||
<InputVector
|
||||
x={scale?.x || 0}
|
||||
y={scale?.y || 0}
|
||||
z={scale?.z || 0}
|
||||
onChange={handleScaleChange}
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<H5>Scale</H5>
|
||||
<InputVector
|
||||
x={scale?.x || 0}
|
||||
y={scale?.y || 0}
|
||||
z={scale?.z || 0}
|
||||
onChange={handleScaleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="h-2" />
|
||||
<div className="flex gap-2">
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="grow"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ToasterContext } from "~/contexts/ToasterContext";
|
||||
// import { APPUI_ACTION_TYPES } from "app/reducers";
|
||||
|
||||
import { TestFeaturesButtons } from "./TestFeaturesButtons";
|
||||
import { Help } from "./Help";
|
||||
|
||||
export const ControlsTopButtons = () => {
|
||||
const editorEngine = useContext(EngineContext);
|
||||
@@ -49,8 +50,7 @@ export const ControlsTopButtons = () => {
|
||||
label: "Load",
|
||||
disabled: sceneToken === "",
|
||||
onClick: handleButtonLoadScene,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Input
|
||||
label="Please provide the Token of the scene you wished to load:"
|
||||
onChange={(e) => {
|
||||
@@ -69,8 +69,7 @@ export const ControlsTopButtons = () => {
|
||||
label: "Save",
|
||||
disabled: sceneName === "",
|
||||
onClick: handleButtonSave,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Input
|
||||
label="Please Enter a name for your scene"
|
||||
onChange={(e) => {
|
||||
@@ -89,8 +88,7 @@ export const ControlsTopButtons = () => {
|
||||
disabled: mediaToken === "",
|
||||
onClick: handleMediaToken,
|
||||
}}
|
||||
title="Add Scene Object via Media Token"
|
||||
>
|
||||
title="Add Scene Object via Media Token">
|
||||
<Input
|
||||
label="Please Enter a Media Token"
|
||||
onChange={(e) => {
|
||||
@@ -104,15 +102,14 @@ export const ControlsTopButtons = () => {
|
||||
variant: "secondary",
|
||||
label: "Help",
|
||||
}}
|
||||
title="Help"
|
||||
>
|
||||
<p>Do you need help?</p>
|
||||
<p>Ask Michael about this project</p>
|
||||
<p>Ask Miles about ThreeJS</p>
|
||||
<p>Ask Wil about React</p>
|
||||
dialogProps={{
|
||||
className: "max-w-6xl w-auto",
|
||||
}}
|
||||
title="Help">
|
||||
<Help />
|
||||
</ButtonDialogue>
|
||||
</div>
|
||||
<TestFeaturesButtons debug={false}/>
|
||||
<TestFeaturesButtons debug={false} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Label } from "~/components";
|
||||
|
||||
const ShortcutsGroup = (props: {
|
||||
label: string;
|
||||
children?: React.ReactNode;
|
||||
}) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label className="text-md font-semibold" {...props}>
|
||||
{props.label}
|
||||
</Label>
|
||||
<div className="relative flex flex-col gap-2.5">
|
||||
{props.children}
|
||||
<div className="absolute top-0 h-full w-0.5 bg-white/15" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Shortcut = (props: { label: string; children: React.ReactNode }) => (
|
||||
<div className="flex items-center gap-2 ps-4">
|
||||
<span className="text-md w-[280px] font-medium opacity-80">
|
||||
{props.label}
|
||||
</span>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Key = (props: { button: string }) => (
|
||||
<div className="flex h-7 w-auto min-w-7 items-center justify-center rounded-md border-b-2 border-[#9E9EA6] bg-white px-2 pt-1 text-center text-sm font-bold text-ui-background">
|
||||
{props.button}
|
||||
</div>
|
||||
);
|
||||
|
||||
const KeyGroup = (props: { children: React.ReactNode }) => (
|
||||
<div className="flex gap-1">{props.children}</div>
|
||||
);
|
||||
|
||||
const Mouse = (props: {
|
||||
button: "left" | "middle" | "right";
|
||||
label: string;
|
||||
}) => (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<img
|
||||
alt={`${props.button} mouse`}
|
||||
src={`/resources/icons/mouse_${props.button === "left" ? "lmb" : props.button === "middle" ? "mmb" : "rmb"}.png`}
|
||||
className="h-7 w-auto object-contain"
|
||||
/>
|
||||
<span className="text-sm font-normal opacity-60">{props.label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Plus = () => <div className="text-xl font-medium">+</div>;
|
||||
|
||||
export const Help = () => {
|
||||
return (
|
||||
<div className="mt-8 grid select-none grid-cols-2 gap-12 ps-4">
|
||||
<div className="flex flex-col gap-8">
|
||||
<ShortcutsGroup label="Navigation">
|
||||
<Shortcut label="Pan View">
|
||||
<Mouse button="right" label="(Drag)" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Rotate View">
|
||||
<Mouse button="middle" label="(Drag)" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Zoom">
|
||||
<Mouse button="middle" label="(Scroll)" />
|
||||
</Shortcut>
|
||||
{/* <Shortcut label="Free Fly">
|
||||
<KeyGroup>
|
||||
<Key button="W" />
|
||||
<Key button="A" />
|
||||
<Key button="S" />
|
||||
<Key button="D" />
|
||||
<Key button="Q" />
|
||||
<Key button="E" />
|
||||
</KeyGroup>
|
||||
</Shortcut> */}
|
||||
</ShortcutsGroup>
|
||||
|
||||
<ShortcutsGroup label="Interaction">
|
||||
<Shortcut label="Select Object">
|
||||
<Mouse button="left" label="(Click)" />
|
||||
</Shortcut>
|
||||
{/* <Shortcut label="Clear Selection">
|
||||
<Key button="Esc" />
|
||||
</Shortcut> */}
|
||||
{/* <Shortcut label="Focus Selection">
|
||||
<Key button="F" />
|
||||
</Shortcut> */}
|
||||
<Shortcut label="Delete Selection">
|
||||
<Key button="Del" />
|
||||
</Shortcut>
|
||||
</ShortcutsGroup>
|
||||
|
||||
<ShortcutsGroup label="Side Panel">
|
||||
<Shortcut label="Add Character">
|
||||
<Mouse button="left" label="(Drag onto scene)" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Add Animation">
|
||||
<Mouse button="left" label="(Drag onto timeline)" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Add Object">
|
||||
<Mouse button="left" label="(Drag onto scene)" />
|
||||
</Shortcut>
|
||||
</ShortcutsGroup>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<ShortcutsGroup label="Timeline">
|
||||
<Shortcut label="Select Clip">
|
||||
<Mouse button="left" label="(Click)" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Move Clip">
|
||||
<Mouse button="left" label="(Drag)" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Delete Selection">
|
||||
<Key button="Del" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Scroll">
|
||||
<Mouse button="middle" label="(Scroll)" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Side Scroll">
|
||||
<Key button="Shift" />
|
||||
<Plus />
|
||||
<Mouse button="middle" label="(Scroll)" />
|
||||
</Shortcut>
|
||||
{/* <Shortcut label="Zoom">
|
||||
<Key button="Ctrl/Cmd" />
|
||||
<Plus />
|
||||
<Mouse button="middle" label="(Scroll)" />
|
||||
</Shortcut> */}
|
||||
<Shortcut label="Add Keyframe to Selected Object">
|
||||
<Key button="K" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Select Keyframe">
|
||||
<Mouse button="left" label="(Click)" />
|
||||
</Shortcut>
|
||||
<Shortcut label="Delete Selected Keyframe">
|
||||
<Key button="Del" />
|
||||
</Shortcut>
|
||||
</ShortcutsGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Help } from "./Help";
|
||||
@@ -1,53 +1,40 @@
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignals, useSignalEffect } from "@preact/signals-react/runtime";
|
||||
import { faCirclePlus, faArrowsRotate } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { AppUiContext } from "~/contexts/AppUiContext";
|
||||
import { AuthenticationContext } from "~/contexts/Authentication";
|
||||
import { APPUI_ACTION_TYPES } from "~/reducers";
|
||||
import { audioFilter, audioItems } from "~/pages/PageEnigma/store";
|
||||
import {
|
||||
AssetFilterOption,
|
||||
MediaItem,
|
||||
AssetType,
|
||||
} from "~/pages/PageEnigma/models";
|
||||
|
||||
import { Button, ButtonIcon } from "~/components";
|
||||
|
||||
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
|
||||
import { MediaItem, AssetType } from "~/pages/PageEnigma/models";
|
||||
import { audioItemsFromServer } from "~/pages/PageEnigma/store/mediaFromServer";
|
||||
import { ListAudioByUser } from "./listAudioByUser";
|
||||
import { ListAudioByUser } from "./utilities";
|
||||
import { inferenceJobs } from "~/pages/PageEnigma/store/inferenceJobs";
|
||||
import { JobState } from "~/hooks/useInferenceJobManager/useInferenceJobManager";
|
||||
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
|
||||
|
||||
import { PageLibrary } from "./pageLibrary";
|
||||
import { PageTTS } from "./pageTTS";
|
||||
import { AudioTabPages } from "./types";
|
||||
|
||||
export const AudioTabComponent = () => {
|
||||
useSignals();
|
||||
const [state, setState] = useState({
|
||||
firstLoad: false,
|
||||
fetchingUserAudio: false,
|
||||
page: AudioTabPages.LIBRARY,
|
||||
});
|
||||
const { authState } = useContext(AuthenticationContext);
|
||||
const [, dispatchAppUiState] = useContext(AppUiContext);
|
||||
const handleOpenTtsDialogue = () => {
|
||||
dispatchAppUiState({
|
||||
type: APPUI_ACTION_TYPES.OPEN_DIALOGUE_TTS,
|
||||
});
|
||||
};
|
||||
const allAudioItems = [...audioItems.value, ...audioItemsFromServer.value];
|
||||
|
||||
const handleListAudioByUser = useCallback((username: string) => {
|
||||
setState((curr) => ({ ...curr, fetchingUserAudio: true }));
|
||||
ListAudioByUser(username).then((res: any[]) => {
|
||||
setState((curr) => ({ ...curr, fetchingUserAudio: false }));
|
||||
audioItemsFromServer.value = res.map((item) => {
|
||||
const morphedItem: MediaItem = {
|
||||
const handleListAudioByUser = useCallback((username:string, sessionToken:string)=>{
|
||||
setState((curr)=>({...curr, fetchingUserAudio:true}));
|
||||
ListAudioByUser(username, sessionToken).then((res:any[])=>{
|
||||
setState((curr)=>({...curr, fetchingUserAudio:false}));
|
||||
audioItemsFromServer.value = res.map(item=>{
|
||||
const morphedItem:MediaItem = {
|
||||
version: 1,
|
||||
type: AssetType.AUDIO,
|
||||
media_id: item.token,
|
||||
object_uuid: item.token,
|
||||
name: item.maybe_title || item.origin.maybe_model.title,
|
||||
description: item.maybe_text_transcript,
|
||||
publicBucketPath: item.public_bucket_path,
|
||||
length: 25,
|
||||
thumbnail: "/resources/placeholders/audio_placeholder.png",
|
||||
isMine: true,
|
||||
@@ -58,14 +45,11 @@ export const AudioTabComponent = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authState.userInfo) {
|
||||
if (
|
||||
state.firstLoad === false &&
|
||||
audioItemsFromServer.value.length === 0
|
||||
) {
|
||||
handleListAudioByUser(authState.userInfo.username);
|
||||
setState((curr) => ({ ...curr, firstLoad: true }));
|
||||
useEffect(()=>{
|
||||
if (authState.userInfo && authState.sessionToken){
|
||||
if(state.firstLoad === false && audioItemsFromServer.value.length === 0){
|
||||
handleListAudioByUser(authState.userInfo.username, authState.sessionToken);
|
||||
setState((curr)=>({...curr, firstLoad:true}));
|
||||
}
|
||||
}
|
||||
}, [authState, state, handleListAudioByUser]);
|
||||
@@ -89,71 +73,30 @@ export const AudioTabComponent = () => {
|
||||
return foundItemOfJob !== undefined;
|
||||
}
|
||||
});
|
||||
if (found === undefined) {
|
||||
handleListAudioByUser(authState.userInfo.username);
|
||||
if(found === undefined && authState.sessionToken){
|
||||
handleListAudioByUser(authState.userInfo.username, authState.sessionToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full overflow-x-auto p-4 pb-0">
|
||||
<TabTitle title="Audio" />
|
||||
<div className="mb-4 flex justify-start gap-2">
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
audioFilter.value === AssetFilterOption.ALL ? "active" : "",
|
||||
)}
|
||||
onClick={() => (audioFilter.value = AssetFilterOption.ALL)}>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
audioFilter.value === AssetFilterOption.MINE ? "active" : "",
|
||||
)}
|
||||
onClick={() => (audioFilter.value = AssetFilterOption.MINE)}
|
||||
disabled={!allAudioItems.some((item) => item.isMine)}>
|
||||
My Audios
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
audioFilter.value === AssetFilterOption.BOOKMARKED
|
||||
? "active"
|
||||
: "",
|
||||
)}
|
||||
onClick={() => (audioFilter.value = AssetFilterOption.BOOKMARKED)}
|
||||
disabled={!allAudioItems.some((item) => item.isBookmarked)}>
|
||||
Bookmarked
|
||||
</button>
|
||||
<ButtonIcon
|
||||
className={twMerge(
|
||||
"absolute right-0 mr-4",
|
||||
// state.fetchingUserAudio ? "animate-spin" : "",
|
||||
)}
|
||||
icon={faArrowsRotate}
|
||||
onClick={() => {
|
||||
if (authState.userInfo?.username) {
|
||||
handleListAudioByUser(authState.userInfo?.username);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full px-4">
|
||||
<Button
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
className="w-full py-3 text-sm font-medium"
|
||||
onClick={handleOpenTtsDialogue}>
|
||||
Generate Audio
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full overflow-y-auto px-4 pt-4">
|
||||
<ItemElements items={allAudioItems} assetFilter={audioFilter.value} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
const changePage = (newPage:AudioTabPages)=>{
|
||||
setState((curr)=>({
|
||||
...curr,
|
||||
page: newPage
|
||||
}))
|
||||
}
|
||||
if(state.page === AudioTabPages.LIBRARY){
|
||||
return <PageLibrary changePage={changePage}/>
|
||||
}else if(state.page === AudioTabPages.TTS && authState.sessionToken){
|
||||
return(
|
||||
<PageTTS
|
||||
changePage={changePage}
|
||||
sessionToken={authState.sessionToken}
|
||||
/>
|
||||
);
|
||||
}else{
|
||||
return(
|
||||
<p>Unknown Error</p>
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
import { faCirclePlay, faCirclePause } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { ButtonIcon } from "~/components";
|
||||
|
||||
export const WaveformPlayer = ({ audio }: { audio: string }) => {
|
||||
const waveSurferRef = useRef<WaveSurfer | undefined>(undefined);
|
||||
const [isPlaying, toggleIsPlaying] = useState(false);
|
||||
|
||||
const containerRef = useCallback((node: HTMLDivElement) => {
|
||||
if (node) {
|
||||
const waveSurfer = WaveSurfer.create({
|
||||
container: node,
|
||||
barWidth: 2,
|
||||
height: 24,
|
||||
cursorWidth: 0,
|
||||
waveColor: "#D7C8C8",
|
||||
progressColor: "#FB8381",
|
||||
});
|
||||
waveSurfer.load(audio);
|
||||
waveSurfer.on("ready", () => {
|
||||
waveSurferRef.current = waveSurfer;
|
||||
});
|
||||
waveSurfer.on("play", () => {
|
||||
toggleIsPlaying(true);
|
||||
});
|
||||
waveSurfer.on("pause", () => {
|
||||
toggleIsPlaying(false);
|
||||
});
|
||||
waveSurfer.on("finish", () => {
|
||||
waveSurfer.seekTo(0);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.log("OK?");
|
||||
waveSurferRef.current?.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1">
|
||||
<ButtonIcon
|
||||
icon={isPlaying ? faCirclePause : faCirclePlay}
|
||||
className="w-auto bg-transparent p-0 text-2xl hover:bg-transparent hover:opacity-75"
|
||||
onClick={() => {
|
||||
waveSurferRef.current?.playPause();
|
||||
}}
|
||||
/>
|
||||
<div ref={containerRef} className="w-full" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useCallback, useContext, useEffect } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
import { WaveformPlayer } from "./WaveformPlayer";
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import {
|
||||
canDrop,
|
||||
currPosition,
|
||||
dragItem,
|
||||
initPosition,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
|
||||
import { H5, H6, Pill } from "~/components";
|
||||
|
||||
function getGcsUrl(bucketRelativePath: string | undefined | null): string {
|
||||
let bucket = "vocodes-public";
|
||||
let path = bucketRelativePath;
|
||||
if (path !== undefined && path !== null && !path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
return `https://storage.googleapis.com/${bucket}${path}`;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item: MediaItem;
|
||||
}
|
||||
|
||||
export const AudioItemElement = ({ item }: Props) => {
|
||||
useSignals();
|
||||
const { startDrag, endDrag } = useContext(TrackContext);
|
||||
|
||||
const { initX, initY } = initPosition.value;
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerUp = () => {
|
||||
if (dragItem.value) {
|
||||
endDrag();
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (dragItem.value) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const deltaX = event.pageX - initX;
|
||||
const deltaY = event.pageY - initY;
|
||||
currPosition.value = { currX: initX + deltaX, currY: initY + deltaY };
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onMouseMove);
|
||||
};
|
||||
}, [startDrag, endDrag, initX, initY]);
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
startDrag(item);
|
||||
currPosition.value = {
|
||||
currX: event.pageX,
|
||||
currY: event.pageY,
|
||||
};
|
||||
initPosition.value = {
|
||||
initX: event.pageX,
|
||||
initY: event.pageY,
|
||||
};
|
||||
canDrop.value = false;
|
||||
}
|
||||
},
|
||||
[item, startDrag],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full cursor-pointer rounded-lg transition-all duration-200"
|
||||
onPointerDown={onPointerDown}>
|
||||
<div className="flex w-full flex-col gap-0.5 rounded-lg bg-assets-background p-2.5">
|
||||
<div className="flex justify-between">
|
||||
<Pill>Audio</Pill>
|
||||
</div>
|
||||
|
||||
{item.publicBucketPath && (
|
||||
<WaveformPlayer audio={getGcsUrl(item.publicBucketPath)} />
|
||||
)}
|
||||
|
||||
<H5 className="text-overflow-ellipsis">{item.name}</H5>
|
||||
{item.description && (
|
||||
<H6 className="text-overflow-ellipsis text-xs text-white/90">
|
||||
{item.description}
|
||||
</H6>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { AudioItemElement } from "./audioItemElement";
|
||||
import { AssetFilterOption, MediaItem } from "~/pages/PageEnigma/models";
|
||||
|
||||
interface Props {
|
||||
items: MediaItem[];
|
||||
assetFilter: AssetFilterOption;
|
||||
}
|
||||
|
||||
export const AudioItemElements = ({ items, assetFilter }: Props) => {
|
||||
const displayItems = items.filter((item) => {
|
||||
if (assetFilter === AssetFilterOption.ALL) {
|
||||
return true;
|
||||
}
|
||||
if (assetFilter === AssetFilterOption.MINE) {
|
||||
return item.isMine;
|
||||
}
|
||||
return item.isBookmarked;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`grid grid-cols-1 gap-2.5`}>
|
||||
{displayItems.map((item) => (
|
||||
<AudioItemElement key={item.media_id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import { listMediaByUser } from "~/api";
|
||||
import {
|
||||
MediaFileType,
|
||||
MediaFileClass,
|
||||
MediaFileSubtype,
|
||||
UserDetailsLight,
|
||||
WeightCategory,
|
||||
WeightType
|
||||
} from "./types";
|
||||
|
||||
|
||||
export interface MediaFile {
|
||||
token: string;
|
||||
media_type: MediaFileType;
|
||||
media_class: MediaFileClass | null;
|
||||
maybe_media_subtype: MediaFileSubtype | null;
|
||||
public_bucket_path: string;
|
||||
maybe_engine_extension: string | null;
|
||||
maybe_batch_token: string;
|
||||
maybe_title: string | null;
|
||||
maybe_original_filename: string | null;
|
||||
maybe_creator_user: UserDetailsLight | null;
|
||||
maybe_prompt_token: string | null;
|
||||
creator_set_visibility: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
maybe_model_weight_info: {
|
||||
title: string;
|
||||
weight_token: string;
|
||||
weight_category: WeightCategory;
|
||||
weight_type: WeightType;
|
||||
maybe_weight_creator: UserDetailsLight;
|
||||
maybe_cover_image_public_bucket_path: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ListAudioByUser = async(username:string) => {
|
||||
return await fetch(listMediaByUser(username),{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
},
|
||||
// credentials: 'include'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if(res.success && res.results){
|
||||
return res.results.filter((item:MediaFile)=>item['media_type']==='audio');
|
||||
}else{
|
||||
Promise.reject();
|
||||
}
|
||||
})
|
||||
.catch(e => ({ success : false }));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { audioFilter, audioItems } from "~/pages/PageEnigma/store";
|
||||
import { AssetFilterOption } from "~/pages/PageEnigma/models";
|
||||
import { audioItemsFromServer } from "~/pages/PageEnigma/store/mediaFromServer";
|
||||
|
||||
import { Button } from "~/components";
|
||||
import { AudioItemElements } from "./audioItemElements";
|
||||
import { AudioTabPages } from "./types";
|
||||
|
||||
export const PageLibrary = ({
|
||||
changePage,
|
||||
}: {
|
||||
changePage: (newPage: AudioTabPages) => void;
|
||||
}) => {
|
||||
useSignals();
|
||||
const allAudioItems = [...audioItems.value, ...audioItemsFromServer.value];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full overflow-x-auto">
|
||||
<div className="mb-4 mt-4 flex items-center justify-start gap-2 px-4">
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
audioFilter.value === AssetFilterOption.ALL ? "active" : "",
|
||||
"disabled",
|
||||
)}
|
||||
onClick={() => (audioFilter.value = AssetFilterOption.ALL)}>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
audioFilter.value === AssetFilterOption.MINE ? "active" : "",
|
||||
"disabled",
|
||||
)}
|
||||
onClick={() => (audioFilter.value = AssetFilterOption.MINE)}
|
||||
disabled={!allAudioItems.some((item) => item.isMine)}>
|
||||
My Audios
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(
|
||||
"filter-tab",
|
||||
audioFilter.value === AssetFilterOption.BOOKMARKED
|
||||
? "active"
|
||||
: "",
|
||||
"disabled",
|
||||
)}
|
||||
onClick={() => (audioFilter.value = AssetFilterOption.BOOKMARKED)}
|
||||
disabled={!allAudioItems.some((item) => item.isBookmarked)}>
|
||||
Bookmarked
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full px-4">
|
||||
<Button
|
||||
icon={faCirclePlus}
|
||||
variant="action"
|
||||
className="w-full py-3 text-sm font-medium"
|
||||
onClick={() => changePage(AudioTabPages.TTS)}>
|
||||
Generate Audio
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 h-full w-full overflow-y-auto px-4">
|
||||
<AudioItemElements
|
||||
items={allAudioItems}
|
||||
assetFilter={audioFilter.value}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,253 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSignalEffect } from "@preact/signals-react/runtime";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import {
|
||||
faChevronLeft,
|
||||
faBrainCircuit,
|
||||
faVolumeHigh,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
H2,
|
||||
Label,
|
||||
LoadingDotsTyping,
|
||||
ListSearchDropdown,
|
||||
Textarea,
|
||||
} from "~/components";
|
||||
|
||||
import {
|
||||
TtsModelListItem,
|
||||
GenerateTtsAudioResponse,
|
||||
} from "~/pages/PageEnigma/models/tts";
|
||||
|
||||
import { addInferenceJob, inferenceJobs } from "../../../store/inferenceJobs";
|
||||
import { JobState } from "~/hooks/useInferenceJobManager/useInferenceJobManager";
|
||||
import {
|
||||
ListTtsModels,
|
||||
GenerateTtsAudio,
|
||||
GetMediaFileByToken,
|
||||
} from "./utilities";
|
||||
import { AudioTabPages } from "./types";
|
||||
import { AudioItemElement } from "./audioItemElement";
|
||||
import { MediaItem, AssetType } from "~/pages/PageEnigma/models";
|
||||
|
||||
type TtsState = {
|
||||
voice: TtsModelListItem | undefined;
|
||||
text: string;
|
||||
hasEnqueued: boolean;
|
||||
inferenceToken?: string;
|
||||
inferenceJobType?: string;
|
||||
hasTtsResult: boolean;
|
||||
};
|
||||
const initialState: TtsState = {
|
||||
voice: undefined,
|
||||
text: "",
|
||||
hasEnqueued: false,
|
||||
inferenceJobType: undefined,
|
||||
inferenceToken: undefined,
|
||||
hasTtsResult: false,
|
||||
};
|
||||
|
||||
export const PageTTS = ({
|
||||
changePage,
|
||||
sessionToken,
|
||||
}: {
|
||||
changePage: (newPage: AudioTabPages) => void;
|
||||
sessionToken: string;
|
||||
}) => {
|
||||
const [ttsState, setTtsState] = useState<TtsState>(initialState);
|
||||
|
||||
const [ttsResultFile, setTtsResultFile] = useState<MediaItem | undefined>();
|
||||
|
||||
useSignalEffect(() => {
|
||||
console.log(inferenceJobs.value);
|
||||
if (ttsState.hasEnqueued && ttsState.inferenceToken) {
|
||||
const found = inferenceJobs.value.find(
|
||||
(job) => job.job_id === ttsState.inferenceToken,
|
||||
);
|
||||
console.log(`finding: ${ttsState.inferenceToken}`);
|
||||
console.log(found);
|
||||
if (found?.job_status === JobState.COMPLETE_SUCCESS) {
|
||||
setTtsState((curr) => ({
|
||||
...curr,
|
||||
hasTtsResult: true,
|
||||
}));
|
||||
GetMediaFileByToken(found.result.entity_token, sessionToken).then(
|
||||
(res) => {
|
||||
console.log(res);
|
||||
const morphedItem: MediaItem = {
|
||||
version: 1,
|
||||
type: AssetType.AUDIO,
|
||||
media_id: res.media_file.token,
|
||||
object_uuid: res.media_file.token,
|
||||
name: ttsState.voice?.title || "",
|
||||
description: ttsState.text,
|
||||
publicBucketPath: res.media_file.public_bucket_path,
|
||||
length: 25,
|
||||
thumbnail: "/resources/placeholders/audio_placeholder.png",
|
||||
isMine: true,
|
||||
// isBookmarked?: boolean;
|
||||
};
|
||||
setTtsResultFile(morphedItem);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const [ttsModels, setTtsModels] = useState<Array<TtsModelListItem>>([]);
|
||||
|
||||
const listModels = useCallback(async () => {
|
||||
const ttsModelsLoaded = ttsModels.length > 0;
|
||||
if (ttsModelsLoaded) {
|
||||
return; // Already queried.
|
||||
}
|
||||
const models = await ListTtsModels(sessionToken);
|
||||
if (models) {
|
||||
setTtsModels(models);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
listModels();
|
||||
}, [listModels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
ttsState.hasEnqueued &&
|
||||
ttsState.inferenceToken &&
|
||||
ttsState.inferenceJobType
|
||||
) {
|
||||
console.log(`tts has Enqueued`);
|
||||
addInferenceJob({
|
||||
version: 1,
|
||||
job_id: ttsState.inferenceToken,
|
||||
job_type: ttsState.inferenceJobType,
|
||||
job_status: JobState.PENDING,
|
||||
});
|
||||
}
|
||||
}, [ttsState]);
|
||||
|
||||
const requestTts = useCallback(
|
||||
(sessionToken: string) => {
|
||||
const modelToken = ttsState.voice
|
||||
? ttsState.voice.model_token
|
||||
: undefined;
|
||||
|
||||
if (modelToken) {
|
||||
setTtsState((curr) => ({
|
||||
...curr,
|
||||
hasEnqueued: true,
|
||||
inferenceToken: undefined,
|
||||
inferenceJobType: undefined,
|
||||
hasAudio: false,
|
||||
result: undefined,
|
||||
}));
|
||||
|
||||
const request = {
|
||||
uuid_idempotency_token: uuidv4(),
|
||||
tts_model_token: modelToken,
|
||||
inference_text: ttsState.text,
|
||||
};
|
||||
|
||||
GenerateTtsAudio(request, sessionToken).then(
|
||||
(res: GenerateTtsAudioResponse) => {
|
||||
if (res && res.inference_job_token) {
|
||||
setTtsState((curr) => ({
|
||||
...curr,
|
||||
inferenceToken: res.inference_job_token,
|
||||
inferenceJobType: res.inference_job_token_type,
|
||||
}));
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
console.log("no voice model selected");
|
||||
}
|
||||
},
|
||||
[ttsState],
|
||||
);
|
||||
|
||||
const handleTextInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setTtsState((curr) => ({
|
||||
...curr,
|
||||
text: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleOnSelect = (val: string) => {
|
||||
console.log(val);
|
||||
const voiceModel = ttsModels.find((item) => {
|
||||
if (item.title === val) return item;
|
||||
});
|
||||
setTtsState((curr) => ({
|
||||
...curr,
|
||||
voice: voiceModel,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4">
|
||||
<div className="mb-5 flex items-center gap-3">
|
||||
<ButtonIcon
|
||||
className="w-auto p-0 text-xl opacity-60 hover:opacity-40"
|
||||
icon={faChevronLeft}
|
||||
onClick={() => changePage(AudioTabPages.LIBRARY)}
|
||||
/>
|
||||
<H2 className="font-semibold">Generate TTS</H2>
|
||||
</div>
|
||||
<Label className="mb-1">Select a Voice</Label>
|
||||
{ttsModels.length > 0 && (
|
||||
<ListSearchDropdown
|
||||
list={ttsModels}
|
||||
listDisplayKey="title"
|
||||
onSelect={handleOnSelect}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
<Label>What would you like to say?</Label>
|
||||
<div className="flex items-center gap-2"></div>
|
||||
</div>
|
||||
<Textarea
|
||||
placeholder="Enter what you want the voice to say here."
|
||||
value={ttsState.text}
|
||||
onChange={handleTextInput}
|
||||
rows={8}
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<div className="h-auto w-full">
|
||||
{!ttsState.hasTtsResult && !ttsState.hasEnqueued && (
|
||||
<Button
|
||||
className="h-11 w-full text-sm"
|
||||
variant={ttsState.hasTtsResult ? "secondary" : "primary"}
|
||||
disabled={ttsState.text === ""}
|
||||
icon={faVolumeHigh}
|
||||
onClick={() => requestTts(sessionToken)}>
|
||||
Generate
|
||||
</Button>
|
||||
)}
|
||||
{!ttsState.hasTtsResult && ttsState.hasEnqueued && (
|
||||
<LoadingDotsTyping className="rounded-lg bg-brand-secondary-500" />
|
||||
)}
|
||||
{ttsResultFile && <AudioItemElement item={ttsResultFile} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-between gap-2">
|
||||
{ttsState.hasEnqueued && (
|
||||
<Button
|
||||
className="h-11 w-full text-sm"
|
||||
type="button"
|
||||
disabled={!ttsState.hasTtsResult || ttsState.text === ""}
|
||||
onClick={() => requestTts(sessionToken)}
|
||||
icon={faVolumeHigh}>
|
||||
Generate Another
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -122,3 +122,39 @@ export enum WeightType {
|
||||
VALL_E = "vall_e",
|
||||
NONE = "none",
|
||||
}
|
||||
|
||||
export enum AudioTabPages {
|
||||
LIBRARY = 'library',
|
||||
TTS = 'tts',
|
||||
}
|
||||
|
||||
|
||||
export interface MediaFile {
|
||||
token: string;
|
||||
media_type: MediaFileType;
|
||||
media_class: MediaFileClass | null;
|
||||
maybe_media_subtype: MediaFileSubtype | null;
|
||||
public_bucket_path: string;
|
||||
maybe_engine_extension: string | null;
|
||||
maybe_batch_token: string;
|
||||
maybe_title: string | null;
|
||||
maybe_original_filename: string | null;
|
||||
maybe_creator_user: UserDetailsLight | null;
|
||||
maybe_prompt_token: string | null;
|
||||
creator_set_visibility: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
maybe_model_weight_info: {
|
||||
title: string;
|
||||
weight_token: string;
|
||||
weight_category: WeightCategory;
|
||||
weight_type: WeightType;
|
||||
maybe_weight_creator: UserDetailsLight;
|
||||
maybe_cover_image_public_bucket_path: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetMediaFileResponse {
|
||||
success: boolean;
|
||||
media_file?: MediaFile;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { listTts, inferTts, listMediaByUser, getMediaFileByToken } from '~/api';
|
||||
import { MediaFile, GetMediaFileResponse } from './types';
|
||||
import {
|
||||
TtsModelListItem,
|
||||
TtsModelListResponsePayload,
|
||||
GenerateTtsAudioErrorType,
|
||||
StatusLike,
|
||||
GenerateTtsAudioRequest,
|
||||
GenerateTtsAudioResponse,
|
||||
} from '~/pages/PageEnigma/models/tts';
|
||||
|
||||
|
||||
|
||||
export const ListAudioByUser = async(username:string, sessionToken: string) => {
|
||||
return await fetch(listMediaByUser(username),{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'session': sessionToken,
|
||||
},
|
||||
// credentials: 'include'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if(res.success && res.results){
|
||||
return res.results.filter((item:MediaFile)=>item['media_type']==='audio');
|
||||
}else{
|
||||
Promise.reject();
|
||||
}
|
||||
})
|
||||
.catch(e => ({ success : false }));
|
||||
}
|
||||
|
||||
export async function ListTtsModels(sessionToken:string) : Promise<Array<TtsModelListItem>| undefined> {
|
||||
return await fetch(listTts, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'session': sessionToken,
|
||||
},
|
||||
// credentials: 'include',
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
const response : TtsModelListResponsePayload = res;
|
||||
if (!response.success) {
|
||||
return;
|
||||
}
|
||||
return response?.models;
|
||||
})
|
||||
.catch(e => {
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function maybeMapError(statuslike: StatusLike) : GenerateTtsAudioErrorType | undefined {
|
||||
switch (statuslike.status) {
|
||||
case 400:
|
||||
return GenerateTtsAudioErrorType.BadRequest;
|
||||
case 404:
|
||||
return GenerateTtsAudioErrorType.NotFound;
|
||||
case 429:
|
||||
return GenerateTtsAudioErrorType.TooManyRequests;
|
||||
case 500:
|
||||
return GenerateTtsAudioErrorType.ServerError;
|
||||
}
|
||||
}
|
||||
|
||||
// export function GenerateTtsAudioIsError(response: GenerateTtsAudioResponse): response is GenerateTtsAudioError {
|
||||
// return !('inference_job_token' in response);
|
||||
// }
|
||||
|
||||
export async function GenerateTtsAudio(request: GenerateTtsAudioRequest, sessionToken:string) : Promise<GenerateTtsAudioResponse>
|
||||
{
|
||||
return await fetch(inferTts, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'session': sessionToken,
|
||||
},
|
||||
// credentials: 'include',
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (!('inference_job_token' in res)) {
|
||||
return { error: GenerateTtsAudioErrorType.UnknownError };
|
||||
}
|
||||
const ret:GenerateTtsAudioResponse = {
|
||||
success: true,
|
||||
inference_job_token: res.inference_job_token,
|
||||
inference_job_token_type: res.inference_job_token_type,
|
||||
}
|
||||
return ret;
|
||||
})
|
||||
.catch(e => {
|
||||
let maybeError = maybeMapError(e);
|
||||
if (maybeError !== undefined) {
|
||||
return { error: maybeError };
|
||||
}
|
||||
return { error: GenerateTtsAudioErrorType.UnknownError };
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
export async function GetMediaFileByToken (fileToken: string, sessionToken: string) : Promise<GetMediaResponse>
|
||||
{
|
||||
return await fetch(getMediaFileByToken(fileToken), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'session': sessionToken,
|
||||
},
|
||||
})
|
||||
.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 };
|
||||
});
|
||||
;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetFilterOption, AssetType } from "~/pages/PageEnigma/models";
|
||||
import { useContext, useState, useEffect } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
characterFilter,
|
||||
@@ -12,17 +12,71 @@ import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
|
||||
import { shapeItems } from "~/pages/PageEnigma/store";
|
||||
import { GetMediaByUser, GetMediaListResponse } from "~/api/media_files/GetMediaByUser";
|
||||
import { AssetFilterOption, AssetType, MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { BucketConfig } from "~/api/BucketConfig";
|
||||
import { AuthenticationContext } from "~/contexts/Authentication";
|
||||
|
||||
interface Props {
|
||||
type: AssetType;
|
||||
}
|
||||
|
||||
export enum FetchStatus {
|
||||
paused,
|
||||
ready,
|
||||
in_progress,
|
||||
success,
|
||||
error
|
||||
}
|
||||
|
||||
export const ObjectsTab = ({ type }: Props) => {
|
||||
useSignals();
|
||||
const [objects,objectsSet] = useState<{ value: MediaItem[] }>({
|
||||
value:[{
|
||||
media_id: "",
|
||||
name: "",
|
||||
type: "",
|
||||
version: 1,
|
||||
}]
|
||||
});
|
||||
|
||||
const { authState } = useContext(AuthenticationContext);
|
||||
|
||||
const [status,statusSet] = useState(FetchStatus.ready);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === FetchStatus.ready && type !== AssetType.CHARACTER) {
|
||||
statusSet(FetchStatus.in_progress);
|
||||
GetMediaByUser(authState.userInfo.username,{},{
|
||||
filter_media_type: "glb"
|
||||
})
|
||||
.then((res: GetMediaListResponse) => {
|
||||
if (res.success && res.results) {
|
||||
statusSet(FetchStatus.success);
|
||||
objectsSet({
|
||||
value: res.results.map((item,i) => {
|
||||
let bucketConfig = new BucketConfig();
|
||||
let itemThumb = bucketConfig.getCdnUrl(item.cover_image.maybe_cover_image_public_bucket_path,600,100);
|
||||
return {
|
||||
media_id: item.token,
|
||||
name: item.maybe_title,
|
||||
type: item.media_type,
|
||||
version: 1,
|
||||
...item.cover_image.maybe_cover_image_public_bucket_path ? {
|
||||
thumbnail: itemThumb
|
||||
} : {}
|
||||
}
|
||||
})
|
||||
.filter((item,i) => (item.thumbnail))
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},[status,type]);
|
||||
|
||||
const assetFilter =
|
||||
type === AssetType.CHARACTER ? characterFilter : objectFilter;
|
||||
const items = type === AssetType.CHARACTER ? characterItems : objectItems;
|
||||
const items = type === AssetType.CHARACTER ? characterItems : objects;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -74,4 +128,4 @@ export const ObjectsTab = ({ type }: Props) => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
audioGroup,
|
||||
objectGroup,
|
||||
deleteCharacter,
|
||||
addShape,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import useUpdateKeyframe from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe";
|
||||
import { deleteObject } from "~/pages/PageEnigma/store/objectGroup/deleteObject";
|
||||
@@ -49,10 +48,10 @@ export const TrackProvider = ({ children }: Props) => {
|
||||
if (dragItem.value.type === AssetType.OBJECT) {
|
||||
addObject(dragItem.value);
|
||||
}
|
||||
if (dragItem.value.type === AssetType.SHAPE) {
|
||||
addShape(dragItem.value);
|
||||
}
|
||||
}
|
||||
// if (dragItem.value.type === AssetType.SHAPE) {
|
||||
// console.log("Dragged In Shape Type")
|
||||
// }
|
||||
|
||||
if (canDrop.value && dragItem.value) {
|
||||
if (dragItem.value.type === AssetType.ANIMATION) {
|
||||
|
||||
@@ -133,10 +133,10 @@ export class TransformClip {
|
||||
if (this.keyframes.length > 0) {
|
||||
let first_pos = this.keyframes[0].position;
|
||||
let first_rot = this.keyframes[0].rotation;
|
||||
let first_quat = new THREE.Quaternion().setFromEuler(new THREE.Euler(THREE.MathUtils.degToRad(first_rot.x), THREE.MathUtils.degToRad(first_rot.y), THREE.MathUtils.degToRad(first_rot.z)));
|
||||
let first_quat = new THREE.Euler(THREE.MathUtils.degToRad(first_rot.x), THREE.MathUtils.degToRad(first_rot.y), THREE.MathUtils.degToRad(first_rot.z));
|
||||
let first_scl = this.keyframes[0].scale;
|
||||
object.position.copy(first_pos);
|
||||
object.rotation.set(first_quat.x, first_quat.y, first_quat.z);
|
||||
object.rotation.copy(first_quat);
|
||||
object.scale.copy(first_scl);
|
||||
this.step_frame = 0;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ class Editor {
|
||||
last_scrub: number;
|
||||
record_stream: any | undefined;
|
||||
recorder: MediaRecorder | undefined;
|
||||
|
||||
selectedCanvas: boolean;
|
||||
// Default params.
|
||||
|
||||
constructor() {
|
||||
@@ -182,6 +184,7 @@ class Editor {
|
||||
this.last_scrub = 0;
|
||||
this.frames = 0;
|
||||
this.last_selected_sum = 0;
|
||||
this.selectedCanvas = false;
|
||||
// Audio Engine Test.
|
||||
|
||||
this.render_width = 1280;
|
||||
@@ -251,27 +254,34 @@ class Editor {
|
||||
// Find the container element
|
||||
const container = document.getElementById("video-scene-container");
|
||||
|
||||
if(container == null) { return; }
|
||||
if (container == null) { return; }
|
||||
|
||||
// Use the container's dimensions
|
||||
const width = container.offsetWidth;
|
||||
const height = container.offsetHeight;
|
||||
|
||||
// Sets up camera and base position.
|
||||
this.camera = new THREE.PerspectiveCamera(70, width / height, 0.15, 30);
|
||||
this.camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 200);
|
||||
this.camera.position.z = 3;
|
||||
this.camera.position.y = 3;
|
||||
this.camera.position.x = -3;
|
||||
|
||||
this.camera.layers.enable(0);
|
||||
this.camera.layers.enable(1); // This camera does not see this layer
|
||||
|
||||
|
||||
this.timeline.camera = this.camera;
|
||||
|
||||
this.render_camera = new THREE.PerspectiveCamera(
|
||||
70,
|
||||
width / height,
|
||||
0.15,
|
||||
30,
|
||||
0.01,
|
||||
200,
|
||||
);
|
||||
|
||||
|
||||
this.render_camera.layers.disable(1); // This camera does not see this layer );
|
||||
|
||||
// Base WebGL render and clock for delta time.
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
@@ -315,24 +325,27 @@ class Editor {
|
||||
this.renderer.domElement,
|
||||
);
|
||||
|
||||
this.orbitControls.mouseButtons = {
|
||||
MIDDLE: THREE.MOUSE.ROTATE,
|
||||
RIGHT: THREE.MOUSE.PAN,
|
||||
}; // Blender Style
|
||||
// this.orbitControls.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; // Standard
|
||||
//this.orbitControls.mouseButtons = {
|
||||
// MIDDLE: THREE.MOUSE.ROTATE,
|
||||
// RIGHT: THREE.MOUSE.PAN,
|
||||
//}; // Blender Style
|
||||
this.orbitControls.mouseButtons = {
|
||||
LEFT: THREE.MOUSE.ROTATE,
|
||||
MIDDLE: THREE.MOUSE.DOLLY,
|
||||
RIGHT: THREE.MOUSE.PAN }; // Standard
|
||||
|
||||
this.control = new TransformControls(this.camera, this.renderer.domElement);
|
||||
this.control.space = "local"; // Local transformation mode
|
||||
// .space = 'world'; // Global mode
|
||||
|
||||
this.control.setScaleSnap(0.05);
|
||||
this.control.setTranslationSnap(0.05);
|
||||
this.control.setRotationSnap(0.05);
|
||||
this.control.setScaleSnap(0.01);
|
||||
this.control.setTranslationSnap(0.01);
|
||||
this.control.setRotationSnap(0.01);
|
||||
console.log("Control Sensitivity:", this.control.sensitivity);
|
||||
|
||||
// OnClick and MouseMove events.
|
||||
window.addEventListener("mousemove", this.onMouseMove.bind(this), false);
|
||||
window.addEventListener("click", this.onMouseClick.bind(this), false);
|
||||
window.addEventListener("keydown", this.onkeydown.bind(this), false);
|
||||
// Base control and debug stuff remove debug in prod.
|
||||
if (this.control == undefined) {
|
||||
return;
|
||||
@@ -349,6 +362,10 @@ class Editor {
|
||||
});
|
||||
this.control.setSize(0.5); // Good default value for visuals.
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
// Configure raycaster to check both layers
|
||||
this.raycaster.layers.set(0); // Enable default layer
|
||||
this.raycaster.layers.enable(1); // Also check objects on the custom layer
|
||||
|
||||
this.mouse = new THREE.Vector2();
|
||||
this.activeScene.scene.add(this.control);
|
||||
// Resets canvas size.
|
||||
@@ -396,9 +413,11 @@ class Editor {
|
||||
this.orbitControls.enabled = true;
|
||||
this.cameraViewControls.enabled = false;
|
||||
}
|
||||
this.selectedCanvas = true;
|
||||
} else {
|
||||
this.orbitControls.enabled = false;
|
||||
this.cameraViewControls.enabled = false;
|
||||
this.selectedCanvas = false;
|
||||
}
|
||||
this.cameraViewControls?.reset();
|
||||
}
|
||||
@@ -421,7 +440,7 @@ class Editor {
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
public async testTestTimelineEvents() {}
|
||||
public async testTestTimelineEvents() { }
|
||||
|
||||
public async loadScene(scene_media_token: string) {
|
||||
this.dispatchAppUiState({
|
||||
@@ -573,6 +592,11 @@ class Editor {
|
||||
});
|
||||
this.updateSelectedUI();
|
||||
editorState.value = EditorStates.CAMERA_VIEW;
|
||||
if (this.activeScene.hot_items) {
|
||||
this.activeScene.hot_items.forEach((element) => {
|
||||
element.visible = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.camera.position.copy(this.last_cam_pos);
|
||||
this.camera.rotation.copy(this.last_cam_rot);
|
||||
@@ -664,9 +688,9 @@ class Editor {
|
||||
|
||||
this.saoPass = new SAOPass(this.activeScene.scene, this.camera);
|
||||
|
||||
this.saoPass.params.saoBias = 3.1;
|
||||
this.saoPass.params.saoBias = 4.1;
|
||||
this.saoPass.params.saoIntensity = 1.0;
|
||||
this.saoPass.params.saoScale = 6.0;
|
||||
this.saoPass.params.saoScale = 32.0;
|
||||
this.saoPass.params.saoKernelRadius = 5.0;
|
||||
this.saoPass.params.saoMinResolution = 0.0;
|
||||
|
||||
@@ -932,10 +956,10 @@ class Editor {
|
||||
audioSegment,
|
||||
"-filter_complex",
|
||||
"[1:a]adelay=" +
|
||||
startTime * 1000 +
|
||||
"|" +
|
||||
startTime * 1000 +
|
||||
"[a1];[0:a][a1]amix=inputs=2[a]",
|
||||
startTime * 1000 +
|
||||
"|" +
|
||||
startTime * 1000 +
|
||||
"[a1];[0:a][a1]amix=inputs=2[a]",
|
||||
"-map",
|
||||
"[a]",
|
||||
`${itteration}final_tmp.wav`,
|
||||
@@ -1339,6 +1363,19 @@ class Editor {
|
||||
}
|
||||
}
|
||||
|
||||
onkeydown(event: any) {
|
||||
if (event.key === 'f' && this.selected && this.orbitControls) {
|
||||
this.orbitControls.target.copy(this.selected.position);
|
||||
this.orbitControls.maxDistance = 4;
|
||||
this.orbitControls.update();
|
||||
this.orbitControls.maxDistance = 999;
|
||||
} else if (event.key === ' ') {
|
||||
if(this.rendering == false && this.switchPreviewToggle == false && this.selectedCanvas){
|
||||
this.startPlayback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sets new mouse location usually used in raycasts.
|
||||
onMouseMove(event: any) {
|
||||
const rect = this.canvReference.getBoundingClientRect();
|
||||
|
||||
@@ -75,6 +75,7 @@ class Scene {
|
||||
obj.receiveShadow = false;
|
||||
obj.castShadow = false;
|
||||
obj.userData['media_id'] = "Point::" + keyframe_uuid;
|
||||
obj.layers.set(1); // Enable default layer
|
||||
if (this.hot_items != undefined) {
|
||||
this.hot_items.push(obj);
|
||||
}
|
||||
@@ -105,6 +106,7 @@ class Scene {
|
||||
cam_obj.userData["name"] = "::CAM::";
|
||||
cam_obj.name = "::CAM::";
|
||||
cam_obj.position.set(0, 0.6, 1.5);
|
||||
cam_obj.layers.set(1);
|
||||
this.scene.add(cam_obj);
|
||||
});
|
||||
}
|
||||
@@ -155,6 +157,8 @@ class Scene {
|
||||
child.userData["metalness"] = 0.0;
|
||||
child.userData["shininess"] = 0.5;
|
||||
child.userData["specular"] = 0.5;
|
||||
child.layers.enable(0);
|
||||
child.layers.enable(1);
|
||||
if (auto_add) { this.scene.add(child); }
|
||||
resolve(child);
|
||||
});
|
||||
@@ -251,6 +255,7 @@ class Scene {
|
||||
const size = 25;
|
||||
const divisions = 50;
|
||||
this.gridHelper = new THREE.GridHelper(size, divisions, new THREE.Color("rgb(199,195,195)"), new THREE.Color("rgb(161,157,157)"));
|
||||
this.gridHelper.layers.set(1); // Enable default layer
|
||||
this.scene.add(this.gridHelper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +306,10 @@ export class TimeLine {
|
||||
),
|
||||
);
|
||||
|
||||
this.scene.createPoint(data_json["position"], data_json["keyframe_uuid"]);
|
||||
let point = this.scene.createPoint(data_json["position"], data_json["keyframe_uuid"]);
|
||||
if(this.editorEngine.camera_person_mode){
|
||||
point.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public deleteObject(object_uuid: string) {
|
||||
@@ -422,11 +425,13 @@ export class TimeLine {
|
||||
const media_id = data["data"]["media_id"];
|
||||
const offset = data["data"]["offset"];
|
||||
const length = data["data"]["length"] + offset;
|
||||
const clip_uuid = data['data']['clip_uuid'];
|
||||
|
||||
for (const element of this.timeline_items) {
|
||||
if (
|
||||
element.media_id === media_id &&
|
||||
element.object_uuid === object_uuid
|
||||
element.object_uuid === object_uuid &&
|
||||
element.clip_uuid == clip_uuid
|
||||
) {
|
||||
element.length = length;
|
||||
element.offset = offset;
|
||||
@@ -439,12 +444,14 @@ export class TimeLine {
|
||||
const object_uuid = data["data"]["object_uuid"];
|
||||
const media_id = data["data"]["media_id"];
|
||||
const type = data["type"];
|
||||
const clip_uuid = data['data']['clip_uuid'];
|
||||
|
||||
for (let i = 0; i < this.timeline_items.length; i++) {
|
||||
const element = this.timeline_items[i];
|
||||
if (
|
||||
element.media_id === media_id &&
|
||||
element.object_uuid === object_uuid
|
||||
element.object_uuid === object_uuid &&
|
||||
element.clip_uuid == clip_uuid
|
||||
) {
|
||||
this.timeline_items.splice(i, 1);
|
||||
break;
|
||||
|
||||
@@ -4,6 +4,8 @@ export interface MediaItem {
|
||||
media_id: string;
|
||||
object_uuid?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
publicBucketPath?: string;
|
||||
length?: number;
|
||||
thumbnail?: string;
|
||||
isMine?: boolean;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Types for Listing TTS Models
|
||||
*/
|
||||
|
||||
export interface TtsModelListResponsePayload {
|
||||
success: boolean,
|
||||
models: Array<TtsModelListItem>,
|
||||
}
|
||||
|
||||
export interface UserRatings {
|
||||
positive_count: number,
|
||||
negative_count: number,
|
||||
// Total count does not take into account "neutral" ratings.
|
||||
total_count: number,
|
||||
}
|
||||
|
||||
export interface TtsModelListItem {
|
||||
model_token: string,
|
||||
tts_model_type: string,
|
||||
creator_user_token: string,
|
||||
creator_username: string,
|
||||
creator_display_name: string,
|
||||
creator_gravatar_hash: string,
|
||||
ietf_language_tag: string,
|
||||
ietf_primary_language_subtag: string,
|
||||
updatable_slug: string,
|
||||
title: string,
|
||||
is_front_page_featured: boolean,
|
||||
is_twitch_featured: boolean,
|
||||
user_ratings: UserRatings,
|
||||
category_tokens: string[],
|
||||
created_at: string,
|
||||
updated_at: string,
|
||||
}
|
||||
|
||||
/*
|
||||
* Types for Making a TTS Generation Request
|
||||
*/
|
||||
export interface GenerateTtsAudioRequest {
|
||||
uuid_idempotency_token: string,
|
||||
tts_model_token: string,
|
||||
inference_text: string,
|
||||
// TODO(2022-03): TEMPORARY
|
||||
is_storyteller_demo?: boolean,
|
||||
}
|
||||
|
||||
export interface GenerateTtsAudioResponse {
|
||||
success?: boolean,
|
||||
inference_job_token?: string,
|
||||
// Which queue to poll
|
||||
inference_job_token_type?: string,
|
||||
error?: GenerateTtsAudioErrorType,
|
||||
}
|
||||
|
||||
export enum GenerateTtsAudioErrorType {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
TooManyRequests,
|
||||
ServerError,
|
||||
UnknownError,
|
||||
}
|
||||
|
||||
export interface EndpointSuccessResponse {
|
||||
success: boolean,
|
||||
inference_job_token: string,
|
||||
inference_job_token_type?: string,
|
||||
}
|
||||
|
||||
export interface EndpointErrorResponse {
|
||||
success: boolean,
|
||||
}
|
||||
|
||||
export type EndpointResponse = EndpointSuccessResponse | EndpointErrorResponse;
|
||||
|
||||
export interface StatusLike {
|
||||
status: number,
|
||||
}
|
||||
@@ -5,7 +5,6 @@ export * from "./dragAndDrop";
|
||||
export * from "./media";
|
||||
export * from "./objectGroup";
|
||||
export * from "./selectedItem";
|
||||
export * from "./shape";
|
||||
export * from "./sidePanel";
|
||||
export * from "./sizing";
|
||||
export * from "./timing";
|
||||
|
||||
@@ -455,6 +455,7 @@ export const audioItems = signal<MediaItem[]>([
|
||||
length: 25,
|
||||
name: "Talk (Demo Sounds)",
|
||||
thumbnail: "/resources/placeholders/audio_placeholder.png",
|
||||
publicBucketPath: "/media/j/a/r/r/3/jarr3asge6t0x048wdzcehzjd2nh4ep7/fakeyou_jarr3asge6t0x048wdzcehzjd2nh4ep7.wav",
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
@@ -463,6 +464,7 @@ export const audioItems = signal<MediaItem[]>([
|
||||
length: 25,
|
||||
name: "NCS Song",
|
||||
thumbnail: "/resources/placeholders/audio_placeholder.png",
|
||||
publicBucketPath: "/media/s/j/t/0/6/sjt06a8y2qrdqe574nry02bpd3bt01ma/upload_sjt06a8y2qrdqe574nry02bpd3bt01ma.wav",
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
|
||||
export function addShape(item: MediaItem) {
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.ADD_SHAPE,
|
||||
data: item,
|
||||
});
|
||||
}
|
||||
@@ -1,136 +1 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonLink,
|
||||
H1,H2,H3,H4,H5,H6,P,Label,
|
||||
ListDropdown,
|
||||
ListSearchDropdown,
|
||||
LoadingBar,
|
||||
LoadingDotsTyping,
|
||||
LoadingDotsBricks,
|
||||
} from "~/components";
|
||||
|
||||
|
||||
export default function ComponentLibrary () {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const loopRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const testdata = [
|
||||
{ name: 'Wade Cooper' },
|
||||
{ name: 'Arlene Mccoy' },
|
||||
{ name: 'Devon Webb' },
|
||||
{ name: 'Tom Cook' },
|
||||
{ name: 'Tanya Fox' },
|
||||
{ name: 'Hellen Schmidt' },
|
||||
]
|
||||
|
||||
const pushProgressBar: ()=>void = useCallback(()=>{
|
||||
loopRef.current = setInterval(function timer(){
|
||||
setProgress((curr)=>{
|
||||
if (curr < 100){
|
||||
return curr+10;
|
||||
}else{
|
||||
if(loopRef.current)
|
||||
clearInterval(loopRef.current);
|
||||
return curr;
|
||||
}
|
||||
})}
|
||||
, 3000);
|
||||
}, []);
|
||||
useEffect(()=>{
|
||||
pushProgressBar();
|
||||
return ()=>{
|
||||
if(loopRef.current)
|
||||
clearInterval(loopRef.current)
|
||||
}
|
||||
},[progress]);
|
||||
|
||||
return(
|
||||
<div
|
||||
className="fixed w-full overflow-scroll"
|
||||
style={{height: "calc(100% - 72px)"}}
|
||||
>
|
||||
<div
|
||||
className='bg-ui-panel w-10/12 max-w-7xl mx-auto my-6 rounded-lg p-6'
|
||||
>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<H1>Typography</H1>
|
||||
<H1>H1. This is an H1 Heading</H1>
|
||||
<H2>H2. This is an H2 Heading</H2>
|
||||
<H3>H3. This is an H3 Heading</H3>
|
||||
<H4>H4. This is an H4 Heading</H4>
|
||||
<Label>Label. Is essentially same as H4</Label>
|
||||
<P>P. Is an normal Paragraph</P>
|
||||
<H5>H5. This is an H5 Heading</H5>
|
||||
<H6>H6. This is an H6 Heading</H6>
|
||||
|
||||
<hr className="mt-2 mb-4"/>
|
||||
<H1>Components</H1>
|
||||
<H4>Buttons</H4>
|
||||
<div className="flex gap-2">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button className="bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary">
|
||||
Prelim Tertiary
|
||||
</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<H4>ButtonLink</H4>
|
||||
<ButtonLink to="/">Back to /</ButtonLink>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4 max-w-2xl">
|
||||
<H4>List Dropdowns</H4>
|
||||
<ListDropdown list={testdata}/>
|
||||
<ListSearchDropdown list={testdata}/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<H4>Loading Dots</H4>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-60 h-40 rounded-lg overflow-hidden">
|
||||
<LoadingDotsTyping />
|
||||
</div>
|
||||
<div className="w-60 h-40 rounded-lg overflow-hidden">
|
||||
<LoadingDotsBricks />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-4 max-w-2xl">
|
||||
<H4>Loading Bar</H4>
|
||||
<div className="flex gap-2 items-center">
|
||||
<LoadingBar
|
||||
progress={progress}
|
||||
isShowing={progress !== 100}
|
||||
wrapperClassName="rounded-lg"
|
||||
message="this takes progress from parent"
|
||||
/>
|
||||
<Button
|
||||
className="h-fit"
|
||||
onClick={()=>setProgress(0)}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<LoadingBar
|
||||
label="labeling the bar"
|
||||
message="displaying a message"
|
||||
/>
|
||||
<LoadingBar
|
||||
label="useFakeTimer = 30000 (30secs)"
|
||||
useFakeTimer={30000}
|
||||
/>
|
||||
<LoadingBar
|
||||
label="useFakeTimer = 3000 (3secs)"
|
||||
useFakeTimer={3000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { PageComponentLibrary as default } from "~/pages/PageComponentLibrary";
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { PageCube } from "../pages/PageCube";
|
||||
import { Button } from '~/components';
|
||||
|
||||
export default function Cube(){
|
||||
const pausedRiddle = true;
|
||||
const [riddle, setRiddle] = useState<string>("");
|
||||
const [submitted, setSubmitted] = useState<boolean>(false);
|
||||
|
||||
const handleInput = (event:React.ChangeEvent<HTMLInputElement>)=>{
|
||||
setRiddle(event.target.value);
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
const state = riddle.length > 5 && riddle[riddle.length-1] === "!";
|
||||
if(state)
|
||||
setSubmitted(state);
|
||||
else
|
||||
alert("What is the price for your blind eye?")
|
||||
}
|
||||
|
||||
if(submitted || pausedRiddle){
|
||||
return <PageCube />
|
||||
}else{
|
||||
return(
|
||||
<div className="w-full flex justify-center content-center">
|
||||
<form
|
||||
className="mt-20 flex flex-col content-center"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<label className='text-white'>
|
||||
Riddle:
|
||||
</label>
|
||||
<input type="text" value={riddle} onChange={handleInput} />
|
||||
<br/>
|
||||
<Button type="submit" className='text-white'>Submit</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { MetaFunction } from "@remix-run/deno";
|
||||
import { TopBarHelmet } from "~/modules/TopBarHelmet";
|
||||
|
||||
import sonic from "./_assets/sonic-the-hedgehog-classic-sonic.gif";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { useEffect } from "react";
|
||||
import { pageHeight, pageWidth } from "~/store";
|
||||
import { sidePanelWidth, timelineHeight } from "~/pages/PageEnigma/store";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "New Remix App" },
|
||||
{ name: "description", content: "Welcome to Remix!" },
|
||||
];
|
||||
};
|
||||
|
||||
export default function Sonic() {
|
||||
return (
|
||||
<div className="size-full">
|
||||
<TopBarHelmet>{null}</TopBarHelmet>
|
||||
<img
|
||||
alt="sonic"
|
||||
src={sonic}
|
||||
className="
|
||||
fixed
|
||||
left-0 top-0
|
||||
-z-10 h-screen
|
||||
w-screen
|
||||
object-cover
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,18 @@
|
||||
border: 3px solid gray;
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Fira Sans", sans-serif;
|
||||
color: #fff;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"terser": "^5.30.3",
|
||||
"three": "^0.162.0",
|
||||
"uuid": "^9.0.1",
|
||||
"wavesurfer.js": "^7.7.11",
|
||||
"webm-writer": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -13181,6 +13182,11 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/wavesurfer.js": {
|
||||
"version": "7.7.11",
|
||||
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.7.11.tgz",
|
||||
"integrity": "sha512-27otJlDBcUSbMh/YkxEZN0yXTHINQNYSOl5aJplYlIaHw2u3BrABUzOmUZV3dKIy8udOLJXS/67y5pOIzgAg+w=="
|
||||
},
|
||||
"node_modules/wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"terser": "^5.30.3",
|
||||
"three": "^0.162.0",
|
||||
"uuid": "^9.0.1",
|
||||
"wavesurfer.js": "^7.7.11",
|
||||
"webm-writer": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
|
After Width: | Height: | Size: 686 B |
|
After Width: | Height: | Size: 634 B |
|
After Width: | Height: | Size: 702 B |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |