Scene - Save Load - v2.0 (#303)

Saving and Loading of Scenes:
- loading scenes from URL (aka the scenetoken) is fixed
- URL changes according to the changing of loaded scene
- scene signal is created for storing the metadata of a scene
- editor can hydrate the scene signal data
- options in the file menu changes states according to scene status

Editor Changes and Fixes:
- fixes the way Editor Engine is constructed and initialized for
stability
- fixes typing issues on all that is passed to Editor Engine from
react-land

Other Fixes, Bells & Whistles:
- changed "back to dashboard" button to "back to editor", and the
correct behavior of it
- cleaned up routes and page files so routes designation are actually
mutually exclusive

Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
wilandwil authored and GitHub committed 2024-05-02 22:55:00 -04:00
1 parent ecb1540863
commit d5e3b9af97
19 files changed
+344 -137

No files matched your search

+4
View File
@@ -19,6 +19,7 @@ interface ButtonDropdownProps {
onClick?: () => void;
disabled?: boolean;
divider?: boolean;
onDialogOpen?: ()=>void;
dialogProps?: {
title: string;
content: React.ReactNode;
@@ -51,6 +52,9 @@ export const ButtonDropdown = ({
if (option.onClick) {
option.onClick();
}
if (option.onDialogOpen) {
option.onDialogOpen();
}
if (option.dialogProps) {
setSelectedOptionIndex(index);
setIsOpen(true);
+19 -7
View File
@@ -1,27 +1,39 @@
import { ReactNode, useEffect, useState } from "react";
import { ReactNode, useEffect, useContext, useState } from "react";
import { AuthenticationContext } from "~/contexts/Authentication";
import { AppUiContext } from "~/contexts/AppUiContext";
import { EngineContext } from "./EngineContext";
import { signalScene } from "~/store";
import Editor from "~/pages/PageEnigma/js/editor";
import { EngineContext } from "./EngineContext";
interface Props {
children: ReactNode;
}
export const EngineProvider = ({ children }: Props) => {
export const EngineProvider = ({
children,
}: Props) => {
const [editor, setEditor] = useState<Editor | null>(null);
const [, dispatchAppUiState] = useContext(AppUiContext);
const {authState} = useContext(AuthenticationContext);
useEffect(() => {
setEditor((curr) => {
if (curr !== null) {
console.warn("Editor Engine already exist");
return curr;
} else {
return new Editor();
} else if (authState.sessionToken && authState.userInfo ){
return new Editor({
dispatchAppUiState,
authState,
signalScene,
});
}
return curr;
});
// }
}, []);
}, [dispatchAppUiState, authState]);
return (
<EngineContext.Provider value={editor}>{children}</EngineContext.Provider>
+31 -25
View File
@@ -1,10 +1,10 @@
import { useState } from "react";
import { useLocation } from "@remix-run/react";
import { useState } from 'react';
import { useLocation, useParams } from "@remix-run/react";
import { useSignals } from "@preact/signals-react/runtime";
import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons";
import { ButtonLink, Input } from "~/components";
import { AuthButtons } from "./AuthButtons";
import { sceneTitle } from "~/store";
import { scene } from "~/store";
import {
showErrorDialog,
errorDialogMessage,
@@ -12,27 +12,35 @@ import {
} from "~/pages/PageEnigma/store";
import { MyMoviesButton } from "~/modules/TopBar/MyMoviesButton";
import { getCurrentLocationWithoutParams } from '~/utilities';
function isEditorPath(path:string){
if ( path === "/" ) return true;
if ( path === "/idealenigma/" ) return true;
return false;
}
interface Props {
pageName: string;
}
export const TopBar = ({ pageName }: Props) => {
useSignals();
const currentLocation = useLocation().pathname;
const currentLocation = getCurrentLocationWithoutParams(useLocation().pathname, useParams());
const [isValid, setIsValid] = useState<boolean>(true);
const handleShowErrorDialog = () => {
errorDialogTitle.value = "Error";
errorDialogMessage.value = "Scene name can not be empty.";
showErrorDialog.value = true;
};
const handleChangeSceneTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
sceneTitle.value = e.target.value;
if (sceneTitle.value !== "") {
const handleChangeSceneTitle = (e: React.ChangeEvent<HTMLInputElement>)=>{
scene.value.title = e.target.value;
if (scene.value.title !== "") {
setIsValid(true);
}
};
const validateSceneTitle = (e: React.FocusEvent<HTMLInputElement>) => {
if (sceneTitle.value === "") {
}
const validateSceneTitle = (e: React.FocusEvent<HTMLInputElement>)=>{
if (scene.value.title === "") {
setIsValid(false);
handleShowErrorDialog();
e.target.focus();
@@ -52,26 +60,24 @@ export const TopBar = ({ pageName }: Props) => {
alt="Logo FakeYou StoryTeller.ai"
/>
</a>
{currentLocation !== "/" && (
{!isEditorPath(currentLocation) && (
<ButtonLink to={"/"} variant="secondary" icon={faChevronLeft}>
Back to Dashboard
Back to Editor
</ButtonLink>
)}
</div>
<div className="flex items-center justify-center gap-2 font-medium">
<div className="flex items-center justify-center gap-2">
<span className="opacity-60">{pageName}</span>
<span className="opacity-60">/</span>
<Input
className="-ml-2"
inputClassName="bg-ui-panel hover:bg-white/5 border-2 border-transparent hover:border-brand-secondary hover:ml-1.5 focus:bg-brand-secondary focus:ml-1.5 h-9 p-2.5"
isError={!isValid}
value={sceneTitle.value}
onChange={handleChangeSceneTitle}
onBlur={validateSceneTitle}
/>
</div>
<div className="flex items-center justify-center font-medium gap-2">
<span className="opacity-60">{pageName}</span>
<span className="opacity-60">/</span>
<Input
className="-ml-2 w-96"
inputClassName="bg-ui-panel focus:bg-brand-secondary focus:ml-3 text-ellipsis"
isError={!isValid}
value={scene.value.title || ""}
onChange={handleChangeSceneTitle}
onBlur={validateSceneTitle}
/>
</div>
<div className="flex justify-end gap-2">
@@ -1,13 +1,12 @@
import { json } from "@remix-run/router";
import { withProtectionRoute } from "~/modules/withProtectedRoute";
import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons";
import { P, H1, ButtonLink } from "~/components";
import { json } from "@remix-run/router";
export const loader = () => {
return json(null, { status: 404 });
};
const Page404 = withProtectionRoute(()=>{
export const Page404 = () => {
return(
<div
className="fixed w-full overflow-scroll"
@@ -22,6 +21,6 @@ const Page404 = withProtectionRoute(()=>{
<ButtonLink to="/" icon={faChevronLeft}>Back to Dashboard</ButtonLink>
</div>
</div>
)
});
);
};
export default Page404;
+6 -2
View File
@@ -7,12 +7,16 @@ import { LoadingBar } from "~/components";
import { GenerateModals } from "~/pages/PageEnigma/comps/GenerateModals/GenerateModals";
import { ErrorDialog } from "~/components/ErrorDialog";
export const PageEnigma = () => {
export const PageEnigma = ({
sceneToken
}:{
sceneToken?:string
}) => {
useInferenceJobManager();
return (
<TrackProvider>
<EngineProvider>
<PageEnigmaComponent />
<PageEnigmaComponent sceneToken={sceneToken}/>
<DragComponent />
<GenerateModals />
<ErrorDialog />
+11 -14
View File
@@ -1,27 +1,24 @@
import { useContext, useEffect } from "react";
import { AuthenticationContext } from "~/contexts/Authentication";
import { AppUiContext } from "~/contexts/AppUiContext";
import { EngineContext } from "~/contexts/EngineContext";
import { PageEditor } from "~/pages/PageEnigma/PageEditor";
import { useParams } from "@remix-run/react";
import { signalScene } from "~/store";
export const PageEnigmaComponent = () => {
const [, dispatchAppUiState] = useContext(AppUiContext);
export const PageEnigmaComponent = ({
sceneToken
}: {
sceneToken?: string;
}) => {
const editor = useContext(EngineContext);
const params = useParams();
useEffect(() => {
if (editor && editor.can_initialize && dispatchAppUiState !== null) {
const sceneToken = params["sceneToken"];
editor.initialize(
{
dispatchAppUiState,
},
sceneToken,
if (editor && editor.can_initialize) {
editor.initialize({
sceneToken: sceneToken || ""}
);
}
}, [editor, dispatchAppUiState, params]);
}, [editor, sceneToken]);
return <PageEditor />;
};
@@ -1,48 +1,86 @@
import { useContext, useState } from "react";
import { sceneTitle } from "~/store";
import { useParams, useLocation, useNavigate } from "@remix-run/react";
import { useSignalEffect } from "@preact/signals-react/runtime";
import { scene } from "~/store";
import { EngineContext } from "~/contexts/EngineContext";
import { AuthenticationContext } from "~/contexts/Authentication";
import { ToasterContext, ToastTypes } from "~/contexts/ToasterContext";
import { faFile } from "@fortawesome/pro-solid-svg-icons";
import { ButtonDropdown, Input } from "~/components";
import { ButtonDialogue } from "~/modules/ButtonDialogue";
import { EngineContext } from "~/contexts/EngineContext";
import { ToasterContext, ToastTypes } from "~/contexts/ToasterContext";
// import { APPUI_ACTION_TYPES } from "app/reducers";
import { TestFeaturesButtons } from "./TestFeaturesButtons";
import { Help } from "./Help";
import { faFile } from "@fortawesome/pro-solid-svg-icons";
import { LoadScene } from "./LoadScene";
import { NewSceneFromTemplate } from "./NewSceneFromTemplate";
export const ControlsTopButtons = () => {
const editorEngine = useContext(EngineContext);
import { getCurrentLocationWithoutParams } from "~/utilities";
const [sceneName, setSceneName] = useState<string>("");
export const ControlsTopButtons = () => {
const params = useParams();
const location = useLocation();
const navigate = useNavigate();
const editorEngine = useContext(EngineContext);
const {authState} = useContext(AuthenticationContext);
const [sceneTitleInput, setSceneTitleInput] = useState<string>("");
const [sceneToken, setSceneToken] = useState<string>("");
const { addToast } = useContext(ToasterContext);
const handleChangeSceneTitleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setSceneTitleInput(e.target.value);
}
const clearSceneTitleInput = ()=>{
setSceneTitleInput("");
}
const handleButtonSave = async () => {
// console.log(`SceneName is ${sceneName}`);
// const sceneMediaToken = await editorEngine?.saveScene(sceneName);
console.log(`SceneName is ${sceneTitle.value}`);
const sceneMediaToken = await editorEngine?.saveScene(sceneTitle.value);
console.log(`SceneName is ${scene.value.title}`);
const sceneMediaToken = await editorEngine?.saveScene(scene.value.title || "");
if (sceneMediaToken) {
addToast(ToastTypes.SUCCESS, sceneMediaToken);
}
};
const handleButtonNewFromTemplate = () => {
editorEngine?.loadScene(sceneToken)
.catch((err) => {
addToast(ToastTypes.ERROR, err.message);
});
};
const handleButtonLoadScene = () => {
console.log(`Scene Token is ${sceneToken}`);
editorEngine?.loadScene(sceneToken).catch((err) => {
addToast(ToastTypes.ERROR, err.message);
});
editorEngine?.loadScene(sceneToken)
.catch((err) => {
addToast(ToastTypes.ERROR, err.message);
});
};
const handleSceneSelection = (token: string) => {
setSceneToken(token);
console.log(`Selected Scene Token is ${token}`);
};
const handleChangeSceneTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
sceneTitle.value = e.target.value;
};
useSignalEffect(()=>{
console.log("useSignalEffect in File Buttons");
console.log(scene.value);
if (!scene.value.isInitializing){
setSceneTitleInput(scene.value.title || "");
if(scene.value.token === undefined){
console.log("delete param from url if it exists");
const currentLocation = getCurrentLocationWithoutParams(location.pathname, params);
console.log(`should navigate to ${currentLocation}`)
navigate(currentLocation);
}
if(scene.value.token && scene.value.token !== params.sceneToken){
console.log("nav to the next param on the url");
const currentLocation = getCurrentLocationWithoutParams(location.pathname, params);
navigate(currentLocation+scene.value.token);
}
}
});
return (
<div className="flex flex-col gap-2 pl-3 pt-3">
@@ -54,27 +92,31 @@ export const ControlsTopButtons = () => {
{
label: "New scene",
description: "Ctrl+N",
onDialogOpen: ()=>{
setSceneTitleInput("Untitled New Scene");
},
dialogProps: {
title: "Create a New Scene",
content: (
<Input
value={sceneTitleInput}
label="Please enter a name for your new scene"
onChange={(e) => {
setSceneName(e.target.value);
}}
onChange={handleChangeSceneTitleInput}
autoComplete="false"
/>
),
confirmButtonProps: {
label: "Create",
disabled: sceneName === "",
onClick: () => editorEngine?.newScene(),
disabled: sceneTitleInput === "",
onClick: () => editorEngine?.newScene(
sceneTitleInput
),
},
closeButtonProps: {
label: "Cancel",
},
showClose: true,
onClose: () => setSceneName(""),
onClose: clearSceneTitleInput,
},
},
{
@@ -88,7 +130,7 @@ export const ControlsTopButtons = () => {
confirmButtonProps: {
label: "Create",
disabled: sceneToken === "",
onClick: handleButtonLoadScene,
onClick: handleButtonNewFromTemplate,
},
closeButtonProps: {
label: "Cancel",
@@ -116,24 +158,22 @@ export const ControlsTopButtons = () => {
},
},
{
disabled: true, // save scene should be disabled if there are no changes
disabled: !scene.value.isModified || (scene.value.ownerToken !== authState.userInfo?.user_token),
// save scene should be disabled if there are no changes
label: "Save scene",
description: "Ctrl+S",
dialogProps: {
title: "Save Scene",
content: (
<Input
value={sceneTitle.value}
value={sceneTitleInput}
label="Please enter a name for your scene"
// onChange={(e) => {
// setSceneName(e.target.value);
// }}
onChange={handleChangeSceneTitle}
onChange={handleChangeSceneTitleInput}
/>
),
confirmButtonProps: {
label: "Save",
disabled: sceneName === "",
disabled: sceneTitleInput === "",
onClick: handleButtonSave,
},
closeButtonProps: {
@@ -144,23 +184,24 @@ export const ControlsTopButtons = () => {
divider: true,
},
{
disabled: !scene.value.isModified || !scene.value.token,
label: "Save scene as copy",
description: "Ctrl+Shift+S",
onDialogOpen: ()=>{
setSceneTitleInput("Copy of " + sceneTitleInput);
},
dialogProps: {
title: "Save Scene as Copy",
content: (
<Input
value={sceneTitle.value}
value={sceneTitleInput}
label="Please enter a name for your scene"
// onChange={(e) => {
// setSceneName(e.target.value);
// }}
onChange={handleChangeSceneTitle}
onChange={handleChangeSceneTitleInput}
/>
),
confirmButtonProps: {
label: "Save",
disabled: sceneName === "",
disabled: sceneTitleInput === "",
onClick: handleButtonSave,
},
closeButtonProps: {
@@ -172,25 +213,6 @@ export const ControlsTopButtons = () => {
]}
/>
{/* <ButtonDialogue
buttonProps={{
variant: "secondary",
label: "Add Scene Object (Test)",
}}
confirmButtonProps={{
label: "Save",
disabled: mediaToken === "",
onClick: handleMediaToken,
}}
title="Add Scene Object via Media Token">
<Input
label="Please Enter a Media Token"
onChange={(e) => {
setMediaToken(e.target.value);
}}
/>
</ButtonDialogue> */}
<ButtonDialogue
buttonProps={{
variant: "secondary",
@@ -19,7 +19,7 @@ export const DragComponent = () => {
const thumbnail = dragItem.value.thumbnail
? dragItem.value.thumbnail
: `/resources/images/default-covers/${dragItem.value.imageIndex}.webp`;
: `/resources/images/default-covers/${dragItem.value.imageIndex ||0}.webp`;
if (overTimeline.value) {
if (canDrop.value && DndAsset.overElement) {
@@ -11,7 +11,7 @@ export const ItemElement = ({ item }: Props) => {
useSignals();
const thumbnail = item.thumbnail
? item.thumbnail
: `/resources/images/default-covers/${item.imageIndex}.webp`;
: `/resources/images/default-covers/${item.imageIndex || 0}.webp`;
return (
<div
+13 -1
View File
@@ -109,6 +109,7 @@ export class APIManager {
public async loadSceneState(
scene_media_file_token: string | null,
signalScene: ((data:any)=>void )| null,
): Promise<any> {
const api_base_url = environmentVariables.value.BASE_API;
const url = `${api_base_url}/v1/media_files/file/${scene_media_file_token}`;
@@ -118,6 +119,17 @@ export class APIManager {
}
const json = await JSON.parse(await response.text());
if(json && json.media_file && signalScene !== null){
if(json.media_file.maybe_title === null){
console.warn(`Scene /w Token: ${scene_media_file_token} has no title`);
}
signalScene({
title: json.media_file.maybe_title || "Untitled Scene",
token: scene_media_file_token,
ownerToken: json.media_file.maybe_creator_user.user_token,
isModified: false,
});
}
const bucket_path = json["media_file"]["public_bucket_path"];
const media_base_url = environmentVariables.value.GOOGLE_API;
const media_url = `${media_base_url}/vocodes-public${bucket_path}`; // gets you a bucket path
@@ -136,7 +148,7 @@ export class APIManager {
reader.readAsText(blob);
});
console.log(`loadSceneState: ${JSON.stringify(json_result)}`);
// console.log(`loadSceneState: ${JSON.stringify(json_result)}`);
return json_result;
}
+44 -17
View File
@@ -24,7 +24,7 @@ import { LipSyncEngine } from "./lip_sync_engine.js";
import { AnimationEngine } from "./animation_engine.js";
import { PointerLockControls } from "three/addons/controls/PointerLockControls.js";
import { APPUI_ACTION_TYPES } from "../../../reducers";
import { APPUI_ACTION_TYPES, AppUiState, AppUiAction } from "../../../reducers";
import { ClipGroup, ClipType } from "~/pages/PageEnigma/models/track";
import { XYZ } from "../datastructures/common";
@@ -40,8 +40,20 @@ import {
EditorStates,
previewSrc,
} from "~/pages/PageEnigma/store/engine";
import { AuthState } from "~/contexts/Authentication/types";
// Main editor class that will call everything else all you need to call is " initialize() ".
export type EditorConstructorConfig = {
dispatchAppUiState: React.Dispatch<AppUiAction>;
signalScene: (data:any)=>void;
authState: AuthState;
};
export type EditorInitializeConfig = {
sceneToken: string;
};
class Editor {
version: number;
activeScene: Scene;
@@ -105,7 +117,9 @@ class Editor {
can_initialize: boolean;
switchPreviewToggle: boolean;
dispatchAppUiState: any; // todo figure out the type
dispatchAppUiState: React.Dispatch<AppUiAction>;
authState: AuthState;
signalScene: ((data:any)=>void);
render_width: number;
render_height: number;
@@ -126,7 +140,11 @@ class Editor {
// global names of scene entities
camera_name: string;
constructor() {
constructor({
dispatchAppUiState,
signalScene,
authState,
}:EditorConstructorConfig) {
console.log(
"If you see this message twice! then it rendered twice, if you see it once it's all good.",
);
@@ -231,8 +249,10 @@ class Editor {
this.current_frame = 0;
// Dispatcher
this.dispatchAppUiState = null;
//setup reactland Callbacks
this.dispatchAppUiState = dispatchAppUiState;
this.signalScene = signalScene;
this.authState = authState;
// Scene State
this.current_scene_media_token = null;
@@ -251,15 +271,9 @@ class Editor {
);
}
initialize(config: any, sceneToken: any) {
//setup reactland Callbacks
this.dispatchAppUiState = config.dispatchAppUiState;
// this is called by the parent for some reason
// this.dispatchAppUiState({
// type: APPUI_ACTION_TYPES.SHOW_EDITOR_LOADER
// });
initialize({
sceneToken
}:EditorInitializeConfig) {
if (this.can_initialize == false) {
console.log("Editor Already Initialized");
return;
@@ -426,6 +440,13 @@ class Editor {
if (this.isEmpty(sceneToken) == false) {
this.loadScene(sceneToken);
}else{
this.signalScene({
title: "Untitled New Scene",
token: undefined,
ownerToken: this.authState.userInfo?.user_token,
isModified: false,
});
}
document.addEventListener("mouseover", (event) => {
@@ -455,7 +476,7 @@ class Editor {
loadingBarIsShowing.value = false;
}
public async newScene() {
public async newScene(sceneTitleInput:string) {
this.activeScene.clear();
this.audio_engine = new AudioEngine();
this.emotion_engine = new EmotionEngine(this.version);
@@ -476,6 +497,13 @@ class Editor {
this.camera_name,
);
this.cam_obj = this.activeScene.get_object_by_name(this.camera_name);
const sceneTitle = sceneTitleInput && sceneTitleInput!=="" ? sceneTitleInput : "Untitled New Scene";
this.signalScene({
title: sceneTitle,
token: undefined,
ownerToken: this.authState.userInfo?.user_token,
isModified: false
})
}
// Token comes in from the front end to load the scene from the site.
@@ -496,14 +524,13 @@ class Editor {
this.current_scene_media_token = scene_media_token;
const scene_json = await this.api_manager
.loadSceneState(this.current_scene_media_token)
.loadSceneState(this.current_scene_media_token, this.signalScene)
.catch((err) => {
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.HIDE_EDITOR_LOADER,
});
throw err;
});
const proxyScene = new StoryTellerProxyScene(
this.version,
this.activeScene,
+47
View File
@@ -0,0 +1,47 @@
import type { HeadersFunction } from "@remix-run/deno";
import { useParams } from "@remix-run/react";
import { withProtectionRoute } from "~/modules/withProtectedRoute";
import { PageEnigma } from "~/pages/PageEnigma";
// import { Page404 } from "~/pages/Page404";
// NB(bt): Netlify's custom directives cannot set headers for pages served by Remix,
// so we must specify them here instead. Netlify header rules (in either _headers or
// netlify.toml) will apply to statically served files (images, css, js), however.
// Setting these headers in `vite.config.ts` will also fail to apply them in
// production.
export const headers: HeadersFunction = ({
actionHeaders,
errorHeaders,
loaderHeaders,
parentHeaders,
}) => ({
// NB(bt): The following two headers are required for enabling SharedArrayBuffer (required by
// ffmpeg.wasm) in the post-SPECTRE world. Without these headers being sent by the page, the
// page cannot use SharedArrayBuffer. Downstream services must also set relevant CORS headers.
//
// See the following documentation on how this complex system works:
//
// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
// - https://stackoverflow.com/questions/73275184/enable-shared-array-buffer-in-cross-domain
// - https://blog.logrocket.com/understanding-sharedarraybuffer-and-cross-origin-isolation/
// - https://github.com/ffmpegwasm/react-app/issues/3
//
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
});
const Index = withProtectionRoute(()=>{
const params = useParams();
return ( <PageEnigma sceneToken={params.sceneToken} />);
//NOTE: wil: CODE SAMPLE FOR ROUTING 404
// PLEAE DO NOT DELETE THE BELOW
// if (params.sceneToken && params.sceneToken.slice(0,2) === "m_" && params.sceneToken.length === 32){
// return ( <PageEnigma sceneToken={params.sceneToken} />);
// }else{
// return (<Page404 />);
// }
});
export default Index;
+6 -1
View File
@@ -1,6 +1,7 @@
import type { HeadersFunction } from "@remix-run/deno";
import { withProtectionRoute } from "~/modules/withProtectedRoute";
import { PageEnigma } from "~/pages/PageEnigma";
import { useParams } from "@remix-run/react";
// NB(bt): Netlify's custom directives cannot set headers for pages served by Remix,
// so we must specify them here instead. Netlify header rules (in either _headers or
@@ -28,5 +29,9 @@ export const headers: HeadersFunction = ({
"Cross-Origin-Opener-Policy": "same-origin",
});
const IdealEnigma = withProtectionRoute(()=><PageEnigma />);
const IdealEnigma = withProtectionRoute(()=>{
const params= useParams();
return <PageEnigma sceneToken={params.sceneToken}/>
});
export default IdealEnigma;
+31
View File
@@ -0,0 +1,31 @@
import type { HeadersFunction } from "@remix-run/deno";
import { withProtectionRoute } from "~/modules/withProtectedRoute";
import { PageEnigma } from "~/pages/PageEnigma";
// NB(bt): Netlify's custom directives cannot set headers for pages served by Remix,
// so we must specify them here instead. Netlify header rules (in either _headers or
// netlify.toml) will apply to statically served files (images, css, js), however.
// Setting these headers in `vite.config.ts` will also fail to apply them in
// production.
export const headers: HeadersFunction = ({
actionHeaders,
errorHeaders,
loaderHeaders,
parentHeaders,
}) => ({
// NB(bt): The following two headers are required for enabling SharedArrayBuffer (required by
// ffmpeg.wasm) in the post-SPECTRE world. Without these headers being sent by the page, the
// page cannot use SharedArrayBuffer. Downstream services must also set relevant CORS headers.
//
// See the following documentation on how this complex system works:
//
// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
// - https://stackoverflow.com/questions/73275184/enable-shared-array-buffer-in-cross-domain
// - https://blog.logrocket.com/understanding-sharedarraybuffer-and-cross-origin-isolation/
// - https://github.com/ffmpegwasm/react-app/issues/3
//
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
});
const IdealEnigma = withProtectionRoute(()=><PageEnigma />);
export default IdealEnigma;
@@ -29,4 +29,5 @@ export const headers: HeadersFunction = ({
});
const Index = withProtectionRoute(()=><PageEnigma />);
export default Index;
-2
View File
@@ -1,7 +1,5 @@
import { signal } from "@preact/signals-core";
export const sceneTitle = signal("Home Office");
export const pageHeight = signal(0);
export const pageWidth = signal(0);
+1
View File
@@ -1 +1,2 @@
export * from "./global";
export * from "./scene";
+27
View File
@@ -0,0 +1,27 @@
import { signal } from "@preact/signals-core";
export interface SceneSignal {
isInitializing: boolean;
title: string | undefined;
token: string | undefined;
ownerToken: string | undefined;
isModified: boolean | undefined;
}
export const scene = signal<SceneSignal>({
isInitializing: true,
title: undefined,
token: undefined,
ownerToken: undefined,
isModified: undefined,
});
export const signalScene = (data:SceneSignal)=>{
scene.value = {
...data,
isInitializing: false,
isModified: true,
//TODO: MILES: implement flagging of isModified
// from editor side, and take this out after
};
}
+15 -1
View File
@@ -1,4 +1,18 @@
import { Params} from "@remix-run/react";
export const kebabCase = (str:string) => str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replace(/[\s_]+/g, '-')
.toLowerCase();
.toLowerCase();
export const getCurrentLocationWithoutParams = (
path:string,
params:Readonly<Params<string>>
)=>{
let result = path;
Object.keys(params).forEach((key)=>{
result = result.replace(("/"+params[key]) || "", "")
});
if (result[result.length -1] !== "/") result = result + "/";
return result;
}