ui-pagescene: migrate ControlsTopButtons into lib (file menu + outliner toggle)

ControlsTopButtons + Help + LoadUserScenes + NewSceneFromTemplate +
ScenePicker + SceneCard now live in
@storyteller/ui-pagescene/comps/ControlsTopButtons/. They access host
plumbing through the adapter (showToast, listUserMediaFiles,
listFeaturedMediaFiles, getCdnUrl, onSceneTitleChange) and read scene
metadata reactively from the new sceneMeta + currentUserToken store
fields.

Lib additions:
- PageSceneStore.sceneMeta + currentUserToken with setSceneMeta /
  setCurrentUserToken actions; SceneMeta interface exported
- comps/ControlsTopButtons/{ControlsTopButtons,Help,LoadUserScenes,
  NewSceneFromTemplate,ScenePicker}/

Apps changes:
- ~/signals/scene.ts: signalScene now mirrors into PageSceneStore so
  the lib has reactive read access without depending on signals
- pages/PageScene/PageScene.tsx: lifted the URL ↔ scene-token sync
  useSignalEffect out of ControlsTopButtons (router-aware bookkeeping
  belongs at the apps shell), plus useSignalEffect mirroring
  authentication.userInfo into store.currentUserToken
- pages/PageScene/PageEditor.tsx: ControlsTopButtons import now
  resolves to @storyteller/ui-pagescene
- Deleted: pages/PageScene/comps/ControlsTopButtons/

Typecheck: 51 → 47 (deleted ControlsTopButtons had several pre-
existing typecheck issues that disappear in the rewrite). Lib build
clean.
This commit is contained in:
Tanish Manku committed 2026-05-08 18:08:46 -07:00
1 parent 6067849ef5
commit 8556ef0a6b
21 files changed
+241 -220

No files matched your search

