create new scene from template/load scene ui (#230)

This commit is contained in:
BFlat authored and GitHub committed 2024-04-25 12:20:16 -04:00
1 parent b39793a1a6
commit 0a186908fb
11 files changed
+378 -38

No files matched your search

+35 -23
View File
@@ -9,7 +9,6 @@ import { Button, ButtonPropsI } from "./Button";
import { TransitionDialogue } from "~/components/TransitionDialogue";
type UnionedButtonProps = { label?: string } & ButtonPropsI;
type ModalPropsType = ButtonDropdownProps["options"][number]["dialogProps"];
interface ButtonDropdownProps {
label: string;
@@ -27,6 +26,7 @@ interface ButtonDropdownProps {
confirmButtonProps?: UnionedButtonProps;
closeButtonProps?: UnionedButtonProps;
showClose?: boolean;
onClose?: () => void;
};
}>;
}
@@ -37,20 +37,31 @@ export const ButtonDropdown = ({
icon,
}: ButtonDropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const [modalProps, setModalProps] = useState<ModalPropsType | null>(null);
const [selectedOptionIndex, setSelectedOptionIndex] = useState<number | null>(
null,
);
const closeModal = () => setIsOpen(false);
const closeModal = () => {
setIsOpen(false);
options[selectedOptionIndex!].dialogProps?.onClose?.();
};
const handleOptionClick = (option) => {
const handleOptionClick = (index: number) => {
const option = options[index];
if (option.onClick) {
option.onClick();
}
if (option.dialogProps) {
setModalProps(option.dialogProps);
setSelectedOptionIndex(index);
setIsOpen(true);
}
};
const currentDialogProps =
selectedOptionIndex !== null
? options[selectedOptionIndex].dialogProps
: null;
return (
<div className="relative">
<Menu as="div" className="inline-block text-left">
@@ -84,7 +95,7 @@ export const ButtonDropdown = ({
className={`duration-50 bg-brand-secondary font-medium text-white transition-all ${
active ? "bg-ui-controls-button/60" : ""
} ${option.disabled ? "pointer-events-none opacity-40" : ""} group flex w-full items-center py-1.5 pl-7 pr-4 text-sm`.trim()}
onClick={() => handleOptionClick(option)}>
onClick={() => handleOptionClick(index)}>
<div className="flex w-full">
<div className="grow text-start">{option.label}</div>
<div className="ml-10 font-normal text-white/75">
@@ -101,34 +112,35 @@ export const ButtonDropdown = ({
</Transition>
</Menu>
{modalProps && (
{currentDialogProps && (
<TransitionDialogue
title={modalProps.title}
title={currentDialogProps.title}
isOpen={isOpen}
onClose={closeModal}
className={modalProps.className}>
{modalProps.content}
className={currentDialogProps.className}>
{currentDialogProps.content}
<div className="mt-6 flex justify-end gap-2">
{modalProps.showClose !== false && modalProps.closeButtonProps && (
<Button
variant="secondary"
{...modalProps.closeButtonProps}
onClick={closeModal}>
{modalProps.closeButtonProps.label}
</Button>
)}
{currentDialogProps.showClose !== false &&
currentDialogProps.closeButtonProps && (
<Button
variant="secondary"
{...currentDialogProps.closeButtonProps}
onClick={closeModal}>
{currentDialogProps.closeButtonProps.label}
</Button>
)}
{modalProps.confirmButtonProps && (
{currentDialogProps.confirmButtonProps && (
<Button
{...modalProps.confirmButtonProps}
{...currentDialogProps.confirmButtonProps}
onClick={(e) => {
if (modalProps.confirmButtonProps?.onClick) {
modalProps.confirmButtonProps?.onClick(e);
if (currentDialogProps.confirmButtonProps?.onClick) {
currentDialogProps.confirmButtonProps?.onClick(e);
}
closeModal();
}}>
{modalProps.confirmButtonProps.label || "Confirm"}
{currentDialogProps.confirmButtonProps.label || "Confirm"}
</Button>
)}
</div>
+1 -2
View File
@@ -1,6 +1,5 @@
import { useEffect } from "react";
import { useEffect, Fragment, ReactNode } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import {
disableHotkeyInput,
@@ -8,6 +8,8 @@ import { ToasterContext } from "~/contexts/ToasterContext";
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);
@@ -38,6 +40,11 @@ export const ControlsTopButtons = () => {
});
};
const handleSceneSelection = (token: string) => {
setSceneToken(token);
console.log(`Selected Scene Token is ${token}`);
};
return (
<div className="flex flex-col gap-2 pl-3 pt-3">
<div className="flex gap-1.5">
@@ -53,9 +60,11 @@ export const ControlsTopButtons = () => {
content: (
<Input
label="Please enter a name for your new scene"
value={sceneName}
onChange={(e) => {
setSceneToken(e.target.value);
setSceneName(e.target.value);
}}
autoComplete="false"
/>
),
confirmButtonProps: {
@@ -67,6 +76,27 @@ export const ControlsTopButtons = () => {
label: "Cancel",
},
showClose: true,
onClose: () => setSceneName(""),
},
},
{
label: "New scene from template...",
description: "Ctrl+Shift+N",
dialogProps: {
title: "Create a New Scene from Template",
content: (
<NewSceneFromTemplate onSceneSelect={handleSceneSelection} />
),
confirmButtonProps: {
label: "Create",
disabled: sceneToken === "",
onClick: handleButtonLoadScene,
},
closeButtonProps: {
label: "Cancel",
},
showClose: true,
className: "max-w-7xl w-auto",
},
},
{
@@ -74,14 +104,7 @@ export const ControlsTopButtons = () => {
description: "Ctrl+O",
dialogProps: {
title: "Load a Scene",
content: (
<Input
label="Please provide the token of the scene you want to load"
onChange={(e) => {
setSceneToken(e.target.value);
}}
/>
),
content: <LoadScene onSceneSelect={handleSceneSelection} />,
confirmButtonProps: {
label: "Load",
disabled: sceneToken === "",
@@ -91,10 +114,11 @@ export const ControlsTopButtons = () => {
label: "Cancel",
},
showClose: true,
className: "max-w-7xl w-auto",
},
},
{
disabled: true, // save scene should be disabled if there are no changes
label: "Save scene",
description: "Ctrl+S",
dialogProps: {
@@ -119,7 +143,6 @@ export const ControlsTopButtons = () => {
},
divider: true,
},
{
label: "Save scene as copy",
description: "Ctrl+Shift+S",
@@ -0,0 +1,147 @@
import { useEffect, useRef, useState } from "react";
import { ScenePicker, SceneTypes } from "../ScenePicker";
import { Label } from "~/components";
interface LoadSceneProps {
onSceneSelect: (token: string) => void;
}
export const LoadScene = ({ onSceneSelect }: LoadSceneProps) => {
// Dummy Data - replace with API data
const dummyScenes: SceneTypes[] = [
{
token: "111",
name: "Dragon",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "222",
name: "Dancing Girl",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "333",
name: "Dragon",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "444",
name: "Home Office",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "555",
name: "Dragon",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "666",
name: "Dancing Girl",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "777",
name: "Dragon",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "888",
name: "Dancing Girl",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "999",
name: "Dragon",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "000",
name: "Home Office",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "0001",
name: "Home Office",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "0002",
name: "Home Office",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "0003",
name: "Home Office",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "0004",
name: "Home Office",
updated_at: "Apr 03, 2024 14:24:48",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
];
const handleSceneSelect = (selectedScene: SceneTypes) => {
onSceneSelect(selectedScene.token);
};
const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const handleScroll = () => {
const element = scrollContainerRef.current;
if (element) {
const atBottom =
element.scrollHeight - element.scrollTop === element.clientHeight;
const hasOverflow = element.scrollHeight > element.clientHeight;
setBottomGradientOpacity(hasOverflow && !atBottom ? 1 : 0);
}
};
useEffect(() => {
const element = scrollContainerRef.current;
if (element) {
handleScroll();
element.addEventListener("scroll", handleScroll);
return () => {
element.removeEventListener("scroll", handleScroll);
};
}
}, []);
return (
<div className="flex flex-col gap-0.5">
<Label>My Scenes</Label>
<div className="relative flex max-h-[500px] flex-col">
<div
className="overflow-y-auto overflow-x-hidden"
ref={scrollContainerRef}>
<ScenePicker
scenes={dummyScenes}
onSceneSelect={handleSceneSelect}
showDate={true}
/>
</div>
<div
className="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-10 bg-gradient-to-t from-ui-panel to-transparent transition-opacity duration-200"
style={{ opacity: bottomGradientOpacity }}
/>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { LoadScene } from "./LoadScene";
@@ -0,0 +1,69 @@
import { useEffect, useRef, useState } from "react";
import { ScenePicker, SceneTypes } from "../ScenePicker";
interface NewSceneFromTemplateProps {
onSceneSelect: (token: string) => void;
}
export const NewSceneFromTemplate = ({
onSceneSelect,
}: NewSceneFromTemplateProps) => {
// Dummy Data - replace with API data
const dummyScenes: SceneTypes[] = [
{
token: "m_9pf7a7v0138zx58f4x6ejsehjcvfq6",
name: "Dragon",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
{
token: "m_nmzvdqr6kr8eqpmxqdzkqj0yknrjwv",
name: "Dancing Girl",
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
];
const handleSceneSelect = (selectedScene: SceneTypes) => {
onSceneSelect(selectedScene.token);
};
const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const handleScroll = () => {
const element = scrollContainerRef.current;
if (element) {
const atBottom =
element.scrollHeight - element.scrollTop === element.clientHeight;
const hasOverflow = element.scrollHeight > element.clientHeight;
setBottomGradientOpacity(hasOverflow && !atBottom ? 1 : 0);
}
};
useEffect(() => {
const element = scrollContainerRef.current;
if (element) {
handleScroll();
element.addEventListener("scroll", handleScroll);
return () => {
element.removeEventListener("scroll", handleScroll);
};
}
}, []);
return (
<div className="flex flex-col gap-0.5">
<div className="relative flex max-h-[500px] flex-col">
<div
className="overflow-y-auto overflow-x-hidden"
ref={scrollContainerRef}>
<ScenePicker scenes={dummyScenes} onSceneSelect={handleSceneSelect} />
</div>
<div
className="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-10 bg-gradient-to-t from-ui-panel to-transparent transition-opacity duration-200"
style={{ opacity: bottomGradientOpacity }}
/>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { NewSceneFromTemplate } from "./NewSceneFromTemplate";
@@ -0,0 +1,84 @@
import React, { useEffect, useState } from "react";
import { twMerge } from "tailwind-merge";
export type SceneTypes = {
token: string;
name: string;
updated_at?: string;
thumbnail: string;
};
interface ScenePickerProps {
scenes: SceneTypes[];
onSceneSelect: (selectedScene: SceneTypes) => void;
showDate?: boolean;
}
export const ScenePicker: React.FC<ScenePickerProps> = ({
scenes,
onSceneSelect,
showDate,
}) => {
const [selectedSceneId, setSelectedSceneId] = useState<string | null>(null);
useEffect(() => {
if (scenes.length > 0 && !selectedSceneId) {
setSelectedSceneId(scenes[0].token);
onSceneSelect(scenes[0]); // Trigger the callback with the first scene
}
}, [scenes, onSceneSelect, selectedSceneId]);
const handleSelected = (scene: SceneTypes) => {
setSelectedSceneId(scene.token);
onSceneSelect(scene);
};
return (
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{scenes.map((scene) => (
<button
key={scene.token}
className={twMerge(
"relative w-[260px] cursor-pointer overflow-hidden rounded-lg border-2 transition-colors ease-in-out ",
selectedSceneId === scene.token
? "border-brand-primary"
: "border-[#4B4B5C] hover:border-ui-controls-button",
)}
onClick={() => handleSelected(scene)}>
<img
className="aspect-video object-cover"
src={scene.thumbnail}
alt={scene.name}
/>
<div className="absolute left-0 top-0 h-full w-full bg-gradient-to-t from-ui-panel to-transparent" />
<div className="absolute bottom-[8px] left-[10px] text-start text-sm drop-shadow-md">
<div className="flex flex-col">
<span className="w-60 truncate text-sm font-medium">
{scene.name}
</span>
{showDate && (
<span className="text-xs opacity-70">{scene.updated_at}</span>
)}
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className={`absolute right-1.5 top-1.5 h-[22px] w-[22px] transition-opacity duration-200 ease-in-out ${
selectedSceneId === scene.token ? "opacity-100" : "opacity-0"
}`}>
<path
opacity="1"
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
fill="#FC6B68"
/>
<path
d="M369 175c-9.4 9.4-9.4 24.6 0 33.9L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0z"
fill="#FFFFFF"
/>
</svg>
</button>
))}
</div>
);
};
@@ -0,0 +1,2 @@
export { ScenePicker } from "./ScenePicker";
export type { SceneTypes } from "./ScenePicker";
@@ -27,7 +27,9 @@ export const ItemPicker = ({
<button
className={twMerge(
"relative cursor-pointer overflow-hidden rounded-lg border-2 transition-colors ease-in-out",
selected ? "border-brand-primary" : "border-ui-border",
selected
? "border-brand-primary"
: "border-[#4B4B5C] hover:border-ui-controls-button",
)}
style={{
minWidth: (width as number) + 4,
@@ -47,7 +49,7 @@ export const ItemPicker = ({
maxHeight: height,
}}
/>
<div className="absolute left-0 top-0 h-full w-full bg-gradient-to-t from-gray-700" />
<div className="absolute left-0 top-0 h-full w-full bg-gradient-to-t from-ui-panel" />
<H4 className="absolute bottom-[1px] left-[6px] text-start text-[13px] drop-shadow-md">
{label}
</H4>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB