Merge pull request #556 from storytold/feature/featured-media-pagination

Featured Media tab wired up with lazy loading pagination
This commit is contained in:
Brandon Thomas authored and GitHub committed 2024-05-06 20:59:54 -07:00
commit 5628d7681b
3 files changed
+244 -165

No files matched your search

@@ -1,4 +1,24 @@
{
"MediaCategories": {
"all": "All",
"audio": "Audio",
"image": "Image",
"video": "Video",
"engine_asset": "3D Asset"
},
"MediaCategoriesPlural": {
"all": "All",
"audio": "Audio",
"image": "Images",
"video": "Videos",
"engine_asset": "3D Assets"
},
"WeightsCategories": {
"all": "All",
"faceAnimation": "Face Animator",
"tts": "Text to speech",
"voiceConversion": "Voice conversion"
},
"WeightTypes": {
"all": "All weights",
"hifigan_tt2": "HiFi-GAN TT2",
@@ -20,5 +40,5 @@
"glb": "GLB",
"gltf": "gltf",
"fbx": "FBX"
},
}
}
}
@@ -1,6 +1,6 @@
import { enumToKeyArr } from "resources";
import { UploadMediaResponse } from "@storyteller/components/src/api/media_files/UploadMedia";
import { UploadEngineAssetResponse }from "@storyteller/components/src/api/media_files/UploadEngineAsset";
import { UploadEngineAssetResponse } from "@storyteller/components/src/api/media_files/UploadEngineAsset";
// 1
@@ -10,7 +10,7 @@ import { UploadEngineAssetResponse }from "@storyteller/components/src/api/media_
export enum EntityType {
unknown,
media,
weights
weights,
}
// 2
@@ -23,14 +23,14 @@ export enum MediaFilters {
audio,
image,
video,
engine_asset
engine_asset,
}
export enum WeightsCategories {
all,
faceAnimation,
tts,
voiceConversion
voiceConversion,
}
// 3
@@ -49,7 +49,7 @@ export enum WeightsFilters {
so_vits_svc,
tt2,
loRA,
vall_e
vall_e,
}
// media file types
@@ -60,17 +60,17 @@ export enum EngineTypes {
glb,
gltf,
obj,
ron
ron,
}
export enum AudioTypes {
mp3,
wav
wav,
}
export enum ImageTypes {
jpg,
png
png,
}
export enum VideoTypes {
@@ -128,15 +128,19 @@ export type JobSelection = WeightCategoriesProp | WeightFilterProp;
export type UploaderResponse = UploadEngineAssetResponse | UploadMediaResponse;
export const ListEntityFilters = (mode?: EntityInputMode) => {
// this an object of all types
// in most cases it is better to have granular caterogization of types
// but because any entity can/could(?) be bookmarked all types are agregated here
// this is condensed because typescript didn't like when I broke it apart.
// Will clean up soon, I promise
const bookmarkFilters = Object.keys({ ...MediaFilters, ...WeightsFilters }).filter(val => isNaN(Number(val))).reduce((obj,current) => ({ ...obj, [current]: current }),{});
const bookmarkFilters = Object.keys({ ...MediaFilters, ...WeightsFilters })
.filter(val => isNaN(Number(val)))
.reduce((obj, current) => ({ ...obj, [current]: current }), {});
const selectedFilters = mode !== undefined ? [bookmarkFilters,MediaFilters,WeightsFilters,WeightsFilters][mode] : EntityInputMode;
const selectedFilters =
mode !== undefined
? [bookmarkFilters, MediaFilters, WeightsFilters, WeightsFilters][mode]
: EntityInputMode;
return Object.values(selectedFilters).filter(val => isNaN(Number(val)));
};
@@ -145,36 +149,59 @@ export const ListEntityFilters = (mode?: EntityInputMode) => {
// note the use t() to translate labels
// this allows easily maintained translations for inputs
export const EntityFilterOptions = (mode?: EntityInputMode, t = (v:string) => v) => {
return ListEntityFilters(mode).map((value) => {
if (typeof value === "string") return { value, label: t(value) }
return { label: "all", value: "all" };
export const EntityFilterOptions = (
mode?: EntityInputMode,
t = (v: string) => v
) => {
const translationNamespaces = [
"BookmarksCategories",
"MediaCategoriesPlural",
"WeightsCategories",
"WeightsCategories",
];
const selectedNamespace = translationNamespaces[mode || -1];
return ListEntityFilters(mode).map(value => {
if (typeof value === "string")
return {
value,
label:
selectedNamespace !== undefined
? t(`${selectedNamespace}.${value}`)
: value,
};
return { label: t(`${selectedNamespace}.all`), value: "all" };
});
};
export const getMediaTypesByCategory = (mediaCategory: MediaFilters) => enumToKeyArr([
{ ...AudioTypes, ...ImageTypes, ...VideoTypes, ...EngineTypes },
AudioTypes,
ImageTypes,
VideoTypes,
EngineTypes
][mediaCategory]);
export const getMediaTypesByCategory = (mediaCategory: MediaFilters) =>
enumToKeyArr(
[
{ ...AudioTypes, ...ImageTypes, ...VideoTypes, ...EngineTypes },
AudioTypes,
ImageTypes,
VideoTypes,
EngineTypes,
][mediaCategory]
);
export const isSelectedType = (mediaCategory: MediaFilters, fileExtension: string) =>
getMediaTypesByCategory(mediaCategory).includes(fileExtension);
export const isSelectedType = (
mediaCategory: MediaFilters,
fileExtension: string
) => getMediaTypesByCategory(mediaCategory).includes(fileExtension);
export const mediaCategoryfromString = (categoryStr: string) => enumToKeyArr(MediaFilters).indexOf(categoryStr);
export const mediaCategoryfromString = (categoryStr: string) =>
enumToKeyArr(MediaFilters).indexOf(categoryStr);
//renaming to getMediaCatgoryByType
export const getMediaCategory = (fileExtension: string) => {
isSelectedType(MediaFilters.image,fileExtension);
if (isSelectedType(MediaFilters.engine_asset, fileExtension)) return MediaFilters.engine_asset;
if (isSelectedType(MediaFilters.audio, fileExtension)) return MediaFilters.audio;
if (isSelectedType(MediaFilters.image, fileExtension)) return MediaFilters.image;
if (isSelectedType(MediaFilters.video, fileExtension)) return MediaFilters.video;
isSelectedType(MediaFilters.image, fileExtension);
if (isSelectedType(MediaFilters.engine_asset, fileExtension))
return MediaFilters.engine_asset;
if (isSelectedType(MediaFilters.audio, fileExtension))
return MediaFilters.audio;
if (isSelectedType(MediaFilters.image, fileExtension))
return MediaFilters.image;
if (isSelectedType(MediaFilters.video, fileExtension))
return MediaFilters.video;
return MediaFilters.all; // will change to "unknown" eventually
}
};
@@ -1,152 +1,184 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import MediaCards from "components/common/Card/MediaCards";
import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import { useBookmarks, useRatings } from "hooks";
import { ListFeaturedMediaFiles } from "@storyteller/components/src/api/media_files/ListFeaturedMediaFiles";
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
import { Button, TempSelect as Select } from "components/common";
import {
faArrowDownWideShort,
faFilter,
} from "@fortawesome/pro-solid-svg-icons";
import AudioPlayerProvider from "components/common/AudioPlayer/AudioPlayerContext";
import SkeletonCard from "components/common/Card/SkeletonCard";
// import { SegmentButtons } from "components/common";
import "./FeaturedTab.scss";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import {
useBookmarks,
useLazyLists,
useLocalize,
useOnScreen,
useRatings,
} from "hooks";
import InfiniteScroll from "react-infinite-scroll-component";
import prepFilter from "resources/prepFilter";
import { ListFeaturedMediaFiles } from "@storyteller/components/src/api/media_files/ListFeaturedMediaFiles";
import {
EntityInputMode,
EntityFilterOptions,
} from "components/entities/EntityTypes";
export default function FeaturedTab() {
const { pathname: source } = useLocation();
export default function MediaTab() {
const { pathname: source, search } = useLocation();
const urlQueries = new URLSearchParams(search);
const bookmarks = useBookmarks();
const ratings = useRatings();
const toTopBtnRef = useRef<HTMLDivElement | null>(null);
const onScreen = useOnScreen(toTopBtnRef, "0px");
const { t } = useLocalize("EntityGeneral");
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const [mediaType, mediaTypeSet] = useState(
urlQueries.get("filter_media_type") || "all"
);
const [showMasonryGrid, setShowMasonryGrid] = useState(true);
const [list, listSet] = useState<MediaFile[]>([]);
const [listType,
// listTypeSet
] = useState("media");
// const [showMasonryGrid, setShowMasonryGrid] = useState(true);
const [status, statusSet] = useState(FetchStatus.ready);
const isLoading =
status === FetchStatus.ready || status === FetchStatus.in_progress;
const media = useLazyLists({
addQueries: {
page_size: urlQueries.get("page_size") || "24",
...prepFilter(mediaType, "filter_media_type"),
},
addSetters: { mediaTypeSet },
fetcher: ListFeaturedMediaFiles,
list,
listSet,
onInputChange: () => setShowMasonryGrid(false),
onSuccess: res => {
ratings.gather({ res, expand: true, key: "token" }); // expand rather than replace for lazy loading
bookmarks.gather({ res, expand: true, key: "token" }); // expand rather than replace for lazy loading
setShowMasonryGrid(true);
},
requestList: true,
});
// const options = [{ label: "Media", value: "media" },{ label: "Weights", value: "weight" }];
// const onChange = ({ target }: any) => {
// listTypeSet(target.value === "weight" ? target.value : "media");
// // clear list
// // statusSet(FetchStatus.ready)
// };
const filterOptions = EntityFilterOptions(EntityInputMode.media, t);
useEffect(() => {
if (status === FetchStatus.ready) {
statusSet(FetchStatus.in_progress);
if (listType === "media") {
ListFeaturedMediaFiles("", {}).then((res: any) => {
console.log("🏮", res);
statusSet(FetchStatus.success);
if (res.results) {
listSet(res.results);
}
});
}
// else if (listType === "weight") {}
}
}, [listType, status]);
const sortOptions = [
{ value: false, label: "Newest" },
{ value: true, label: "Oldest" },
// { value: "mostliked", label: "Most Liked" },
];
return (
<div className="d-flex flex-column gap-4">
<div>
<div className="fy-featured-header mb-3">
<h3 className="fw-semibold mb-0">Featured</h3>
{
// <SegmentButtons {...{ onChange, options, value: listType }}/> // switch between media/weights control
}
<Link to="/explore/media">
View media
<FontAwesomeIcon icon={faChevronRight} className="ms-2" />
</Link>
<>
<div className="d-flex flex-wrap gap-3 mb-3">
<div className="d-flex flex-grow-1 flex-wrap gap-2">
<Select
{...{
icon: faArrowDownWideShort,
options: sortOptions,
name: "sort",
onChange: media.onChange,
value: media.sort,
}}
/>
<Select
{...{
icon: faFilter,
options: filterOptions,
name: "mediaType",
onChange: media.onChange,
value: mediaType,
}}
/>
</div>
{isLoading && !list.length ? (
{media.urlCursor ? (
<div>
<Button
{...{
className: `to-top-button`,
buttonRef: toTopBtnRef,
label: "Back to top",
onClick: () => media.reset(),
small: true,
}}
/>
</div>
) : null}
</div>
{media.urlCursor && !onScreen ? (
<Button
{...{
className: `to-top-button-off-screen`,
label: "Back to top",
onClick: () => media.reset(),
small: true,
}}
/>
) : null}
<AudioPlayerProvider>
{media.isLoading && !media.list.length ? (
<div className="row gx-3 gy-3">
{Array.from({ length: 12 }).map((_, index) => (
<SkeletonCard key={index} />
))}
</div>
) : (
<>
{list.length === 0 && status === 3 ? (
<div className="text-center mt-4 opacity-75">
No featured media.
</div>
) : (
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{list.map((data: any, key: number) => {
let props = {
bookmarks,
data,
ratings,
showCreator: true,
source,
type: "media",
};
return (
<div
{...{
className: "col-12 col-sm-6 col-xl-4 grid-item",
key,
}}
>
<MediaCards {...{ type: data.media_type, props }} />
</div>
);
})}
</MasonryGrid>
)}
</>
)}
</div>
{/* <div>
<div className="d-flex align-items-center mb-3">
<h3 className="fw-semibold mb-0 flex-grow-1">
Featured Image Weights
</h3>
<Link to="/explore/weights">
View all
<FontAwesomeIcon icon={faChevronRight} className="ms-2" />
</Link>
</div>
{isLoading ? (
<div className="row gx-3 gy-3">
{Array.from({ length: 6 }).map((_, index) => (
<SkeletonCard key={index} />
))}
</div>
) : (
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{data.map((data, index) => {
let card = (
<ImageCard
key={index}
data={data}
type="weights"
showCreator={true}
/>
);
return (
<div key={index} className="col-12 col-sm-6 col-xl-4 grid-item">
{card}
<InfiniteScroll
dataLength={media.list.length}
next={media.getMore}
hasMore={!media.list.length || !!media.next}
loader={
media.list.length !== 0 &&
media.isLoading && (
<div className="mt-4 d-flex justify-content-center">
<div className="spinner-border text-light" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
})}
</MasonryGrid>
)
}
endMessage={
<p className="text-center mt-4 opacity-75">No more results.</p>
}
className="overflow-hidden"
>
{showMasonryGrid && (
<>
{media.list.length === 0 && media.status === 3 ? (
<div className="text-center mt-4 opacity-75">
No media created yet.
</div>
) : (
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{media.list.map((data: any, key: number) => {
let props = {
bookmarks,
data,
source,
ratings,
type: "media",
showCreator: true,
};
return (
<div
{...{
className: "col-12 col-sm-6 col-xl-4 grid-item",
key,
}}
>
<MediaCards {...{ type: data.media_type, props }} />
</div>
);
})}
</MasonryGrid>
)}
</>
)}
</InfiniteScroll>
)}
</div> */}
</div>
</AudioPlayerProvider>
</>
);
}