@@ -1,12 +1,12 @@
import React, { useContext, useEffect, useRef } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { Controls3D } from "./comps/Controls3D";
import { ControlsTopButtons } from "./comps/ControlsTopButtons";
import { pageHeight, pageWidth } from "~/signals";
import {
CameraAspectRatio,
ControlPanelSceneObject,
ControlsTopButtons,
EditorCanvas,
EngineContext,
FocalLengthDisplay,
@@ -7,6 +7,8 @@
// scene state when the user briefly visits another tab.
import { useEffect } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useSignalEffect } from "@preact/signals-react/runtime";
import {
DragComponent,
EditorLoadingBar,
@@ -15,10 +17,19 @@ import {
usePageSceneStore,
} from "@storyteller/ui-pagescene";
import { useTabStore } from "~/pages/Stores/TabState";
import { authentication, scene } from "~/signals";
import { getCurrentLocationWithoutParams } from "~/utilities";
import { PageEditor } from "~/pages/PageScene/PageEditor";
import { useTauriPageSceneAdapter } from "./useTauriPageSceneAdapter";
export const PageScene = ({ sceneToken }: { sceneToken?: string }) => {
// Route-aware bookkeeping — lifted out of ControlsTopButtons so the
// lib stays router-agnostic. Keeps the URL in sync with the
// currently loaded scene's token so a refresh / share preserves
// the scene selection.
const params = useParams();
const location = useLocation();
const navigate = useNavigate();
// Tab-cache plumbing. The provider only knows about its own React
// lifecycle; the host decides where the in-memory cache string
// lives. Reading the current value once on render is fine — the
@@ -46,6 +57,37 @@ export const PageScene = ({ sceneToken }: { sceneToken?: string }) => {
};
}, []);
// Mirror the host's authentication signal into the lib store so
// lib-resident UI (ControlsTopButtons) can do ownership permission
// checks without depending on the host's signal system.
useSignalEffect(() => {
usePageSceneStore
.getState()
.setCurrentUserToken(authentication.userInfo.value?.user_token);
});
// URL ↔ loaded scene sync. Mirrors the previous useSignalEffect
// that lived inside ControlsTopButtons; lives here so the lib
// doesn't need react-router-dom.
useSignalEffect(() => {
if (scene.value.isInitializing) return;
const currentLocation = getCurrentLocationWithoutParams(
location.pathname,
params,
);
if (scene.value.token === undefined) {
if (params.sceneToken) {
history.pushState({}, "", currentLocation);
}
navigate(currentLocation, { replace: true });
} else if (scene.value.token) {
if (params.sceneToken && scene.value.token !== params.sceneToken) {
history.pushState({}, "", currentLocation + scene.value.token);
}
navigate(currentLocation + scene.value.token, { replace: true });
}
});
return (
<EngineProvider
sceneToken={sceneToken}
@@ -1,8 +0,0 @@
export {
Help,
Key,
KeyGroup,
Mouse,
Plus,
ShortcutsGroup,
} from "@storyteller/ui-pagescene";
@@ -1 +0,0 @@
export { Help } from "./Help";
@@ -1,2 +0,0 @@
export { ScenePicker } from "./ScenePicker";
export type { SceneTypes } from "./ScenePicker";
@@ -1,2 +0,0 @@
export { ControlsTopButtons } from './ControlsTopButtons';
@@ -1,4 +1,5 @@
import { signal } from "@preact/signals-core";
import { usePageSceneStore } from "@storyteller/ui-pagescene";
export interface SceneSignal {
isInitializing?: boolean;
@@ -16,13 +17,24 @@ export const scene = signal<SceneSignal>({
});
export const signalScene = (data: SceneSignal) => {
scene.value = {
const next = {
...data,
isInitializing: false,
isModified: true,
//TODO: MILES: implement flagging of isModified
// from editor side, and take this out after
};
scene.value = next;
// Mirror into the lib's PageSceneStore so ControlsTopButtons (and
// any other lib component) can read scene metadata reactively
// without depending on the host's signal system.
usePageSceneStore.getState().setSceneMeta({
title: next.title,
token: next.token,
ownerToken: next.ownerToken,
isModified: next.isModified,
isInitializing: next.isInitializing,
});
};
export const getSceneSignals = (): SceneSignal => {
+1 -1
View File
@@ -1 +1 @@
{"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/typescript/lib/lib.esnext.iterator.d.ts","../../node_modules/typescript/lib/lib.esnext.float16.d.ts","../../node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/@remix-run/node/dist/globals.d.ts","../../node_modules/@types/cookie/index.d.ts","../../node_modules/@remix-run/server-runtime/dist/crypto.d.ts","../../node_modules/@remix-run/server-runtime/dist/cookies.d.ts","../../node_modules/@remix-run/server-runtime/dLine truncated
{"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/typescript/lib/lib.esnext.iterator.d.ts","../../node_modules/typescript/lib/lib.esnext.float16.d.ts","../../node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/@remix-run/node/dist/globals.d.ts","../../node_modules/@types/cookie/index.d.ts","../../node_modules/@remix-run/server-runtime/dist/crypto.d.ts","../../node_modules/@remix-run/server-runtime/dist/cookies.d.ts","../../node_modules/@remix-run/server-runtime/dLine truncated
@@ -15,6 +15,7 @@ export { usePageSceneStore } from "./lib/PageSceneStore";
export type {
Camera,
FocalLengthDragging,
SceneMeta,
SceneObject,
SceneObjectKind,
OutlinerItem,
@@ -183,6 +184,7 @@ export { getSceneGenerationMetaData } from "./lib/sceneMetadata";
// are either surfaced via adapter slots or replaced with
// lib-internal state.
export { AssetMenu, AssetModal } from "./lib/comps/AssetMenu";
export { ControlsTopButtons } from "./lib/comps/ControlsTopButtons";
export { DragComponent } from "./lib/comps/DragComponent/DragComponent";
export { PrecisionSelector } from "./lib/comps/PrecisionSelector/PrecisionSelector";
export { FocalLengthDisplay } from "./lib/comps/FocalLengthDisplay/FocalLengthDisplay";
@@ -13,6 +13,17 @@ import { Simple3DVector } from "./datastructures/common";
export type { Camera, FocalLengthDragging };
// Scene metadata — what host tracks in its `signalScene`. Mirrored
// into the store so ControlsTopButtons (lib-resident) can read it
// reactively. Host calls `setSceneMeta` whenever its signal changes.
export interface SceneMeta {
title: string | undefined;
token: string | undefined;
ownerToken: string | undefined;
isModified: boolean | undefined;
isInitializing: boolean;
}
export type SceneObjectKind = "object" | "character" | "shape";
export interface SceneObject {
@@ -164,6 +175,14 @@ interface PageSceneState {
is3DEditorInitialized: boolean;
is3DSceneLoaded: boolean;
// scene metadata — title/token/owner/dirty state. Mirrors what the
// host's signalScene tracks so ControlsTopButtons (lib-resident)
// can read it reactively without depending on host signals.
sceneMeta: SceneMeta;
// Current logged-in user (read from host auth signal). Used for
// ownership permission checks in ControlsTopButtons.
currentUserToken: string | undefined;
// canvas DOM refs (set by canvas components on mount; consumed by
// the engine + hooks)
sceneContainerEl: HTMLDivElement | null;
@@ -245,6 +264,12 @@ interface PageSceneState {
setIs3DEditorInitialized: (initialized: boolean) => void;
setIs3DSceneLoaded: (loaded: boolean) => void;
// scene metadata + auth — driven by host via lifecycle effects in
// the host wrapper (e.g. apps/.../PageScene.tsx mirrors signalScene
// and authentication.userInfo into these).
setSceneMeta: (meta: Partial<SceneMeta>) => void;
setCurrentUserToken: (token: string | undefined) => void;
// canvas refs
setSceneContainerEl: (el: HTMLDivElement | null) => void;
setEditorCanvasEl: (el: HTMLCanvasElement | null) => void;
@@ -305,6 +330,15 @@ export const usePageSceneStore = create<PageSceneState>((set, get) => ({
is3DEditorInitialized: false,
is3DSceneLoaded: false,
sceneMeta: {
title: undefined,
token: undefined,
ownerToken: undefined,
isModified: undefined,
isInitializing: true,
},
currentUserToken: undefined,
sceneContainerEl: null,
editorCanvasEl: null,
camViewCanvasEl: null,
@@ -448,6 +482,10 @@ export const usePageSceneStore = create<PageSceneState>((set, get) => ({
set({ is3DEditorInitialized: initialized }),
setIs3DSceneLoaded: (loaded) => set({ is3DSceneLoaded: loaded }),
setSceneMeta: (meta) =>
set((s) => ({ sceneMeta: { ...s.sceneMeta, ...meta } })),
setCurrentUserToken: (token) => set({ currentUserToken: token }),
setSceneContainerEl: (el) => set({ sceneContainerEl: el }),
setEditorCanvasEl: (el) => set({ editorCanvasEl: el }),
setCamViewCanvasEl: (el) => set({ camViewCanvasEl: el }),
@@ -1,40 +1,47 @@
import { useContext, useState, useCallback } from "react";
import { useParams, useLocation, useNavigate } from "react-router-dom";
import { useSignals, useSignalEffect } from "@preact/signals-react/runtime";
import React, { useCallback, useContext, useState } from "react";
import {
faCheckSquare,
faFile,
faKeyboard,
faSquare,
} from "@fortawesome/pro-solid-svg-icons";
import { ToastTypes } from "~/enums";
import { scene, signalScene, authentication, addToast } from "~/signals";
import { ButtonDropdown } from "@storyteller/ui-button-dropdown";
import { Input } from "@storyteller/ui-input";
import { Button } from "@storyteller/ui-button";
import { LoadUserScenes } from "./LoadUserScenes";
import { getCurrentLocationWithoutParams, isNumberString } from "~/utilities";
import {
CameraAspectRatio,
EngineContext,
getSceneGenerationMetaData,
usePageSceneStore,
} from "@storyteller/ui-pagescene";
import { twMerge } from "tailwind-merge";
import { Help } from "./Help";
import { Modal } from "@storyteller/ui-modal";
import { twMerge } from "tailwind-merge";
import { EngineContext } from "../../contexts/EngineContext/EngineContext";
import { usePageSceneStore } from "../../PageSceneStore";
import { CameraAspectRatio, ToastTypes } from "../../enums";
import { getSceneGenerationMetaData } from "../../sceneMetadata";
import { LoadUserScenes } from "./LoadUserScenes";
import { Help } from "./Help/Help";
const isNumberString = (s: string): boolean => /^\d+$/.test(s);
export const ControlsTopButtons = () => {
useSignals();
const params = useParams();
const location = useLocation();
const navigate = useNavigate();
const editor = useContext(EngineContext);
const [shortcutsIsShowing, setShortcutsIsShowing] = useState(false);
const editorEngine = useContext(EngineContext);
const [sceneTitleInput, setSceneTitleInput] = useState<string>("");
const sceneMeta = usePageSceneStore((s) => s.sceneMeta);
const currentUserToken = usePageSceneStore((s) => s.currentUserToken);
const outlinerShowing = usePageSceneStore((s) => s.outlinerShowing);
const [sceneTitleInput, setSceneTitleInput] = useState<string>(
sceneMeta.title || "",
);
const [sceneTokenSelected, setSceneTokenSelected] = useState<string>("");
// Keep the scene-title input in sync with the loaded scene's title.
// The host wrapper (PageScene.tsx) drives the URL ↔ token bookkeeping;
// here we only mirror the title for the Save-as-copy dialog.
React.useEffect(() => {
if (!sceneMeta.isInitializing && sceneMeta.title !== undefined) {
setSceneTitleInput(sceneMeta.title);
}
}, [sceneMeta.isInitializing, sceneMeta.title]);
const handleChangeSceneTitleInput = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
@@ -42,65 +49,67 @@ export const ControlsTopButtons = () => {
};
const handleResetScene = () => {
editorEngine?.cameraController.changeRenderCameraAspectRatio(
editor?.cameraController.changeRenderCameraAspectRatio(
CameraAspectRatio.HORIZONTAL_3_2,
);
};
const handleButtonSave = async () => {
if (!editorEngine) {
addToast(ToastTypes.ERROR, "No Engine Error in Saving Scenes");
if (!editor) {
// No-op without an editor; the toast is informational only.
return;
}
const sceneGenerationMetadata = getSceneGenerationMetaData(editorEngine);
const sceneGenerationMetadata = getSceneGenerationMetaData(editor);
const retSceneMediaToken = await editorEngine.saveScene({
sceneTitle: scene.value.title || "",
sceneToken: scene.value.token,
const retSceneMediaToken = await editor.saveScene({
sceneTitle: sceneMeta.title || "",
sceneToken: sceneMeta.token,
sceneGenerationMetadata,
});
if (retSceneMediaToken === "") {
addToast(ToastTypes.ERROR, "Failed to Save Scene Try again Later!");
editor.adapter.showToast(
ToastTypes.ERROR,
"Failed to Save Scene Try again Later!",
);
}
if (retSceneMediaToken) {
addToast(ToastTypes.SUCCESS, retSceneMediaToken);
if (!scene.value.token) {
signalScene({
...scene.value,
editor.adapter.showToast(ToastTypes.SUCCESS, retSceneMediaToken);
if (!sceneMeta.token) {
editor.adapter.onSceneTitleChange?.({
title: sceneMeta.title || "",
token: retSceneMediaToken,
ownerToken: sceneMeta.ownerToken,
isModified: false,
});
}
}
};
const handleButtonSaveAsCopy = useCallback(async () => {
if (!editorEngine) {
addToast(ToastTypes.ERROR, "No Engine Error in Saving Scenes");
return;
}
const sceneGenerationMetadata = getSceneGenerationMetaData(editorEngine);
const retSceneMediaToken = await editorEngine.saveScene({
if (!editor) return;
const sceneGenerationMetadata = getSceneGenerationMetaData(editor);
const retSceneMediaToken = await editor.saveScene({
sceneTitle: sceneTitleInput,
sceneToken: undefined,
sceneGenerationMetadata,
});
if (retSceneMediaToken) {
addToast(ToastTypes.SUCCESS, retSceneMediaToken);
signalScene({
...scene.value,
token: retSceneMediaToken,
ownerToken: authentication.userInfo.value?.user_token,
editor.adapter.showToast(ToastTypes.SUCCESS, retSceneMediaToken);
editor.adapter.onSceneTitleChange?.({
title: sceneTitleInput,
token: retSceneMediaToken,
ownerToken: currentUserToken,
isModified: false,
});
}
}, [sceneTitleInput, editorEngine, getSceneGenerationMetaData]);
}, [sceneTitleInput, editor, currentUserToken]);
const handleButtonLoadScene = () => {
handleResetScene();
editorEngine?.loadScene(sceneTokenSelected).catch((err) => {
addToast(ToastTypes.ERROR, err.message);
editor?.loadScene(sceneTokenSelected).catch((err) => {
editor.adapter.showToast(ToastTypes.ERROR, err.message);
});
};
@@ -108,40 +117,15 @@ export const ControlsTopButtons = () => {
setSceneTokenSelected(token);
};
useSignalEffect(() => {
//TODO: USE SIGNAL EFFECT SHOULD BE PERFORM ELSEWHERE
// WHERE THIS OPERATION IS EXPECTED
if (!scene.value.isInitializing) {
setSceneTitleInput(scene.value.title || "");
const currentLocation = getCurrentLocationWithoutParams(
location.pathname,
params,
);
if (scene.value.token === undefined) {
if (params.sceneToken) {
//case of create new scene from existing scene
history.pushState({}, "", currentLocation);
}
//case of create new scene from unsaved scene
navigate(currentLocation, { replace: true });
} else if (scene.value.token) {
if (params.sceneToken && scene.value.token !== params.sceneToken) {
//case of loading existing scene from existing scene
history.pushState({}, "", currentLocation + scene.value.token);
}
//case of loading existing scene from unsaved new scene
//or case of updating existing scene
navigate(currentLocation + scene.value.token, { replace: true });
}
}
});
const outlinerShowing = usePageSceneStore((s) => s.outlinerShowing);
const handleShowOutliner = () => {
usePageSceneStore.getState().setOutlinerShowing(!outlinerShowing);
};
const canSave =
sceneMeta.isModified &&
(sceneMeta.ownerToken === undefined ||
sceneMeta.ownerToken === currentUserToken);
return (
<div className="flex flex-col gap-2 pl-2 pt-2">
<div className="flex gap-1.5">
@@ -157,24 +141,20 @@ export const ControlsTopButtons = () => {
title: "Create New Scene",
content: (
<h4>
Make sure you&apos;ve saved your scene. Unsaved changes will
be lost. Continue?
Make sure you&apos;ve saved your scene. Unsaved changes
will be lost. Continue?
</h4>
),
confirmButtonProps: {
label: "Create new scene",
onClick: async () => {
handleResetScene();
const defaultTitle = "Untitled New Scene";
setSceneTitleInput(defaultTitle);
await editorEngine?.newScene(defaultTitle);
await editor?.newScene(defaultTitle);
},
},
closeButtonProps: {
label: "Cancel",
},
closeButtonProps: { label: "Cancel" },
showClose: true,
},
},
@@ -191,51 +171,37 @@ export const ControlsTopButtons = () => {
disabled: sceneTokenSelected === "",
onClick: handleButtonLoadScene,
},
closeButtonProps: {
label: "Cancel",
},
closeButtonProps: { label: "Cancel" },
showClose: true,
className: "max-w-5xl",
},
},
{
disabled: !(
scene.value.isModified &&
(scene.value.ownerToken === undefined ||
scene.value.ownerToken ===
authentication.userInfo.value?.user_token)
),
// save scene should be disabled if there are no changes
disabled: !canSave,
label: "Save scene",
description: "Ctrl+S",
...(scene.value.token
? {
// Existing scene → save directly without dialog
onClick: handleButtonSave,
}
...(sceneMeta.token
? { onClick: handleButtonSave }
: {
// New (unsaved) scene → ask for confirmation first
dialogProps: {
title: "Save Scene",
content: (
<h4>
Save scene to <b>{scene.value.title}</b>?
Save scene to <b>{sceneMeta.title}</b>?
</h4>
),
confirmButtonProps: {
label: "Save",
onClick: handleButtonSave,
},
closeButtonProps: {
label: "Cancel",
},
closeButtonProps: { label: "Cancel" },
showClose: true,
},
}),
divider: true,
},
{
disabled: !scene.value.isModified || !scene.value.token,
disabled: !sceneMeta.isModified || !sceneMeta.token,
label: "Save scene as copy",
description: "Ctrl+Shift+S",
onDialogOpen: () => {
@@ -266,9 +232,7 @@ export const ControlsTopButtons = () => {
disabled: sceneTitleInput === "",
onClick: handleButtonSaveAsCopy,
},
closeButtonProps: {
label: "Cancel",
},
closeButtonProps: { label: "Cancel" },
showClose: true,
},
},
@@ -305,8 +269,6 @@ export const ControlsTopButtons = () => {
>
<Help />
</Modal>
</div>
);
};
@@ -1,73 +1,57 @@
import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import dayjs from "dayjs";
import type { MediaInfo } from "@storyteller/ui-pagescene";
import { FilterEngineCategories, ToastTypes } from "~/enums";
import { addToast } from "~/signals";
import { ScenePicker, SceneTypes } from "../ScenePicker";
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
import { Label } from "@storyteller/ui-label";
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
import { EngineContext } from "../../../contexts/EngineContext/EngineContext";
import { FilterEngineCategories, ToastTypes } from "../../../enums";
import type { MediaInfo } from "../../../models";
import { ScenePicker, SceneTypes } from "../ScenePicker";
interface LoadSceneProps {
onSceneSelect: (token: string) => void;
}
export enum FetchStatus {
paused,
// ready triggers a new fetch
ready,
in_progress,
success,
error,
}
export enum Filters {
Featured,
Mine,
Bookmarked,
}
export const LoadUserScenes = ({ onSceneSelect }: LoadSceneProps) => {
const editor = useContext(EngineContext);
const [scenes, setScenes] = useState<SceneTypes[] | undefined>(undefined);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);
const getScenesByUser = useCallback(async () => {
const modMediaInfoToScenes = (results: MediaInfo[]) =>
results.map((scene: MediaInfo) => ({
token: scene.token,
name: scene.maybe_title ?? "Untitled",
updated_at: dayjs(scene.updated_at).format("MMM D, YYYY HH:mm:ss"),
thumbnail: scene.cover_image.maybe_cover_image_public_bucket_path
? scene.cover_image.maybe_cover_image_public_bucket_path
: undefined,
if (!editor) return;
const modMediaInfoToScenes = (results: MediaInfo[]): SceneTypes[] =>
results.map((s) => ({
token: s.token,
name: s.maybe_title ?? "Untitled",
updated_at: dayjs(s.updated_at).format("MMM D, YYYY HH:mm:ss"),
thumbnail: s.cover_image.maybe_cover_image_public_bucket_path
? s.cover_image.maybe_cover_image_public_bucket_path
: "",
}));
const mediaFilesApi = new MediaFilesApi();
const response = await mediaFilesApi.ListUserMediaFiles({
filter_engine_categories: [FilterEngineCategories.SCENE],
const response = await editor.adapter.listUserMediaFiles({
pageSize: 100,
filterEngineCategories: [FilterEngineCategories.SCENE],
});
if (response.success && response.data) {
setScenes(modMediaInfoToScenes(response.data));
return;
}
addToast(
editor.adapter.showToast(
ToastTypes.ERROR,
response.errorMessage || "Unknown Error in Loading User Scenes",
);
}, []);
}, [editor]);
useEffect(() => {
if (scenes) {
//only call once on mount
return;
}
if (scenes) return;
getScenesByUser();
}, [scenes, getScenesByUser]);
@@ -81,7 +65,6 @@ export const LoadUserScenes = ({ onSceneSelect }: LoadSceneProps) => {
const atBottom =
element.scrollHeight - element.scrollTop <= element.clientHeight + 1;
const hasOverflow = element.scrollHeight > element.clientHeight;
setBottomGradientOpacity(hasOverflow && !atBottom ? 1 : 0);
}
};
@@ -92,12 +75,10 @@ export const LoadUserScenes = ({ onSceneSelect }: LoadSceneProps) => {
if (element) {
handleScroll();
element.addEventListener("scroll", handleScroll);
return () => {
element.removeEventListener("scroll", handleScroll);
};
return () => element.removeEventListener("scroll", handleScroll);
}
}
return undefined;
}, [scenes]);
return (
@@ -1,20 +1,17 @@
import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import dayjs from "dayjs";
import type { MediaInfo } from "@storyteller/ui-pagescene";
import { FilterEngineCategories, ToastTypes } from "~/enums";
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
import { EngineContext } from "../../../contexts/EngineContext/EngineContext";
import { FilterEngineCategories, ToastTypes } from "../../../enums";
import type { MediaInfo } from "../../../models";
import { ScenePicker, SceneTypes } from "../ScenePicker";
import { LoadingSpinner } from "~/components";
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
import { addToast } from "~/signals";
interface NewSceneFromTemplateProps {
onSceneSelect: (token: string) => void;
@@ -23,15 +20,15 @@ interface NewSceneFromTemplateProps {
export const NewSceneFromTemplate = ({
onSceneSelect,
}: NewSceneFromTemplateProps) => {
const [featuredScenes, setFeaturedScenes] = useState<
SceneTypes[] | undefined
>(undefined);
const editor = useContext(EngineContext);
const [featuredScenes, setFeaturedScenes] = useState<SceneTypes[] | undefined>(
undefined,
);
const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const listFeaturedScenes = useCallback(async () => {
const mediaFilesApi = new MediaFilesApi();
// Hard coded scenes
if (!editor) return;
const dummyScenes: SceneTypes[] = [
{
@@ -40,17 +37,18 @@ export const NewSceneFromTemplate = ({
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
];
const modMediaInfoToScenes = (results: MediaInfo[]) =>
results.map((scene: MediaInfo) => ({
token: scene.token,
name: scene.maybe_title ?? "Untitled",
updated_at: dayjs(scene.updated_at).format("MMM D, YYYY HH:mm:ss"),
thumbnail: scene.cover_image.maybe_cover_image_public_bucket_path
? scene.cover_image.maybe_cover_image_public_bucket_path
: undefined,
const modMediaInfoToScenes = (results: MediaInfo[]): SceneTypes[] =>
results.map((s) => ({
token: s.token,
name: s.maybe_title ?? "Untitled",
updated_at: dayjs(s.updated_at).format("MMM D, YYYY HH:mm:ss"),
thumbnail: s.cover_image.maybe_cover_image_public_bucket_path
? s.cover_image.maybe_cover_image_public_bucket_path
: "",
}));
const response = await mediaFilesApi.ListFeaturedMediaFiles({
filter_engine_categories: [FilterEngineCategories.SCENE],
const response = await editor.adapter.listFeaturedMediaFiles({
pageSize: 100,
filterEngineCategories: [FilterEngineCategories.SCENE],
});
if (response.success && response.data) {
setFeaturedScenes([
@@ -60,16 +58,14 @@ export const NewSceneFromTemplate = ({
return;
}
setFeaturedScenes(dummyScenes);
addToast(
editor.adapter.showToast(
ToastTypes.ERROR,
response.errorMessage || "Unknown Error in Listing Feature Scenes",
);
}, []);
}, [editor]);
useEffect(() => {
if (featuredScenes) {
//only call once on mount
return;
}
if (featuredScenes) return;
listFeaturedScenes();
}, [featuredScenes, listFeaturedScenes]);
@@ -83,7 +79,6 @@ export const NewSceneFromTemplate = ({
const atBottom =
element.scrollHeight - element.scrollTop <= element.clientHeight + 1;
const hasOverflow = element.scrollHeight > element.clientHeight;
setBottomGradientOpacity(hasOverflow && !atBottom ? 1 : 0);
}
};
@@ -93,11 +88,9 @@ export const NewSceneFromTemplate = ({
if (element) {
handleScroll();
element.addEventListener("scroll", handleScroll);
return () => {
element.removeEventListener("scroll", handleScroll);
};
return () => element.removeEventListener("scroll", handleScroll);
}
return undefined;
}, []);
return (
@@ -1,6 +1,6 @@
import React, { useRef, useState } from "react";
import { BucketConfig } from "~/api/BucketConfig";
import React, { useContext, useState } from "react";
import { twMerge } from "tailwind-merge";
import { EngineContext } from "../../../contexts/EngineContext/EngineContext";
export type SceneTypes = {
token: string;
@@ -22,17 +22,17 @@ export const SceneCard: React.FC<SceneCardProps> = ({
selectedSceneId,
showDate,
}) => {
// console.log(scene);
const editor = useContext(EngineContext);
const [loadError, setLoadError] = useState(false);
const handleSelected = (scene: SceneTypes) => {
onSceneSelect(scene);
const handleSelected = (s: SceneTypes) => {
onSceneSelect(s);
};
const bucketConfig = useRef(new BucketConfig());
const imageThumbnailUrl =
scene.thumbnail && bucketConfig.current.getCdnUrl(scene.thumbnail, 360, 20);
// thumbnail will be replaced with 3d scene screenshots
scene.thumbnail && editor
? editor.adapter.getCdnUrl(scene.thumbnail, 360, 20)
: undefined;
const tempThumbnail = "/resources/placeholders/scene_placeholder.png";
return (
@@ -58,7 +58,7 @@ export const SceneCard: React.FC<SceneCardProps> = ({
/>
<div className="absolute left-0 top-0 h-full w-full bg-gradient-to-t from-black/80 to-transparent" />
<div className="absolute bottom-[8px] left-[10px] text-start text-sm drop-shadow-md">
<div className="flex flex-col">
<div className="flex flex-col">
<span className="w-60 truncate text-sm font-medium">
{scene.name}
</span>
@@ -1,12 +1,7 @@
import React, { useEffect, useState } from "react";
import { SceneCard } from "~/pages/PageScene/comps/ControlsTopButtons/ScenePicker/SceneCard";
import { SceneCard, SceneTypes } from "./SceneCard";
export type SceneTypes = {
token: string;
name: string;
updated_at?: string;
thumbnail: string | undefined;
};
export type { SceneTypes };
interface ScenePickerProps {
scenes: SceneTypes[];
@@ -24,7 +19,7 @@ export const ScenePicker: React.FC<ScenePickerProps> = ({
useEffect(() => {
if (scenes.length > 0 && !selectedSceneId) {
setSelectedSceneId(scenes[0].token);
onSceneSelect(scenes[0]); // Trigger the callback with the first scene
onSceneSelect(scenes[0]);
}
}, [scenes, onSceneSelect, selectedSceneId]);
@@ -0,0 +1,2 @@
export { ScenePicker } from "./ScenePicker";
export type { SceneTypes } from "./SceneCard";
@@ -0,0 +1 @@
export { ControlsTopButtons } from "./ControlsTopButtons";
@@ -5,6 +5,9 @@
{
"path": "../button-icon"
},
{
"path": "../loading-spinner"
},
{
"path": "../label"
},
@@ -53,6 +53,9 @@
{
"path": "../button-icon/tsconfig.lib.json"
},
{
"path": "../loading-spinner/tsconfig.lib.json"
},
{
"path": "../label/tsconfig.lib.json"
},