Feature/load scene (#285)

This PR removes the dummy data for Load Scene and uses real data
instead.

Co-authored-by: Quicksticks-oss <chongobongotoday@gmail.com>
This commit is contained in:
FromTheLOLAshesandQuicksticks-oss authored and GitHub committed 2024-05-01 13:38:02 -04:00
1 parent 79b11ff0e8
commit 2c460e4e63
12 files changed
+252 -197

No files matched your search

+7 -4
View File
@@ -1,17 +1,20 @@
import MakeRequest from "../MakeRequest";
import { MediaFile, Pagination } from "~/pages/PageEnigma/models";
import { MediaInfo } from "~/pages/PageEnigma/models";
export interface GetMediaRequest {}
export interface GetMediaListResponse {
pagination: Pagination;
success: boolean;
results: MediaFile[];
results: MediaInfo[];
}
export interface Pagination {}
export interface GetMediaParams {
page_index: number;
filter_media_type: string;
page_index?: number;
filter_media_type?: string;
filter_engine_categories?: string;
}
export const GetMediaByUser = MakeRequest<
+27
View File
@@ -0,0 +1,27 @@
import { twMerge } from "tailwind-merge";
interface LoadingSpinnerProps {
className?: string;
}
export const LoadingSpinner = ({ className }: LoadingSpinnerProps) => {
return (
<svg
className={twMerge("h-6 w-6 animate-spin", className)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
);
};
+25 -27
View File
@@ -86,33 +86,31 @@ export const TransitionDialogue = ({
<Dialog.Title
as="div"
className="mb-5 flex justify-between p-5 pb-0 text-xl font-bold text-white">
{titleIcon && (
<>
{onTitleIconClick ? (
<button
className="flex items-center gap-3"
onClick={onTitleIconClick}>
{titleIcon && (
<FontAwesomeIcon
icon={titleIcon}
className={titleIconClassName}
/>
)}
{title}
</button>
) : (
<div className="flex items-center gap-3">
{titleIcon && (
<FontAwesomeIcon
icon={titleIcon}
className={titleIconClassName}
/>
)}
{title}
</div>
)}
</>
)}
<>
{onTitleIconClick ? (
<button
className="flex items-center gap-3"
onClick={onTitleIconClick}>
{titleIcon && (
<FontAwesomeIcon
icon={titleIcon}
className={titleIconClassName}
/>
)}
{title}
</button>
) : (
<div className="flex items-center gap-3">
{titleIcon && (
<FontAwesomeIcon
icon={titleIcon}
className={titleIconClassName}
/>
)}
{title}
</div>
)}
</>
{showClose && (
<Tooltip position="top" content="Close">
<button
+2
View File
@@ -23,6 +23,7 @@ import { Pill } from "./Pill";
import { WaveformPlayer } from "./WaveformPlayer";
import UploadModal from "./UploadModal";
import { ErrorDialog } from "./ErrorDialog";
import { LoadingSpinner } from "./LoadingSpinner";
export {
Button,
@@ -52,6 +53,7 @@ export {
LoadingDots,
LoadingDotsBricks,
LoadingDotsTyping,
LoadingSpinner,
Pagination,
Pill,
WaveformPlayer,
@@ -9,12 +9,17 @@ import {
ErrorResponse,
GetJobStatusResponse,
JobState,
MediaFileType,
} from "~/pages/PageEnigma/models";
import { ToasterContext, ToastTypes } from "~/contexts/ToasterContext";
import { activeJobs, movies } from "~/pages/PageEnigma/store";
import { listMediaByUser } from "~/api";
import { AuthenticationContext } from "~/contexts/Authentication";
import { STORAGE_KEYS } from "~/contexts/Authentication/types";
import {
GetMediaByUser,
GetMediaListResponse,
} from "~/api/media_files/GetMediaByUser";
export async function GetInferenceJobStatus(
jobToken: string,
@@ -75,22 +80,14 @@ export function GetCompletedMovies(username?: string) {
if (!username) {
return;
}
const sessionToken = localStorage.getItem(STORAGE_KEYS.SESSION_TOKEN) || "";
fetch(
listMediaByUser(username) +
"?" +
new URLSearchParams({ filter_media_type: "video" }),
GetMediaByUser(
username,
{},
{
method: "GET",
headers: {
Accept: "application/json",
session: sessionToken,
},
// credentials: 'include'
filter_media_type: MediaFileType.Video,
},
)
.then((res) => res.json())
.then((res) => {
.then((res: GetMediaListResponse) => {
if (res.success && res.results) {
if (
JSON.stringify(movies.value) !==
@@ -95,7 +95,7 @@ export const ControlsTopButtons = () => {
label: "Cancel",
},
showClose: true,
className: "max-w-7xl w-auto",
className: "max-w-5xl",
},
},
{
@@ -113,7 +113,7 @@ export const ControlsTopButtons = () => {
label: "Cancel",
},
showClose: true,
className: "max-w-7xl w-auto",
className: "max-w-5xl",
},
},
{
@@ -1,99 +1,64 @@
import { useEffect, useRef, useState } from "react";
import {
useContext,
useEffect,
useRef,
useState,
useLayoutEffect,
} from "react";
import { ScenePicker, SceneTypes } from "../ScenePicker";
import { Label } from "~/components";
import { Label, LoadingSpinner } from "~/components";
import {
GetMediaByUser,
GetMediaListResponse,
} from "~/api/media_files/GetMediaByUser";
import { MediaFileType } from "~/pages/PageEnigma/models";
import { AuthenticationContext } from "~/contexts/Authentication";
import dayjs from "dayjs";
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 [scenes, setScenes] = useState<SceneTypes[]>([]);
const { authState } = useContext(AuthenticationContext);
const sceneLoading = useRef(false);
const [isSceneLoading, setIsSceneLoading] = useState(true);
useEffect(() => {
if (!authState.userInfo || scenes.length || sceneLoading.current) {
return;
}
sceneLoading.current = true;
GetMediaByUser(
authState.userInfo.username,
{},
{
filter_engine_categories: MediaFileType.Scene,
},
)
.then((res: GetMediaListResponse) => {
if (res.success && res.results) {
setScenes(
res.results.map((scene) => ({
token: scene.token,
name: scene.maybe_title ?? "Untitled",
updated_at: dayjs(scene.updated_at).format(
"MMM D, YYYY HH:mm:ss",
),
thumbnail: scene.public_bucket_path,
})),
);
setIsSceneLoading(false);
}
})
.catch(() => {
return {
success: false,
error_reason: "Unknown error",
};
});
}, [scenes, authState.userInfo]);
const handleSceneSelect = (selectedScene: SceneTypes) => {
onSceneSelect(selectedScene.token);
@@ -105,14 +70,14 @@ export const LoadScene = ({ onSceneSelect }: LoadSceneProps) => {
const element = scrollContainerRef.current;
if (element) {
const atBottom =
element.scrollHeight - element.scrollTop === element.clientHeight;
element.scrollHeight - element.scrollTop <= element.clientHeight + 1;
const hasOverflow = element.scrollHeight > element.clientHeight;
setBottomGradientOpacity(hasOverflow && !atBottom ? 1 : 0);
}
};
useEffect(() => {
useLayoutEffect(() => {
const element = scrollContainerRef.current;
if (element) {
handleScroll();
@@ -122,21 +87,38 @@ export const LoadScene = ({ onSceneSelect }: LoadSceneProps) => {
element.removeEventListener("scroll", handleScroll);
};
}
}, []);
}, [isSceneLoading]);
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="relative flex max-h-[500px] min-h-[140px] flex-col">
{isSceneLoading ? (
<div className="flex items-center justify-center gap-3 py-12">
<LoadingSpinner />
<span className="font-medium opacity-70">Loading scenes...</span>
</div>
) : (
<>
{scenes.length !== 0 ? (
<div
className="overflow-y-auto overflow-x-hidden"
ref={scrollContainerRef}>
<ScenePicker
scenes={scenes}
onSceneSelect={handleSceneSelect}
showDate={true}
/>
</div>
) : (
<div className="flex items-center justify-center gap-3 py-12">
<span className="font-medium opacity-50">
You have no saved scenes yet.
</span>
</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 }}
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useLayoutEffect, useRef, useState } from "react";
import { ScenePicker, SceneTypes } from "../ScenePicker";
interface NewSceneFromTemplateProps {
@@ -8,7 +8,7 @@ interface NewSceneFromTemplateProps {
export const NewSceneFromTemplate = ({
onSceneSelect,
}: NewSceneFromTemplateProps) => {
// Dummy Data - replace with API data
// Hard coded scenes
const dummyScenes: SceneTypes[] = [
{
token: "m_9pf7a7v0138zx58f4x6ejsehjcvfq6",
@@ -32,14 +32,14 @@ export const NewSceneFromTemplate = ({
const element = scrollContainerRef.current;
if (element) {
const atBottom =
element.scrollHeight - element.scrollTop === element.clientHeight;
element.scrollHeight - element.scrollTop <= element.clientHeight + 1;
const hasOverflow = element.scrollHeight > element.clientHeight;
setBottomGradientOpacity(hasOverflow && !atBottom ? 1 : 0);
}
};
useEffect(() => {
useLayoutEffect(() => {
const element = scrollContainerRef.current;
if (element) {
handleScroll();
@@ -53,7 +53,7 @@ export const NewSceneFromTemplate = ({
return (
<div className="flex flex-col gap-0.5">
<div className="relative flex max-h-[500px] flex-col">
<div className="relative flex max-h-[500px] min-h-[140px] flex-col">
<div
className="overflow-y-auto overflow-x-hidden"
ref={scrollContainerRef}>
@@ -0,0 +1,81 @@
import React from "react";
import { twMerge } from "tailwind-merge";
export type SceneTypes = {
token: string;
name: string;
updated_at?: string;
thumbnail: string;
};
interface SceneCardProps {
scene: SceneTypes;
onSceneSelect: (selectedScene: SceneTypes) => void;
selectedSceneId: string | null;
showDate?: boolean;
}
export const SceneCard: React.FC<SceneCardProps> = ({
scene,
onSceneSelect,
selectedSceneId,
showDate,
}) => {
const handleSelected = (scene: SceneTypes) => {
onSceneSelect(scene);
};
// const bucketConfig = useRef(new BucketConfig());
// const imageUrl = bucketConfig.current.getCdnUrl(
// scene.thumbnail + "-thumb.gif",
// 360,
// 20,
// );
// thumbnail will be replaced with 3d scene screenshots
const tempThumbnail = "/resources/placeholders/scene_placeholder.png";
return (
<button
key={scene.token}
className={twMerge(
"relative w-full 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={tempThumbnail}
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>
);
};
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { twMerge } from "tailwind-merge";
import { SceneCard } from "~/pages/PageEnigma/comps/ControlsTopButtons/ScenePicker/SceneCard";
export type SceneTypes = {
token: string;
@@ -36,48 +36,13 @@ export const ScenePicker: React.FC<ScenePickerProps> = ({
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
<SceneCard
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>
scene={scene}
onSceneSelect={() => handleSelected(scene)}
selectedSceneId={selectedSceneId}
showDate={showDate}
/>
))}
</div>
);
+1 -1
View File
@@ -58,7 +58,7 @@ export enum MediaFileType {
// Full Storyteller Engine scenes (RON = Rusty Object Notation)
// This is a temporary format that will eventually go away.
SceneRon = "scene_ron",
Scene = "scene",
None = "none",
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { timelineScrollX } from "~/pages/PageEnigma/store/timing";
// timeline
export const scale = signal(1);
export const filmLength = signal(12);
export const filmLength = signal(7);
export const timelineHeight = signal(0);
export const cameraMinimized = signal(false);