mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
Merge pull request #556 from storytold/feature/featured-media-pagination
Featured Media tab wired up with lazy loading pagination
This commit is contained in:
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user