mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 13:02:37 -05:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eed156da5 | ||
|
|
94f2d2ec68 | ||
|
|
384b841528 | ||
|
|
62f97317ff | ||
|
|
8210a9f063 | ||
|
|
b4d603098d |
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft 2D",
|
||||
"version": "0.1.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/editor2d/dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "nx run editor2d:dev",
|
||||
"beforeBuildCommand": "nx run editor2d:build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ArtCraft 2D",
|
||||
"width": 2400,
|
||||
"height": 1300,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft 3D",
|
||||
"version": "0.1.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/editor3d/dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "nx run editor3d:dev",
|
||||
"beforeBuildCommand": "nx run editor3d:build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ArtCraft 3D",
|
||||
"width": 2400,
|
||||
"height": 1300,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor3d/dist"
|
||||
beforeDevCommand = "nx run editor3d:dev"
|
||||
devUrl = "http://localhost:5173"
|
||||
@@ -4,10 +4,10 @@
|
||||
"version": "0.1.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/editor3d/dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "nx run editor3d:dev",
|
||||
"beforeBuildCommand": "nx run editor3d:build"
|
||||
"frontendDist": "../../../frontend/apps/editor2d/dist",
|
||||
"devUrl": "http://localhost:5741",
|
||||
"beforeDevCommand": "nx run editor2d:dev",
|
||||
"beforeBuildCommand": "nx run editor2d:build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
|
||||
@@ -42,6 +42,16 @@ npx nx build editor2d
|
||||
dev:
|
||||
npx nx dev editor3d
|
||||
npx nx dev editor2d
|
||||
|
||||
|
||||
|
||||
# component build
|
||||
1.
|
||||
npx nx g @nx/react:library libs/components/toaster --import-path=@storyteller/ui-toaster --bundler=vite
|
||||
npm install
|
||||
2.import it first in code
|
||||
3.nx sync
|
||||
4.nx build
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@headlessui/react": "^2.1.3",
|
||||
"@preact/signals-react": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
|
||||
@@ -62,6 +62,7 @@ export class EditEngine {
|
||||
this.editMode = EditModes.SELECT;
|
||||
|
||||
this.boardCanvasRef = boardCanvasRef;
|
||||
|
||||
this.stage = new Konva.Stage({
|
||||
container: this.boardCanvasRef,
|
||||
width: window.innerWidth,
|
||||
@@ -238,13 +239,19 @@ export class EditEngine {
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(0, 0);
|
||||
console.log("onBoardCanvasResize Event called");
|
||||
console.log("Board Canvas values below");
|
||||
console.log(this.boardCanvasRef.offsetWidth);
|
||||
console.log(this.boardCanvasRef.offsetHeight);
|
||||
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
height: this.boardCanvasRef.offsetHeight,
|
||||
},
|
||||
});
|
||||
|
||||
this.uiLayer.draw();
|
||||
this.stage.width(this.boardCanvasRef.offsetWidth);
|
||||
this.stage.height(this.boardCanvasRef.offsetHeight);
|
||||
|
||||
@@ -510,7 +510,20 @@ export class Engine {
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
|
||||
console.log("onBoardCanvasResize Event called");
|
||||
// Print window dimensions for debugging
|
||||
console.log("Window dimensions:", {
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight,
|
||||
outerWidth: window.outerWidth,
|
||||
outerHeight: window.outerHeight,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(
|
||||
window.innerWidth,
|
||||
window.innerHeight,
|
||||
);
|
||||
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
|
||||
@@ -516,36 +516,48 @@ export class RealTimeDrawEngine {
|
||||
width: number | undefined,
|
||||
height: number | undefined,
|
||||
) {
|
||||
console.log("updateCaptureCanvas");
|
||||
if (!this.captureCanvas) {
|
||||
return;
|
||||
}
|
||||
if (width) {
|
||||
this.width = width;
|
||||
}
|
||||
if (height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
// Ensures that all the nodes stag in the same place should
|
||||
// there be a window resize.
|
||||
// recompute the position
|
||||
// to ensure that the position of this stays
|
||||
|
||||
const scaleFactor = 1;
|
||||
// Store old values for calculating delta
|
||||
const oldPositionX = this.positionX;
|
||||
const oldPositionY = this.positionY;
|
||||
|
||||
// recompute the position
|
||||
// If width/height aren't provided, use window dimensions
|
||||
if (width === undefined) {
|
||||
this.width = window.innerWidth * scaleFactor; // Using 80% of window width
|
||||
} else {
|
||||
this.width = width * scaleFactor;
|
||||
}
|
||||
|
||||
if (height === undefined) {
|
||||
this.height = window.innerHeight * scaleFactor; // Using 80% of window height
|
||||
} else {
|
||||
this.height = height * scaleFactor;
|
||||
}
|
||||
|
||||
console.log("New dimensions:", this.width, this.height);
|
||||
|
||||
// Calculate new position to center the canvas in the window
|
||||
this.positionX = (window.innerWidth - this.width) / 2;
|
||||
this.positionY = (window.innerHeight - this.height) / 2;
|
||||
|
||||
console.log("New centered position:", this.positionX, this.positionY);
|
||||
|
||||
// Update the canvas position and size
|
||||
this.captureCanvas.setPosition({
|
||||
x: this.positionX / 2,
|
||||
y: this.positionY / 2,
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
});
|
||||
this.captureCanvas.size({ width: this.width, height: this.height });
|
||||
|
||||
// this is the change in positions
|
||||
// Calculate the change in position
|
||||
const deltaX = this.positionX - oldPositionX;
|
||||
const deltaY = this.positionY - oldPositionY;
|
||||
|
||||
// Update all children except the capture canvas
|
||||
var children = this.mediaLayerRef.getChildren();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let node = children[i];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { withProtectionRoute } from "~/components/hoc";
|
||||
import { useRenderCounter } from "~/hooks/useRenderCounter";
|
||||
import { Resource, invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// Components of the page
|
||||
import { ToolbarUserProfile } from "~/components/features";
|
||||
@@ -26,6 +27,11 @@ import {
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export const isTauri = (): boolean => {
|
||||
console.log(window.__TAURI_INTERNALS__);
|
||||
return window.__TAURI_INTERNALS__;
|
||||
};
|
||||
|
||||
export const Main = withProtectionRoute(() => {
|
||||
// This is a hook that will log the number of times the component has rerendered
|
||||
// Let's make sure we only log once
|
||||
@@ -40,7 +46,7 @@ export const Main = withProtectionRoute(() => {
|
||||
const realtimeEngineRef = useRef<EngineType | null>(null);
|
||||
const generationEngineRef = useRef<GenerationEngine | null>(null);
|
||||
const [firstTimeDialogOpen, setFirstTimeDialogOpen] = useState(true);
|
||||
|
||||
const [signinDialogOpen, setSigninDialogOpen] = useState(false);
|
||||
const appModeValue = appMode.value;
|
||||
let childView;
|
||||
|
||||
@@ -97,7 +103,7 @@ export const Main = withProtectionRoute(() => {
|
||||
}}
|
||||
className="max-w-5xl"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-6">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<h1 className="text-3xl font-bold">
|
||||
<FontAwesomeIcon
|
||||
@@ -123,7 +129,22 @@ export const Main = withProtectionRoute(() => {
|
||||
</video>
|
||||
</div>
|
||||
<Button
|
||||
className="px-4 py-3 font-semibold"
|
||||
className="font-semibold"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
onClick={async () => {
|
||||
if (isTauri()) {
|
||||
await invoke('open_login_command');
|
||||
setFirstTimeDialogOpen(false);
|
||||
} else {
|
||||
console.error('Tauri is not available in this environment');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Signin to OpenAI to get Started
|
||||
</Button>
|
||||
{/* <Button
|
||||
className="font-semibold"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
onClick={() => {
|
||||
@@ -131,7 +152,7 @@ export const Main = withProtectionRoute(() => {
|
||||
}}
|
||||
>
|
||||
Start creating now
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</>
|
||||
|
||||
@@ -60,6 +60,7 @@ export const LoadingBar = ({
|
||||
|
||||
return (
|
||||
<Transition
|
||||
as="div"
|
||||
className={wrapperClassName}
|
||||
show={show}
|
||||
enter="transition-opacity duration-150"
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { H4 } from '.';
|
||||
import { H4 } from ".";
|
||||
|
||||
interface LoadingDotsProps {
|
||||
className?: string;
|
||||
isShowing?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoadingDotsInnerProps {
|
||||
className?: string;
|
||||
isShowing?: boolean;
|
||||
type?: 'typing'|'bricks';
|
||||
type?: "typing" | "bricks";
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const LoadingDotsTyping = (props: LoadingDotsProps)=>{
|
||||
return (
|
||||
<LoadingDots {...props} />
|
||||
)
|
||||
}
|
||||
export const LoadingDotsBricks = (props: LoadingDotsProps)=>{
|
||||
return (
|
||||
<LoadingDots {...props} type="bricks"/>
|
||||
)
|
||||
}
|
||||
export const LoadingDotsTyping = (props: LoadingDotsProps) => {
|
||||
return <LoadingDots {...props} />;
|
||||
};
|
||||
export const LoadingDotsBricks = (props: LoadingDotsProps) => {
|
||||
return <LoadingDots {...props} type="bricks" />;
|
||||
};
|
||||
|
||||
export function LoadingDots({
|
||||
className,
|
||||
isShowing=true,
|
||||
type = 'typing',
|
||||
isShowing = true,
|
||||
type = "typing",
|
||||
message,
|
||||
}: LoadingDotsInnerProps){
|
||||
const classNames = twMerge("w-full h-full flex flex-col justify-center items-center bg-ui-background gap-6", className);
|
||||
}: LoadingDotsInnerProps) {
|
||||
const classNames = twMerge(
|
||||
"w-full h-full flex flex-col justify-center items-center bg-ui-background gap-6",
|
||||
className,
|
||||
);
|
||||
|
||||
return(
|
||||
return (
|
||||
<Transition
|
||||
as="div"
|
||||
className={classNames}
|
||||
show={isShowing}
|
||||
enter="transition-opacity duration-150"
|
||||
@@ -45,17 +45,9 @@ export function LoadingDots({
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{ type==='typing' &&
|
||||
<div className="dot-typing"></div>
|
||||
}
|
||||
{ type==='bricks' &&
|
||||
<div className="dot-bricks"></div>
|
||||
}
|
||||
{
|
||||
message &&
|
||||
<H4>{message}</H4>
|
||||
}
|
||||
{type === "typing" && <div className="dot-typing"></div>}
|
||||
{type === "bricks" && <div className="dot-bricks"></div>}
|
||||
{message && <H4>{message}</H4>}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,76 +1,95 @@
|
||||
import { Fragment } from "react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { faCheck, faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { MediaFileAnimationType } from "~/enums";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type SelectValue = string | number;
|
||||
export type SelectOption = { label: string; value: SelectValue };
|
||||
|
||||
interface ListDropdownProps {
|
||||
options: SelectOption[];
|
||||
onChange: (val: MediaFileAnimationType) => void;
|
||||
onChange: (val: SelectValue) => void;
|
||||
placeholder?: string;
|
||||
value?: SelectValue;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export const Select = ({
|
||||
onChange,
|
||||
options,
|
||||
placeholder,
|
||||
value,
|
||||
id,
|
||||
}: ListDropdownProps) => {
|
||||
const selectedOption = options.find(
|
||||
(option: SelectOption) => option.value === value,
|
||||
) || { label: placeholder || "", value: "" };
|
||||
|
||||
return (
|
||||
<Listbox value={value} onChange={onChange}>
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button className="relative h-10 w-full cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm">
|
||||
<span className={`block truncate${value ? "" : " opacity-50"}`}>
|
||||
{selectedOption.label}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true" />
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-lg focus:outline-none sm:text-sm">
|
||||
{options.map((option, itemIdx) => (
|
||||
<Listbox.Option
|
||||
key={itemIdx}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 text-white ${
|
||||
active ? "text-white" : "text-gray-400"
|
||||
}`
|
||||
}
|
||||
value={option.value}
|
||||
<div className="relative mt-1">
|
||||
<Listbox value={value || ""} onChange={onChange}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Button
|
||||
id={id}
|
||||
className="relative h-10 w-full cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm"
|
||||
>
|
||||
<span
|
||||
className={twMerge("block truncate", !value && "opacity-50")}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<FontAwesomeIcon icon={faCheck} aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
{selectedOption.label}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2.5">
|
||||
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true" />
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-xl focus:outline-none sm:text-sm">
|
||||
{options.map((option, itemIdx) => (
|
||||
<Listbox.Option
|
||||
key={itemIdx}
|
||||
className={({ active, selected }) =>
|
||||
twMerge(
|
||||
"relative cursor-pointer select-none py-2.5 pl-8 pr-2 text-white transition-all duration-150 ease-in-out",
|
||||
active && "bg-brand-primary/20 text-white",
|
||||
selected && "bg-brand-primary/40",
|
||||
!selected && "text-white/90",
|
||||
)
|
||||
}
|
||||
value={option.value}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,11 +27,17 @@ const DialogBackdrop = ({ className }: { className?: string }) => {
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave="ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className={twMerge("fixed inset-0 bg-black/60", className)} />
|
||||
<div
|
||||
className={twMerge("fixed inset-0 bg-black/60", className)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Transition.Child>
|
||||
);
|
||||
};
|
||||
@@ -71,7 +77,7 @@ export const TransitionDialogue = ({
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enter="ease-out duration-200"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
@@ -86,6 +92,10 @@ export const TransitionDialogue = ({
|
||||
className,
|
||||
)}
|
||||
style={{ minWidth: width }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{title && (
|
||||
<Dialog.Title
|
||||
|
||||
@@ -35,13 +35,15 @@ export const H4 = ({ className, children }: TypoProps) => (
|
||||
export const Label = ({
|
||||
className,
|
||||
children,
|
||||
required,
|
||||
...rest
|
||||
}: LabelHTMLAttributes<HTMLLabelElement>) => (
|
||||
}: LabelHTMLAttributes<HTMLLabelElement> & { required?: boolean }) => (
|
||||
<label
|
||||
className={twMerge(baseTypo, "mb-2 text-[15px] font-medium", className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{required && <span className="ml-0.5 text-[#ff6467]">*</span>}
|
||||
</label>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FileUploader as DragDropFilesUploader } from "react-drag-drop-files";
|
||||
import { DragAndDropZone } from "./DragAndDropZone";
|
||||
import { Label } from "../Typography";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -10,8 +11,8 @@ interface Props {
|
||||
|
||||
export const FileUploader = ({ fileTypes, title, file, setFile }: Props) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
{title}
|
||||
<div className="flex w-full flex-col">
|
||||
<Label required>{title}</Label>
|
||||
<DragDropFilesUploader
|
||||
handleChange={(file: File | null) => {
|
||||
setFile(file);
|
||||
|
||||
+31
-20
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
|
||||
import { Button, H6, Input, ListDropdown } from "~/components";
|
||||
import { Button, H6, Input, Label, ListDropdown } from "~/components";
|
||||
import { FileUploader } from "../UploadModal/FileUploader";
|
||||
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "./utilities";
|
||||
import { upload3DObjects } from "./utilities/upload3DObjects";
|
||||
import { UploaderState } from "~/models";
|
||||
import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCube } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -21,7 +22,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UploadFiles3D = ({
|
||||
title,
|
||||
fileTypes,
|
||||
engineCategory,
|
||||
options,
|
||||
@@ -121,16 +121,19 @@ export const UploadFiles3D = ({
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex flex-col gap-4">
|
||||
<Input
|
||||
placeholder="Enter the title here"
|
||||
errorMessage={uploadTitle.error}
|
||||
value={uploadTitle.value}
|
||||
onChange={(event) => setUploadTitle({ value: event.target.value })}
|
||||
className={uploadTitle.error ? "mb-3" : ""}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Label required>Asset Name</Label>
|
||||
<Input
|
||||
placeholder="Enter the asset name here"
|
||||
errorMessage={uploadTitle.error}
|
||||
value={uploadTitle.value}
|
||||
onChange={(event) => setUploadTitle({ value: event.target.value })}
|
||||
className={uploadTitle.error ? "mb-3" : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FileUploader
|
||||
title={title}
|
||||
title="Upload 3D Model"
|
||||
fileTypes={fileTypes}
|
||||
file={assetFile.value}
|
||||
setFile={(file: File | null) => {
|
||||
@@ -149,16 +152,20 @@ export const UploadFiles3D = ({
|
||||
<H6 className="z-10 text-red">{assetFile.error}</H6>
|
||||
)}
|
||||
|
||||
<div className="relative m-auto aspect-square w-full overflow-hidden rounded-lg bg-brand-secondary">
|
||||
<canvas className="h-full w-full" ref={canvasCallbackRef} />
|
||||
<div className="relative m-auto w-full overflow-hidden rounded-lg bg-brand-secondary">
|
||||
<canvas
|
||||
className="pointer-events-none h-full w-full"
|
||||
ref={canvasCallbackRef}
|
||||
/>
|
||||
{!assetFile.value && (
|
||||
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
|
||||
File Preview
|
||||
<H6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50">
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
Your model preview will appear here
|
||||
</H6>
|
||||
)}
|
||||
{previewStatus.type.includes("Error") && (
|
||||
<>
|
||||
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
|
||||
<H6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 w-full text-center">
|
||||
{previewStatus.type}
|
||||
{previewStatus.message && <br />}
|
||||
{previewStatus.message}
|
||||
@@ -167,13 +174,17 @@ export const UploadFiles3D = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
Upload
|
||||
</Button>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!uploadTitle.value || !assetFile.value}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
+116
-17
@@ -1,20 +1,31 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TransitionDialogue, LoadingDots } from "~/components";
|
||||
import {
|
||||
TransitionDialogue,
|
||||
LoadingDots,
|
||||
Label,
|
||||
CloseButton,
|
||||
} from "~/components";
|
||||
|
||||
import { UploadAssetError } from "../UploadModal/UploadAssetError";
|
||||
import { UploadSuccess } from "../UploadModal/UploadSuccess";
|
||||
import { UploadFiles3D } from "./UploadFiles3D";
|
||||
import { FilterEngineCategories, UploaderStates } from "~/enums";
|
||||
import { initialUploaderState, UploaderState } from "~/models";
|
||||
import { Select } from "../Select";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
UploaderStates,
|
||||
OBJECT_FILE_TYPE,
|
||||
CHARACTER_MIXAMO_FILE_TYPE,
|
||||
IMAGEPLANE_FILE_TYPE,
|
||||
} from "~/enums";
|
||||
import { assetModalVisible } from "~/pages/PageEnigma/signals";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
fileTypes: string[];
|
||||
engineCategory: FilterEngineCategories;
|
||||
options?: {
|
||||
fileSubtypes?: { [key: string]: string }[];
|
||||
hasLength?: boolean;
|
||||
@@ -22,17 +33,61 @@ interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
// Map categories to their allowed file types
|
||||
const categoryFileTypes: Partial<Record<FilterEngineCategories, string[]>> = {
|
||||
[FilterEngineCategories.OBJECT]: Object.values(OBJECT_FILE_TYPE),
|
||||
[FilterEngineCategories.CHARACTER]: [
|
||||
...Object.values(CHARACTER_MIXAMO_FILE_TYPE),
|
||||
],
|
||||
[FilterEngineCategories.IMAGE_PLANE]: Object.values(IMAGEPLANE_FILE_TYPE),
|
||||
[FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE),
|
||||
[FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE),
|
||||
};
|
||||
|
||||
// Map engine categories to asset modal tabs
|
||||
const categoryToAssetTab: Partial<Record<FilterEngineCategories, string>> = {
|
||||
[FilterEngineCategories.OBJECT]: "objects",
|
||||
[FilterEngineCategories.CHARACTER]: "character",
|
||||
[FilterEngineCategories.IMAGE_PLANE]: "image-planes",
|
||||
[FilterEngineCategories.LOCATION]: "sets",
|
||||
[FilterEngineCategories.CREATURE]: "creatures",
|
||||
};
|
||||
|
||||
// Excluded category options for the upload modal
|
||||
const categoryOptions = Object.entries(FilterEngineCategories)
|
||||
.filter(
|
||||
([, value]) =>
|
||||
![
|
||||
"audio",
|
||||
"video_plane",
|
||||
"skybox",
|
||||
"set_dressing",
|
||||
"expression",
|
||||
"animation",
|
||||
"scene",
|
||||
].includes(value),
|
||||
)
|
||||
.sort(([, a], [, b]) => (a === "object" ? -1 : b === "object" ? 1 : 0))
|
||||
.map(([key, value]) => ({
|
||||
// This is because sets is location i guess - BFlat
|
||||
label:
|
||||
key === "LOCATION"
|
||||
? "Set"
|
||||
: key.charAt(0) + key.slice(1).toLowerCase().replace(/_/g, " "),
|
||||
value: value,
|
||||
}));
|
||||
|
||||
export function UploadModal3D({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
title,
|
||||
fileTypes,
|
||||
engineCategory,
|
||||
options,
|
||||
}: Props) {
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
useState<FilterEngineCategories>(FilterEngineCategories.OBJECT);
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
@@ -52,14 +107,35 @@ export function UploadModal3D({
|
||||
switch (uploaderState.status) {
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<UploadFiles3D
|
||||
title={title}
|
||||
engineCategory={engineCategory}
|
||||
fileTypes={fileTypes}
|
||||
options={options}
|
||||
onClose={onClose}
|
||||
onUploadProgress={updateUploaderState}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="flex w-full flex-col">
|
||||
<Label>Category</Label>
|
||||
<Select
|
||||
id="category-select"
|
||||
options={categoryOptions}
|
||||
value={selectedCategory}
|
||||
onChange={(value) => {
|
||||
if (
|
||||
typeof value === "string" &&
|
||||
Object.values(FilterEngineCategories).includes(
|
||||
value as FilterEngineCategories,
|
||||
)
|
||||
) {
|
||||
setSelectedCategory(value as FilterEngineCategories);
|
||||
}
|
||||
}}
|
||||
placeholder="Select a category"
|
||||
/>
|
||||
</div>
|
||||
<UploadFiles3D
|
||||
title={title}
|
||||
engineCategory={selectedCategory}
|
||||
fileTypes={categoryFileTypes[selectedCategory] || []}
|
||||
options={options}
|
||||
onClose={onClose}
|
||||
onUploadProgress={updateUploaderState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case UploaderStates.uploadingAsset:
|
||||
case UploaderStates.uploadingCover:
|
||||
@@ -73,10 +149,25 @@ export function UploadModal3D({
|
||||
case UploaderStates.success:
|
||||
return (
|
||||
<UploadSuccess
|
||||
title={title}
|
||||
title="3D model"
|
||||
onOk={() => {
|
||||
onSuccess();
|
||||
onClose();
|
||||
// Store the selected category and tab in sessionStorage
|
||||
const assetTab = categoryToAssetTab[selectedCategory];
|
||||
if (assetTab) {
|
||||
sessionStorage.setItem(
|
||||
"lastUploadedCategory",
|
||||
selectedCategory,
|
||||
);
|
||||
sessionStorage.setItem("lastUploadedTab", assetTab);
|
||||
// Reopen the asset modal
|
||||
assetModalVisible.value = true;
|
||||
} else {
|
||||
// Clear any existing stored values if we don't have a valid tab
|
||||
sessionStorage.removeItem("lastUploadedCategory");
|
||||
sessionStorage.removeItem("lastUploadedTab");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -87,7 +178,7 @@ export function UploadModal3D({
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={FilterEngineCategories.OBJECT}
|
||||
type={selectedCategory}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
@@ -104,10 +195,18 @@ export function UploadModal3D({
|
||||
/>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<TransitionDialogue isOpen={isOpen} onClose={onClose} title={title}>
|
||||
<TransitionDialogue
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
className="max-w-xl"
|
||||
showClose={false}
|
||||
>
|
||||
<CloseButton className="absolute right-4 top-4" onClick={onClose} />
|
||||
<UploaderModalContent />
|
||||
</TransitionDialogue>
|
||||
);
|
||||
|
||||
+49
-32
@@ -22,7 +22,14 @@ export const loadPreviewOnCanvas = ({
|
||||
}) => {
|
||||
// Setup of scene, camera, and renderer in the canvas
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
|
||||
|
||||
// Calculate proper aspect ratio from canvas dimensions
|
||||
const width = canvas.getBoundingClientRect().width || 0;
|
||||
const height = canvas.getBoundingClientRect().height || 0;
|
||||
const aspectRatio = width / height;
|
||||
|
||||
// Reduce FOV to make objects appear larger
|
||||
const camera = new THREE.PerspectiveCamera(35, aspectRatio, 0.1, 1000);
|
||||
camera.position.z = 2;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
@@ -32,14 +39,16 @@ export const loadPreviewOnCanvas = ({
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
|
||||
renderer.setSize(
|
||||
canvas.getBoundingClientRect().width || 0,
|
||||
canvas.getBoundingClientRect().height || 0,
|
||||
);
|
||||
renderer.setSize(width, height);
|
||||
|
||||
const color = 0xfcece7;
|
||||
const light = new THREE.HemisphereLight(color, 0x8d8d8d, 3.0);
|
||||
|
||||
// Add some additional lighting to better show the model
|
||||
const frontLight = new THREE.DirectionalLight(0xffffff, 2);
|
||||
frontLight.position.set(0, 0, 10);
|
||||
scene.add(frontLight);
|
||||
|
||||
scene.add(light);
|
||||
|
||||
// load the file into the preview mini-scene depending of the file's type
|
||||
@@ -88,33 +97,41 @@ const glbLoader = ({
|
||||
child.userData["color"] = "#FFFFFF";
|
||||
scene.add(child);
|
||||
|
||||
let maxSize = 2;
|
||||
if (scene.children.length > 0) {
|
||||
scene.children.forEach((child) => {
|
||||
child.traverse((object: THREE.Object3D | THREE.Mesh) => {
|
||||
// Assuming `object` is your Three.js object and you know it's a Mesh
|
||||
if (object instanceof THREE.Mesh) {
|
||||
object.geometry.computeBoundingBox();
|
||||
const boundingBox = object.geometry.boundingBox;
|
||||
const center = new THREE.Vector3();
|
||||
boundingBox.getCenter(center);
|
||||
const dimensions = new THREE.Vector3();
|
||||
boundingBox.getSize(dimensions);
|
||||
const maxDim = Math.max(
|
||||
dimensions.x,
|
||||
dimensions.y,
|
||||
dimensions.z,
|
||||
);
|
||||
if (maxSize < maxDim) {
|
||||
maxSize = maxDim;
|
||||
camera.position.set(-maxDim, maxDim, maxDim);
|
||||
camera.lookAt(center);
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Calculate bounding box for all objects in the scene
|
||||
const box = new THREE.Box3();
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
object.geometry.computeBoundingBox();
|
||||
box.expandByObject(object);
|
||||
}
|
||||
});
|
||||
|
||||
// Get the center and size of the bounding box
|
||||
const center = new THREE.Vector3();
|
||||
const size = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
box.getSize(size);
|
||||
|
||||
// Calculate the radius of the bounding sphere
|
||||
const radius = Math.max(size.x, size.y, size.z) * 0.5;
|
||||
|
||||
// Position camera to fit the object in view with minimal padding
|
||||
const fov = camera.fov * (Math.PI / 180);
|
||||
const distance = (radius * 1.2) / Math.tan(fov * 0.5);
|
||||
|
||||
// Position camera at a better angle for preview
|
||||
camera.position.set(
|
||||
center.x + distance * 0.6,
|
||||
center.y + distance * 0.4,
|
||||
center.z + distance * 0.6,
|
||||
);
|
||||
camera.lookAt(center);
|
||||
|
||||
// Adjust the camera's near and far planes to ensure the model is visible
|
||||
camera.near = distance * 0.01;
|
||||
camera.far = distance * 100;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
renderer.render(scene, camera);
|
||||
statusCallback({
|
||||
type: "OK",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
|
||||
import { UploadModal3D } from "~/components/reusable/UploadModal3D";
|
||||
import {
|
||||
faPlus,
|
||||
faSearch,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
faChevronRight,
|
||||
faMountainCity,
|
||||
faDog,
|
||||
faImage,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, CloseButton, Input, Tooltip } from "~/components";
|
||||
@@ -26,14 +28,10 @@ import {
|
||||
} from "../../signals";
|
||||
import { MediaItem } from "../../models";
|
||||
import { useUserObjects, useFeaturedObjects } from "../SidePanelTabs/hooks";
|
||||
import { FilterEngineCategories } from "~/enums";
|
||||
import { FilterEngineCategories, FilterMediaType } from "~/enums";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
interface AssetModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
import { isAnyStatusFetching } from "../SidePanelTabs/utilities";
|
||||
|
||||
type AssetTab = {
|
||||
id: string;
|
||||
@@ -80,6 +78,7 @@ export const AssetModal = () => {
|
||||
const [activeAssetTab, setActiveAssetTab] = useState("all");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [reopenAfterAdd, setReopenAfterAdd] = useState(false);
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -93,116 +92,219 @@ export const AssetModal = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (assetModalVisible.value) {
|
||||
// Check for stored category and tab from upload
|
||||
const lastUploadedTab = sessionStorage.getItem("lastUploadedTab");
|
||||
if (lastUploadedTab) {
|
||||
setActiveLibraryTab("mine");
|
||||
setActiveAssetTab(lastUploadedTab);
|
||||
// Clear the stored values
|
||||
sessionStorage.removeItem("lastUploadedTab");
|
||||
sessionStorage.removeItem("lastUploadedCategory");
|
||||
}
|
||||
// Small delay to ensure the modal is fully mounted
|
||||
const timer = setTimeout(() => {
|
||||
searchInputRef.current?.focus();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [assetModalVisible.value]);
|
||||
|
||||
// Fetch objects for different categories
|
||||
const { userObjects: userCharacters } = useUserObjects({
|
||||
const {
|
||||
userObjects: userCharacters,
|
||||
userFetchStatus: userCharactersFetchStatus,
|
||||
fetchUserObjects: fetchUserCharacters,
|
||||
} = useUserObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.CHARACTER],
|
||||
defaultErrorMessage: "Error fetching user characters",
|
||||
});
|
||||
|
||||
const { userObjects: userObjects } = useUserObjects({
|
||||
const {
|
||||
userObjects: userObjects,
|
||||
userFetchStatus: userObjectsFetchStatus,
|
||||
fetchUserObjects: fetchUserObjects,
|
||||
} = useUserObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.OBJECT],
|
||||
defaultErrorMessage: "Error fetching user objects",
|
||||
});
|
||||
|
||||
const { featuredObjects: featuredCharacters } = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.CHARACTER],
|
||||
defaultErrorMessage: "Error fetching featured characters",
|
||||
});
|
||||
|
||||
const { featuredObjects: featuredObjects } = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.OBJECT],
|
||||
defaultErrorMessage: "Error fetching featured objects",
|
||||
});
|
||||
|
||||
const { userObjects: userSets } = useUserObjects({
|
||||
const {
|
||||
userObjects: userSets,
|
||||
userFetchStatus: userSetsFetchStatus,
|
||||
fetchUserObjects: fetchUserSets,
|
||||
} = useUserObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.LOCATION],
|
||||
defaultErrorMessage: "Error fetching user sets",
|
||||
});
|
||||
|
||||
const { featuredObjects: featuredSets } = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.LOCATION],
|
||||
defaultErrorMessage: "Error fetching featured sets",
|
||||
});
|
||||
|
||||
const { userObjects: userCreatures } = useUserObjects({
|
||||
const {
|
||||
userObjects: userCreatures,
|
||||
userFetchStatus: userCreaturesFetchStatus,
|
||||
fetchUserObjects: fetchUserCreatures,
|
||||
} = useUserObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.CREATURE],
|
||||
defaultErrorMessage: "Error fetching user creatures",
|
||||
});
|
||||
|
||||
const { featuredObjects: featuredCreatures } = useFeaturedObjects({
|
||||
const {
|
||||
userObjects: userImagePlanes,
|
||||
userFetchStatus: userImagePlanesFetchStatus,
|
||||
fetchUserObjects: fetchUserImagePlanes,
|
||||
} = useUserObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.IMAGE_PLANE],
|
||||
defaultErrorMessage: "Error fetching user image planes",
|
||||
});
|
||||
|
||||
const {
|
||||
featuredObjects: featuredCharacters,
|
||||
featuredFetchStatus: featuredCharactersFetchStatus,
|
||||
} = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.CHARACTER],
|
||||
filterMediaTypes: [FilterMediaType.GLB],
|
||||
defaultErrorMessage: "Error fetching featured characters",
|
||||
});
|
||||
|
||||
const {
|
||||
featuredObjects: featuredObjects,
|
||||
featuredFetchStatus: featuredObjectsFetchStatus,
|
||||
} = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.OBJECT],
|
||||
defaultErrorMessage: "Error fetching featured objects",
|
||||
});
|
||||
|
||||
const {
|
||||
featuredObjects: featuredSets,
|
||||
featuredFetchStatus: featuredSetsFetchStatus,
|
||||
} = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.LOCATION],
|
||||
defaultErrorMessage: "Error fetching featured sets",
|
||||
});
|
||||
|
||||
const {
|
||||
featuredObjects: featuredCreatures,
|
||||
featuredFetchStatus: featuredCreaturesFetchStatus,
|
||||
} = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.CREATURE],
|
||||
defaultErrorMessage: "Error fetching featured creatures",
|
||||
});
|
||||
|
||||
const {
|
||||
featuredObjects: featuredImagePlanes,
|
||||
featuredFetchStatus: featuredImagePlanesFetchStatus,
|
||||
} = useFeaturedObjects({
|
||||
filterEngineCategories: [FilterEngineCategories.IMAGE_PLANE],
|
||||
defaultErrorMessage: "Error fetching featured image planes",
|
||||
});
|
||||
|
||||
const isFetching = isAnyStatusFetching([
|
||||
userCharactersFetchStatus,
|
||||
userObjectsFetchStatus,
|
||||
userSetsFetchStatus,
|
||||
userCreaturesFetchStatus,
|
||||
userImagePlanesFetchStatus,
|
||||
featuredCharactersFetchStatus,
|
||||
featuredObjectsFetchStatus,
|
||||
featuredSetsFetchStatus,
|
||||
featuredCreaturesFetchStatus,
|
||||
featuredImagePlanesFetchStatus,
|
||||
]);
|
||||
|
||||
const libraryTabs = [
|
||||
{ id: "library", label: "Library" },
|
||||
{ id: "mine", label: "Mine" },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const assetTabs: AssetTab[] = [
|
||||
{ id: "all", label: "All", icon: faLayerGroup, items: [] },
|
||||
{
|
||||
id: "character",
|
||||
label: "Character",
|
||||
icon: faUser,
|
||||
engineCategory: FilterEngineCategories.CHARACTER,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? [...demoCharacterItems.value, ...(featuredCharacters ?? [])]
|
||||
: (userCharacters ?? []),
|
||||
},
|
||||
{
|
||||
id: "objects",
|
||||
label: "Objects",
|
||||
icon: faCube,
|
||||
engineCategory: FilterEngineCategories.OBJECT,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? [...demoShapeItems.value, ...(featuredObjects ?? [])]
|
||||
: (userObjects ?? []),
|
||||
},
|
||||
{
|
||||
id: "sets",
|
||||
label: "Sets",
|
||||
icon: faMountainCity,
|
||||
engineCategory: FilterEngineCategories.LOCATION,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? (featuredSets ?? [])
|
||||
: (userSets ?? []),
|
||||
},
|
||||
{
|
||||
id: "creatures",
|
||||
label: "Creatures",
|
||||
icon: faDog,
|
||||
engineCategory: FilterEngineCategories.CREATURE,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? (featuredCreatures ?? [])
|
||||
: (userCreatures ?? []),
|
||||
},
|
||||
{
|
||||
id: "skybox",
|
||||
label: "Skybox",
|
||||
icon: faSun,
|
||||
items: demoSkyboxItems.value,
|
||||
},
|
||||
];
|
||||
const assetTabs = useMemo<AssetTab[]>(
|
||||
() => [
|
||||
{ id: "all", label: "All", icon: faLayerGroup, items: [] },
|
||||
{
|
||||
id: "character",
|
||||
label: "Character",
|
||||
icon: faUser,
|
||||
engineCategory: FilterEngineCategories.CHARACTER,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? [...demoCharacterItems.value, ...(featuredCharacters ?? [])]
|
||||
: (userCharacters ?? []),
|
||||
},
|
||||
{
|
||||
id: "objects",
|
||||
label: "Objects",
|
||||
icon: faCube,
|
||||
engineCategory: FilterEngineCategories.OBJECT,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? [...demoShapeItems.value, ...(featuredObjects ?? [])]
|
||||
: (userObjects ?? []),
|
||||
},
|
||||
{
|
||||
id: "sets",
|
||||
label: "Sets",
|
||||
icon: faMountainCity,
|
||||
engineCategory: FilterEngineCategories.LOCATION,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? (featuredSets ?? [])
|
||||
: (userSets ?? []),
|
||||
},
|
||||
{
|
||||
id: "creatures",
|
||||
label: "Creatures",
|
||||
icon: faDog,
|
||||
engineCategory: FilterEngineCategories.CREATURE,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? (featuredCreatures ?? [])
|
||||
: (userCreatures ?? []),
|
||||
},
|
||||
{
|
||||
id: "skybox",
|
||||
label: "Skybox",
|
||||
icon: faSun,
|
||||
items: activeLibraryTab === "library" ? demoSkyboxItems.value : [],
|
||||
},
|
||||
{
|
||||
id: "image-planes",
|
||||
label: "Image Planes",
|
||||
icon: faImage,
|
||||
engineCategory: FilterEngineCategories.IMAGE_PLANE,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? (featuredImagePlanes ?? [])
|
||||
: (userImagePlanes ?? []),
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
activeLibraryTab,
|
||||
demoCharacterItems.value,
|
||||
demoShapeItems.value,
|
||||
demoSkyboxItems.value,
|
||||
featuredCharacters,
|
||||
featuredCreatures,
|
||||
featuredObjects,
|
||||
featuredSets,
|
||||
featuredImagePlanes,
|
||||
userCharacters,
|
||||
userCreatures,
|
||||
userObjects,
|
||||
userSets,
|
||||
userImagePlanes,
|
||||
],
|
||||
);
|
||||
|
||||
// Combine all items for the "All" tab
|
||||
const allItems = useMemo(() => {
|
||||
return assetTabs
|
||||
const items = assetTabs
|
||||
.filter((tab) => tab.id !== "all")
|
||||
.flatMap((tab) => tab.items);
|
||||
// console.log("All items:", items);
|
||||
// console.log("Active library tab:", activeLibraryTab);
|
||||
// console.log("User creatures:", userCreatures);
|
||||
// console.log("User objects:", userObjects);
|
||||
return items;
|
||||
}, [assetTabs]);
|
||||
|
||||
// Update the items array for the "All" tab
|
||||
@@ -238,11 +340,31 @@ export const AssetModal = () => {
|
||||
setSearchTerm("");
|
||||
}, [activeAssetTab]);
|
||||
|
||||
// Refresh user content when switching to mine tab
|
||||
useEffect(() => {
|
||||
if (activeLibraryTab === "mine") {
|
||||
// Trigger a refetch of user content
|
||||
fetchUserCharacters();
|
||||
fetchUserObjects();
|
||||
fetchUserSets();
|
||||
fetchUserCreatures();
|
||||
fetchUserImagePlanes();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeLibraryTab]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (activeAssetTab === "all" && !searchTerm) {
|
||||
// Filter out empty categories when in "mine" tab
|
||||
const tabsToShow = assetTabs
|
||||
.slice(1)
|
||||
.filter(
|
||||
(tab) => activeLibraryTab === "library" || tab.items.length > 0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full space-y-2 overflow-y-auto">
|
||||
{assetTabs.slice(1).map((tab) => (
|
||||
{tabsToShow.map((tab) => (
|
||||
<AllTabSection
|
||||
key={tab.id}
|
||||
label={tab.label}
|
||||
@@ -257,13 +379,18 @@ export const AssetModal = () => {
|
||||
return (
|
||||
<ItemElements
|
||||
items={displayedItems}
|
||||
busy={false}
|
||||
busy={isFetching}
|
||||
debug={`asset-modal-${currentTab.id}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddAsset = () => {
|
||||
assetModalVisible.value = false;
|
||||
setIsUploadModalOpen(true);
|
||||
};
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
if (reopenAfterAdd) {
|
||||
// Small delay to allow the modal to close and reopen
|
||||
setTimeout(() => {
|
||||
@@ -277,113 +404,129 @@ export const AssetModal = () => {
|
||||
const clearSearch = () => setSearchTerm("");
|
||||
|
||||
return (
|
||||
<TransitionDialogue
|
||||
isOpen={assetModalVisible.value && assetModalVisibleDuringDrag.value}
|
||||
onClose={handleClose}
|
||||
className="h-[640px] max-w-4xl"
|
||||
childPadding={false}
|
||||
>
|
||||
<div className="grid h-full grid-cols-12 gap-3">
|
||||
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:dark:bg-white/10">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">3D Assets</h2>
|
||||
<Tooltip content="Upload model" position="top" delay={200}>
|
||||
<Button
|
||||
className="h-6 w-6 rounded-full border-none bg-transparent text-white/70 transition-colors hover:bg-transparent hover:text-white/100"
|
||||
onClick={handleAddAsset}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-2">
|
||||
{assetTabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.id}
|
||||
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
|
||||
className={twMerge(
|
||||
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
|
||||
activeAssetTab === tab.id &&
|
||||
"border-brand-primary bg-brand-primary/10 hover:bg-brand-primary/10",
|
||||
)}
|
||||
onClick={() => setActiveAssetTab(tab.id)}
|
||||
>
|
||||
<FontAwesomeIcon icon={tab.icon} className="mr-2 opacity-70" />
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-auto flex items-center gap-2 pt-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="reopen-after-add"
|
||||
checked={reopenAfterAdd}
|
||||
onChange={(e) => setReopenAfterAdd(e.target.checked)}
|
||||
className="h-4 w-4 rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary"
|
||||
/>
|
||||
<label htmlFor="reopen-after-add" className="text-sm text-white/70">
|
||||
Reopen after adding
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-9 p-3 pb-0 ps-0 pt-2">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="h-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<TabSelector
|
||||
tabs={libraryTabs}
|
||||
activeTab={activeLibraryTab}
|
||||
onTabChange={setActiveLibraryTab}
|
||||
className="w-auto"
|
||||
/>
|
||||
<div className="relative grow">
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder="Search"
|
||||
className="grow"
|
||||
inputClassName="pr-2.5"
|
||||
icon={faSearch}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
iconClassName="text-white/60"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<CloseButton
|
||||
onClick={clearSearch}
|
||||
className="absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20"
|
||||
/>
|
||||
<>
|
||||
<TransitionDialogue
|
||||
isOpen={assetModalVisible.value && assetModalVisibleDuringDrag.value}
|
||||
onClose={handleClose}
|
||||
className="h-[640px] max-w-4xl"
|
||||
childPadding={false}
|
||||
>
|
||||
<div className="grid h-full grid-cols-12 gap-3">
|
||||
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:dark:bg-white/10">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">
|
||||
3D Assets
|
||||
</h2>
|
||||
<Tooltip content="Upload model" position="top" delay={200}>
|
||||
<Button
|
||||
className="h-6 w-6 rounded-full border-none bg-transparent text-white/70 transition-colors hover:bg-transparent hover:text-white/100"
|
||||
onClick={handleAddAsset}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-2">
|
||||
{assetTabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.id}
|
||||
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
|
||||
className={twMerge(
|
||||
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
|
||||
activeAssetTab === tab.id &&
|
||||
"border-brand-primary bg-brand-primary/10 hover:bg-brand-primary/10",
|
||||
)}
|
||||
</div>
|
||||
<CloseButton onClick={handleClose} />
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"overflow-auto-y mt-4 h-[574px]",
|
||||
activeAssetTab !== "all" && "h-[552px]",
|
||||
)}
|
||||
onClick={() => setActiveAssetTab(tab.id)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={tab.icon}
|
||||
className="mr-2 opacity-70"
|
||||
/>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-auto flex items-center gap-2 pt-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="reopen-after-add"
|
||||
checked={reopenAfterAdd}
|
||||
onChange={(e) => setReopenAfterAdd(e.target.checked)}
|
||||
className="h-4 w-4 rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary"
|
||||
/>
|
||||
<label
|
||||
htmlFor="reopen-after-add"
|
||||
className="text-sm text-white/70"
|
||||
>
|
||||
{activeAssetTab !== "all" && !searchTerm && (
|
||||
<div className="mb-2 flex items-center font-semibold">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
|
||||
onClick={() => setActiveAssetTab("all")}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronLeft}
|
||||
className="text-sm font-semibold opacity-70"
|
||||
Reopen after adding
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-9 p-3 pb-0 ps-0 pt-2">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="h-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<TabSelector
|
||||
tabs={libraryTabs}
|
||||
activeTab={activeLibraryTab}
|
||||
onTabChange={setActiveLibraryTab}
|
||||
className="w-auto"
|
||||
/>
|
||||
<div className="relative grow">
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder="Search"
|
||||
className="grow"
|
||||
inputClassName="pr-2.5"
|
||||
icon={faSearch}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
iconClassName="text-white/60"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<CloseButton
|
||||
onClick={clearSearch}
|
||||
className="absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20"
|
||||
/>
|
||||
</Button>
|
||||
{currentTab.label}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{renderContent()}
|
||||
<CloseButton onClick={handleClose} />
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"overflow-auto-y mt-4 h-[574px]",
|
||||
activeAssetTab !== "all" && "h-[552px]",
|
||||
)}
|
||||
>
|
||||
{activeAssetTab !== "all" && !searchTerm && (
|
||||
<div className="mb-2 flex items-center font-semibold">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
|
||||
onClick={() => setActiveAssetTab("all")}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronLeft}
|
||||
className="text-sm font-semibold opacity-70"
|
||||
/>
|
||||
</Button>
|
||||
{currentTab.label}
|
||||
</div>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
</TransitionDialogue>
|
||||
<UploadModal3D
|
||||
onClose={() => setIsUploadModalOpen(false)}
|
||||
onSuccess={handleUploadSuccess}
|
||||
isOpen={isUploadModalOpen}
|
||||
title="Upload 3D Asset"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@mediapipe/tasks-vision": "^0.10.20",
|
||||
"@netlify/edge-functions": "^2.3.1",
|
||||
"@netlify/remix-edge-adapter": "^3.2.0",
|
||||
|
||||
Generated
+11
-35
@@ -13,6 +13,7 @@
|
||||
"libs/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@remix-run/serve": "^2.15.0",
|
||||
"pnpm": "^10.8.1",
|
||||
@@ -79,7 +80,6 @@
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@headlessui/react": "^2.1.3",
|
||||
"@preact/signals-react": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
@@ -582,25 +582,6 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"apps/editor2d/node_modules/@headlessui/react": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.1.tgz",
|
||||
"integrity": "sha512-daiUqVLae8CKVjEVT19P/izW0aGK0GNhMSAeMlrDebKmoVZHcRRwbxzgtnEadUVDXyBsWo9/UH4KHeniO+0tMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
"@react-aria/focus": "^3.17.1",
|
||||
"@react-aria/interactions": "^3.21.3",
|
||||
"@tanstack/react-virtual": "^3.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"apps/editor2d/node_modules/@preact/signals-react": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@preact/signals-react/-/signals-react-2.3.0.tgz",
|
||||
@@ -1171,7 +1152,6 @@
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@mediapipe/tasks-vision": "^0.10.20",
|
||||
"@netlify/edge-functions": "^2.3.1",
|
||||
"@netlify/remix-edge-adapter": "^3.2.0",
|
||||
@@ -4769,20 +4749,22 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.7.19",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz",
|
||||
"integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==",
|
||||
"license": "MIT",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.2.tgz",
|
||||
"integrity": "sha512-zbniWOYBQ8GHSUIOPY7BbdIn6PzUOq0z41RFrF30HbjsxG6Rrfk+6QulR8Kgf2Vwj2a/rE6i62q5vo+2gI5dJA==",
|
||||
"dependencies": {
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.60",
|
||||
"client-only": "^0.0.1"
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
"@react-aria/focus": "^3.17.1",
|
||||
"@react-aria/interactions": "^3.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.6",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16 || ^17 || ^18",
|
||||
"react-dom": "^16 || ^17 || ^18"
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
@@ -14869,12 +14851,6 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"scripts": {},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@remix-run/serve": "^2.15.0",
|
||||
"pnpm": "^10.8.1",
|
||||
|
||||
@@ -5,12 +5,20 @@ set -euxo pipefail
|
||||
# Tauri doesn't let you configure the frontend project directory statically, though they do provide an
|
||||
# environment variable to pass it to the CLI. Without doing this, the tauri cli randomly walks the
|
||||
# filesystem and finds the wrong frontend code.
|
||||
|
||||
# Kill any process running on port 5741
|
||||
if lsof -i tcp:5741 &>/dev/null; then
|
||||
lsof -i tcp:5741 -t | xargs kill -9
|
||||
echo "Killed process running on port 5741"
|
||||
else
|
||||
echo "No process running on port 5741"
|
||||
fi
|
||||
# TODO(bt,2025-02-13): This is not the correct way to get the root dir
|
||||
brew install pkg-config pixman cairo pango libpng jpeg giflib librsvg
|
||||
root_dir=$(pwd)
|
||||
frontend_path="${root_dir}/frontend"
|
||||
|
||||
pushd "${frontend_path}"
|
||||
|
||||
#nvm use stable
|
||||
npm install
|
||||
popd
|
||||
@@ -20,5 +28,5 @@ export TAURI_APP_PATH="${root_dir}/crates/desktop/tauri-artcraft"
|
||||
|
||||
export CONFIG_PATH="${TAURI_APP_PATH}/tauri.artcraft_2d.conf.toml"
|
||||
|
||||
#cargo tauri dev --no-watch -- --config "${CONFIG_PATH}"
|
||||
# TODO: --no-watch
|
||||
cargo tauri dev --config "${CONFIG_PATH}"
|
||||
|
||||
@@ -18,6 +18,7 @@ popd
|
||||
export TAURI_FRONTEND_PATH="${frontend_path}"
|
||||
export TAURI_APP_PATH="${root_dir}/crates/desktop/tauri-artcraft"
|
||||
|
||||
export CONFIG_PATH="${TAURI_APP_PATH}/tauri.artcraft_2d.conf.json"
|
||||
export CONFIG_PATH="${TAURI_APP_PATH}/tauri.artcraft_3d.conf.toml"
|
||||
|
||||
cargo tauri dev --config "${TAURI_APP_PATH}/tauri.artcraft_2d.conf.json" -- --no-watch
|
||||
# TODO: --no-watch
|
||||
cargo tauri dev --config "${CONFIG_PATH}"
|
||||
|
||||
Reference in New Issue
Block a user