Api2.5/ Listing Scenes (#501)

- list user scenes done
- list featured scenes done

Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
wilandwil authored and GitHub committed 2024-06-19 10:45:11 -04:00
1 parent a40173bc74
commit efa223d795
6 files changed
+88 -280

No files matched your search

@@ -1,170 +0,0 @@
import { useEffect, useRef, useState, useLayoutEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import dayjs from "dayjs";
import { MediaInfo } from "~/pages/PageEnigma/models";
import { MediaFileType } from "~/pages/PageEnigma/enums";
import { authentication } from "~/signals";
import {
GetMediaByUser,
GetMediaListResponse,
} from "~/api/media_files/GetMediaByUser";
import {
ListFeaturedMediaFiles,
ListFeaturedMediaFilesResponse,
} from "~/api/media_files/ListFeaturedMediaFiles";
import { ScenePicker, SceneTypes } from "../ScenePicker";
import { Label, LoadingSpinner } from "~/components";
interface LoadSceneProps {
onSceneSelect: (token: string) => void;
}
export enum FetchStatus {
paused,
// ready triggers a new fetch
ready,
in_progress,
success,
error,
}
export enum Filters {
Featured,
Mine,
Bookmarked,
}
export const LoadScene = ({ onSceneSelect }: LoadSceneProps) => {
useSignals();
const { userInfo } = authentication;
const [scenes, setScenes] = useState<SceneTypes[]>([]);
const [featured, featuredSet] = useState<SceneTypes[]>([]);
const sceneLoading = useRef(false);
const [isSceneLoading, setIsSceneLoading] = useState(true);
const [featuredStatus, featuredStatusSet] = useState(FetchStatus.ready);
useEffect(() => {
if (!userInfo.value || scenes.length || sceneLoading.current) {
return;
}
sceneLoading.current = true;
const updateScenes = (results: MediaInfo[]) =>
results.map((scene: MediaInfo) => ({
token: scene.token,
name: scene.maybe_title ?? "Untitled",
updated_at: dayjs(scene.updated_at).format("MMM D, YYYY HH:mm:ss"),
thumbnail: scene.cover_image.maybe_cover_image_public_bucket_path
? scene.cover_image.maybe_cover_image_public_bucket_path
: undefined,
}));
// console.log("load scene");
GetMediaByUser(
userInfo.value.username,
{},
{
filter_engine_categories: MediaFileType.Scene,
},
)
.then((res: GetMediaListResponse) => {
if (res.success && res.results) {
setScenes(updateScenes(res.results));
setIsSceneLoading(false);
}
})
.catch(() => {
return {
success: false,
error_reason: "Unknown error",
};
});
if (featuredStatus === FetchStatus.ready) {
featuredStatusSet(FetchStatus.in_progress);
ListFeaturedMediaFiles(
"",
{},
{
filter_engine_categories: "scene",
page_size: 100,
},
).then((res: ListFeaturedMediaFilesResponse) => {
if (res.success && res.results) {
featuredStatusSet(FetchStatus.success);
featuredSet(updateScenes(res.results));
}
});
}
}, [featuredStatus, scenes, userInfo.value]);
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 + 1;
const hasOverflow = element.scrollHeight > element.clientHeight;
setBottomGradientOpacity(hasOverflow && !atBottom ? 1 : 0);
}
};
useLayoutEffect(() => {
const element = scrollContainerRef.current;
if (element) {
handleScroll();
element.addEventListener("scroll", handleScroll);
return () => {
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] 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 || featured.length !== 0 ? (
<div
className="overflow-y-auto overflow-x-hidden"
ref={scrollContainerRef}
>
<ScenePicker
scenes={[...scenes, ...featured]}
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 }}
/>
</div>
</div>
);
};
@@ -1 +0,0 @@
export { LoadScene } from "./LoadScene";
@@ -1,16 +1,23 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import dayjs from "dayjs";
import { MediaInfo } from "~/pages/PageEnigma/models";
import { ToastTypes } from "~/enums";
import { FilterEngineCategories, ToastTypes } from "~/enums";
import { addToast } from "~/signals";
import { GetScenesByUser } from "./utilities";
import { ScenePicker, SceneTypes } from "../ScenePicker";
import { Label, LoadingSpinner } from "~/components";
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
interface LoadSceneProps {
onSceneSelect: (token: string) => void;
}
@@ -35,12 +42,7 @@ export const LoadUserScenes = ({ onSceneSelect }: LoadSceneProps) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);
useEffect(() => {
if (scenes) {
//only call once on mount
return;
}
const getScenesByUser = useCallback(async () => {
const modMediaInfoToScenes = (results: MediaInfo[]) =>
results.map((scene: MediaInfo) => ({
token: scene.token,
@@ -51,14 +53,27 @@ export const LoadUserScenes = ({ onSceneSelect }: LoadSceneProps) => {
: undefined,
}));
GetScenesByUser().then((res) => {
if ("results" in res) {
setScenes(modMediaInfoToScenes(res.results));
return;
}
addToast(ToastTypes.ERROR, res.error_reason);
const mediaFilesApi = new MediaFilesApi();
const response = await mediaFilesApi.ListUserMediaFiles({
filter_engine_categories: [FilterEngineCategories.SCENE],
});
}, [scenes]);
if (response.success && response.data) {
setScenes(modMediaInfoToScenes(response.data));
return;
}
addToast(
ToastTypes.ERROR,
response.errorMessage || "Unknown Error in Loading User Scenes",
);
}, []);
useEffect(() => {
if (scenes) {
//only call once on mount
return;
}
getScenesByUser();
}, [scenes, getScenesByUser]);
const handleSceneSelect = (selectedScene: SceneTypes) => {
onSceneSelect(selectedScene.token);
@@ -1,36 +0,0 @@
import { GetMediaListResponse } from "~/pages/PageEnigma/models";
import { authentication, environmentVariables } from "~/signals";
const getMediaFilesByUsername = (username: string) =>
`${environmentVariables.value.BASE_API}/v1/media_files/list/user/${username}?filter_media_type=scene_json`;
export const GetScenesByUser = () => {
const unknownError = {
success: false,
error_reason: "Unknown error in Loading User Scenes",
};
if (!authentication.userInfo.value) {
return new Promise<typeof unknownError>((resolve) => {
resolve(unknownError);
});
}
const endpoint = getMediaFilesByUsername(
authentication.userInfo.value.username,
);
return fetch(endpoint, {
method: "GET",
headers: {
Accept: "application/json",
},
credentials: "include",
})
.then((res) => res.json())
.then((res: GetMediaListResponse) => {
return res;
})
.catch(() => {
return unknownError;
});
};
@@ -1,10 +1,20 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import dayjs from "dayjs";
import { MediaInfo } from "~/pages/PageEnigma/models";
import { FilterEngineCategories, ToastTypes } from "~/enums";
import { ScenePicker, SceneTypes } from "../ScenePicker";
import { ListFeaturedScenes } from "./utilities";
import { LoadingSpinner } from "~/components";
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
import { addToast } from "~/signals";
interface NewSceneFromTemplateProps {
onSceneSelect: (token: string) => void;
@@ -13,16 +23,16 @@ interface NewSceneFromTemplateProps {
export const NewSceneFromTemplate = ({
onSceneSelect,
}: NewSceneFromTemplateProps) => {
const [featuredScenes, setFeaturedScenes] = useState<SceneTypes[]>([]);
const [featuredScenes, setFeaturedScenes] = useState<
SceneTypes[] | undefined
>(undefined);
const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1);
const scrollContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (featuredScenes.length > 0) {
//only call once on mount
return;
}
const listFeaturedScenes = useCallback(async () => {
const mediaFilesApi = new MediaFilesApi();
// Hard coded scenes
const dummyScenes: SceneTypes[] = [
{
token: "m_nmzvdqr6kr8eqpmxqdzkqj0yknrjwv",
@@ -30,7 +40,6 @@ export const NewSceneFromTemplate = ({
thumbnail: "/resources/placeholders/scene_placeholder.png",
},
];
const modMediaInfoToScenes = (results: MediaInfo[]) =>
results.map((scene: MediaInfo) => ({
token: scene.token,
@@ -40,18 +49,29 @@ export const NewSceneFromTemplate = ({
? scene.cover_image.maybe_cover_image_public_bucket_path
: undefined,
}));
ListFeaturedScenes().then((res) => {
if (res.success && "results" in res) {
setFeaturedScenes([
...modMediaInfoToScenes(res.results),
...dummyScenes,
]);
return;
}
setFeaturedScenes(dummyScenes);
const response = await mediaFilesApi.ListFeaturedMediaFiles({
filter_engine_categories: [FilterEngineCategories.SCENE],
});
}, [featuredScenes]);
if (response.success && response.data) {
setFeaturedScenes([
...modMediaInfoToScenes(response.data),
...dummyScenes,
]);
return;
}
setFeaturedScenes(dummyScenes);
addToast(
ToastTypes.ERROR,
response.errorMessage || "Unknown Error in Listing Feature Scenes",
);
}, []);
useEffect(() => {
if (featuredScenes) {
//only call once on mount
return;
}
listFeaturedScenes();
}, [featuredScenes, listFeaturedScenes]);
const handleSceneSelect = (selectedScene: SceneTypes) => {
onSceneSelect(selectedScene.token);
@@ -83,15 +103,23 @@ export const NewSceneFromTemplate = ({
return (
<div className="flex flex-col gap-0.5">
<div className="relative flex max-h-[500px] min-h-[140px] flex-col">
<div
className="overflow-y-auto overflow-x-hidden"
ref={scrollContainerRef}
>
<ScenePicker
scenes={featuredScenes}
onSceneSelect={handleSceneSelect}
/>
</div>
{!featuredScenes && (
<div className="flex items-center justify-center gap-3 py-12">
<LoadingSpinner />
<span className="font-medium opacity-70">Loading scenes...</span>
</div>
)}
{featuredScenes && (
<div
className="overflow-y-auto overflow-x-hidden"
ref={scrollContainerRef}
>
<ScenePicker
scenes={featuredScenes}
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 }}
@@ -1,28 +0,0 @@
import { ListFeaturedMediaFilesResponse } from "~/api/media_files/ListFeaturedMediaFiles";
import { environmentVariables } from "~/signals";
const listFeaturedScenes = () =>
`${environmentVariables.value.BASE_API}/v1/media_files/list_featured?filter_media_type=scene_json`;
export const ListFeaturedScenes = () => {
const unknownError = {
success: false,
error_reason: "Unknown error in Loading Featured Scenes",
};
const endpoint = listFeaturedScenes();
return fetch(endpoint, {
method: "GET",
headers: {
Accept: "application/json",
},
credentials: "include",
})
.then((res) => res.json())
.then((res: ListFeaturedMediaFilesResponse) => {
return res;
})
.catch(() => {
return unknownError;
});
};