Merge pull request #433 from storytold/bflat-fy-explore-endpoints

weights endpoint and masonry grid scrolling to top bug fix
This commit is contained in:
BFlat authored and GitHub committed 2023-12-22 17:53:26 +07:00
commit 684e463e7c
12 files changed
+345 -211

No files matched your search

@@ -8,3 +8,17 @@ export enum WeightType {
LORA = "loRA",
VALL_E = "vall_e",
}
export const weightTypeInfo = {
[WeightType.TT2]: { label: "TT2", color: "teal" },
[WeightType.HIFIGAN_TT2]: {
label: "HifiGAN TT2",
color: "blue",
},
[WeightType.RVCv2]: { label: "RVCv2", color: "orange" },
[WeightType.SD_15]: { label: "SD 1.5", color: "green" },
[WeightType.SDXL]: { label: "SDXL", color: "purple" },
[WeightType.SVC]: { label: "SVC", color: "cyan" },
[WeightType.LORA]: { label: "LoRA", color: "pink" },
[WeightType.VALL_E]: { label: "VALL-E", color: "ultramarine" },
};
@@ -1,6 +1,5 @@
import MakeRequest from "../MakeRequest";
import { MediaFile } from "./GetMedia";
// import { MediaFileType } from "../_common/enums/MediaFileType";
import { LazyPagination } from "../_common/SharedFetchTypes";
export interface ListMediaFilesRequest {}
@@ -1,28 +1,42 @@
// import MakeRequest from "../MakeRequest";
import MakeRequest from "../MakeRequest";
import { UserDetailsLight } from "../_common/UserDetailsLight";
import { WeightCategory } from "../_common/enums/WeightCategory";
import { WeightType } from "../_common/enums/WeightType";
export interface Weight {
weight_token: string;
weight_type: WeightType;
weight_category: WeightCategory;
maybe_creator_user: UserDetailsLight | null;
weights_type: WeightType;
weights_category: WeightCategory;
title: string;
public_bucket_path: string;
creator_set_visibility: string;
created_at: Date;
updated_at: Date;
title: string;
creator: UserDetailsLight;
description_markdown: string;
description_rendered_html: string;
file_checksum_sha2: string;
file_size_bytes: number;
maybe_cached_user_ratings_ratio: number | null;
maybe_cover_image_public_bucket_path: string | null;
version: number;
}
// export interface GetMediaRequest {}
export interface GetWeightRequest {
weight_token: "string";
}
// export interface GetMediaResponse {
// success: boolean,
// media_file?: MediaFile,
// }
export interface GetWeightResponse {
success: boolean;
media_file?: Weight;
}
// export const GetMedia = MakeRequest<string, GetMediaRequest, GetMediaResponse, {}>({
// method: "GET",
// routingFunction: (mediaFileToken: string) => `/v1/media_files/file/${mediaFileToken}`,
// });
export const GetWeight = MakeRequest<
string,
GetWeightRequest,
GetWeightResponse,
{}
>({
method: "GET",
routingFunction: (weightToken: string) => `/v1/weights/weight/${weightToken}`,
});
@@ -0,0 +1,25 @@
import MakeRequest from "../MakeRequest";
import { Weight } from "./GetWeight";
import { LazyPagination } from "../_common/SharedFetchTypes";
export interface ListWeightsRequest {}
export interface ListWeightsResponse {
pagination: LazyPagination;
success: boolean;
results: Weight[];
}
export interface ListWeightsParams {
page_index: number;
}
export const ListWeights = MakeRequest<
string,
ListWeightsRequest,
ListWeightsResponse,
ListWeightsParams
>({
method: "GET",
routingFunction: () => `/v1/weights/list`,
});
@@ -25,13 +25,11 @@ export default function AudioCard({
}: AudioCardProps) {
const history = useHistory();
// console.log("🐙",data, data.public_bucket_path);
const handleCardClick = () => {
if (type === "media") {
history.push(`/media/${data.token}`);
} else if (type === "weights") {
history.push(`/weight/${data.token}`);
history.push(`/weight/${data.weight_token}`);
}
};
@@ -81,13 +79,15 @@ export default function AudioCard({
{showCreator && (
<div className="flex-grow-1">
<CreatorName
displayName={data.maybe_creator.display_name}
gravatarHash={data.maybe_creator.gravatar_hash}
avatarIndex={data.maybe_creator.default_avatar.image_index}
backgroundIndex={
data.maybe_creator.default_avatar.color_index
displayName={data.maybe_creator?.display_name || "Anonymous"}
gravatarHash={data.maybe_creator?.gravatar_hash || null}
avatarIndex={
data.maybe_creator?.default_avatar.image_index || 0
}
username={data.maybe_creator.username}
backgroundIndex={
data.maybe_creator?.default_avatar.color_index || 0
}
username={data.maybe_creator?.username || "anonymous"}
/>
</div>
)}
@@ -126,9 +126,7 @@ export default function AudioCard({
<div className="d-flex align-items-center mt-3">
<div className="flex-grow-1">
<h6 className="fw-semibold text-white mb-1">
{data.weight_name}
</h6>
<h6 className="fw-semibold text-white mb-1">{data.title}</h6>
<p className="fs-7 opacity-75">{timeAgo}</p>
</div>
</div>
@@ -144,15 +142,13 @@ export default function AudioCard({
{showCreator && (
<div className="flex-grow-1">
<CreatorName
displayName={data.maybe_creator_user.display_name}
gravatarHash={data.maybe_creator_user.gravatar_hash}
avatarIndex={
data.maybe_creator_user.default_avatar.image_index
}
displayName={data.creator?.display_name || "Anonymous"}
gravatarHash={data.creator?.gravatar_hash || null}
avatarIndex={data.creator?.default_avatar.image_index || 0}
backgroundIndex={
data.maybe_creator_user.default_avatar.color_index
data.creator?.default_avatar.color_index || 0
}
username={data.maybe_creator_user.username}
username={data.creator?.username || "anonymous"}
/>
</div>
)}
@@ -8,6 +8,7 @@ import FavoriteButton from "components/common/FavoriteButton";
import CreatorName from "../CreatorName";
import Button from "components/common/Button";
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
import useWeightTypeInfo from "hooks/useWeightTypeInfo/useWeightTypeInfo";
interface ImageCardProps {
data: any;
@@ -22,7 +23,7 @@ export default function ImageCard({ data, type, showCreator }: ImageCardProps) {
if (type === "media") {
history.push(`/media/${data.token}`);
} else if (type === "weights") {
history.push(`/weight/${data.token}`);
history.push(`/weight/${data.weight_token}`);
}
};
@@ -36,6 +37,9 @@ export default function ImageCard({ data, type, showCreator }: ImageCardProps) {
console.log(`The item is now ${data.isLiked ? "liked" : "not liked"}.`);
};
const { label: weightBadgeLabel, color: weightBadgeColor } =
useWeightTypeInfo(data.weights_type);
return (
<Card padding={false} onClick={handleCardClick}>
{type === "media" && (
@@ -68,11 +72,17 @@ export default function ImageCard({ data, type, showCreator }: ImageCardProps) {
{showCreator && (
<div className="flex-grow-1">
<CreatorName
displayName={data.maybe_creator?.display_name || "" } // these empty strings are fallbacks to prevent errors
gravatarHash={data.maybe_creator?.gravatar_hash || "" } // since maybe_creator does not exist until loaded
avatarIndex={data.maybe_creator?.default_avatar.image_index || "" } // consider wrapping everything that uses
backgroundIndex={data.maybe_creator?.default_avatar.color_index || "" } // !!maybe_creator condition
username={data.maybe_creator?.username || "" } // which only displays when that value is defined
displayName={
data.maybe_creator?.display_name || "Anonymous"
}
gravatarHash={data.maybe_creator?.gravatar_hash || null}
avatarIndex={
data.maybe_creator?.default_avatar.image_index || 0
}
backgroundIndex={
data.maybe_creator?.default_avatar.color_index || 0
}
username={data.maybe_creator?.username || "anonymous"}
/>
</div>
)}
@@ -89,15 +99,22 @@ export default function ImageCard({ data, type, showCreator }: ImageCardProps) {
{type === "weights" && (
<>
<img
src={data.public_bucket_path}
alt={data.weight_name}
src={
data.maybe_cover_image_public_bucket_path ||
"/images/avatars/default-pfp.png"
}
alt={data.title}
className="card-img"
/>
<div className="card-img-overlay">
<div className="card-img-gradient" />
<div className="d-flex align-items-center">
<div className="d-flex flex-grow-1">
<Badge label="LORA" color="pink" overlay={true} />
<Badge
label={weightBadgeLabel}
color={weightBadgeColor}
overlay={true}
/>
</div>
<Button
icon={faArrowRight}
@@ -112,9 +129,7 @@ export default function ImageCard({ data, type, showCreator }: ImageCardProps) {
<div className="card-img-overlay-text">
<div className="d-flex align-items-center mt-3">
<div className="flex-grow-1">
<h6 className="fw-semibold text-white mb-1">
{data.weight_name}
</h6>
<h6 className="fw-semibold text-white mb-1">{data.title}</h6>
<p className="fs-7 opacity-75 mb-0">{timeAgo}</p>
</div>
</div>
@@ -128,15 +143,15 @@ export default function ImageCard({ data, type, showCreator }: ImageCardProps) {
{showCreator && (
<div className="flex-grow-1">
<CreatorName
displayName={data.maybe_creator?.display_name || "" }
gravatarHash={data.maybe_creator?.gravatar_hash}
displayName={data.creator?.display_name || "Anonymous"}
gravatarHash={data.creator?.gravatar_hash || null}
avatarIndex={
data.maybe_creator?.default_avatar.image_index
data.creator?.default_avatar.image_index || 0
}
backgroundIndex={
data.maybe_creator?.default_avatar.color_index
data.creator?.default_avatar.color_index || 0
}
username={data.maybe_creator?.username}
username={data.creator?.username || "anonymous"}
/>
</div>
)}
@@ -144,10 +159,7 @@ export default function ImageCard({ data, type, showCreator }: ImageCardProps) {
<div>
<LikeButton onToggle={handleLike} likeCount={data.likes} />
</div>
<FavoriteButton
onToggle={handleLike}
favoriteCount={data.likes}
/>
<FavoriteButton onToggle={handleLike} />
</div>
</div>
</div>
@@ -64,11 +64,17 @@ export default function VideoCard({ data, type, showCreator }: VideoCardProps) {
{showCreator && (
<div className="flex-grow-1">
<CreatorName
displayName={data.maybe_creator?.display_name || "" }
gravatarHash={data.maybe_creator?.gravatar_hash || "" }
avatarIndex={data.maybe_creator?.default_avatar.image_index || "" }
backgroundIndex={data.maybe_creator?.default_avatar.color_index || "" }
username={data.maybe_creator?.username || "" }
displayName={
data.maybe_creator?.display_name || "Anonymous"
}
gravatarHash={data.maybe_creator?.gravatar_hash || ""}
avatarIndex={
data.maybe_creator?.default_avatar.image_index || ""
}
backgroundIndex={
data.maybe_creator?.default_avatar.color_index || ""
}
username={data.maybe_creator?.username || "anonymous"}
/>
</div>
)}
@@ -1,6 +1,6 @@
import Masonry from "masonry-layout"; //Refer to: https://github.com/desandro/masonry
import imagesLoaded from "imagesloaded"; //Refer to: https://github.com/desandro/imagesloaded
import React, { useEffect } from "react";
import React, { useEffect, useRef } from "react";
import Masonry from "masonry-layout";
import imagesLoaded from "imagesloaded";
interface MasonryGridProps {
children: React.ReactNode;
@@ -13,38 +13,77 @@ export default function MasonryGrid({
onLayoutComplete,
gridRef,
}: MasonryGridProps) {
const masonryInstance = useRef<Masonry | null>(null);
useEffect(() => {
let masonryInstance: Masonry | null = null;
const updateLayout = () => {
//Needs this type guard to check that it is not undefined.
if (masonryInstance && typeof masonryInstance.layout === "function") {
masonryInstance.layout();
}
};
if (gridRef.current) {
masonryInstance = new Masonry(gridRef.current, {
masonryInstance.current = new Masonry(gridRef.current, {
itemSelector: ".grid-item",
percentPosition: true,
transitionDuration: 0,
});
//Needs this or images will overflow masonry card. It checks if image is loaded then after it is loaded, it resizes the card.
imagesLoaded(gridRef.current, function () {
updateLayout();
onLayoutComplete?.();
});
}
//Clean up
return () => {
if (masonryInstance && typeof masonryInstance.destroy === "function") {
masonryInstance.destroy();
if (
masonryInstance.current &&
typeof masonryInstance.current.destroy === "function"
) {
masonryInstance.current.destroy();
}
};
}, [gridRef]);
useEffect(() => {
const currentGrid = gridRef.current;
const currentMasonryInstance = masonryInstance.current;
if (currentGrid && currentMasonryInstance) {
imagesLoaded(currentGrid, function () {
if (typeof currentMasonryInstance.layout === "function") {
currentMasonryInstance.layout();
onLayoutComplete?.();
}
});
}
}, [gridRef, onLayoutComplete]);
useEffect(() => {
const currentGrid = gridRef.current;
const currentMasonryInstance = masonryInstance.current;
if (currentGrid && currentMasonryInstance) {
// Observer to detect when new grid items are added
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
// Append new items to the Masonry layout
const newItems = Array.from(mutation.addedNodes).filter(
node =>
node instanceof HTMLElement &&
node.classList.contains("grid-item")
);
if (newItems.length) {
if (typeof currentMasonryInstance.appended === "function") {
currentMasonryInstance.appended(newItems);
}
}
}
});
});
// Observe the grid for new items
observer.observe(currentGrid, {
childList: true,
});
// Clean up observer on unmount
return () => {
observer.disconnect();
};
}
}, [gridRef]);
return (
<div
ref={gridRef}
@@ -87,17 +87,14 @@ function Tabs({ tabs }: TabsProps) {
))}
</ul>
<TabContent padding={activeTabProps?.padding}>
{tabs.map(tab => (
<a.div
key={tab.to}
style={fade}
className={`tab-pane fade ${
activeTab === tab.to ? "show active" : ""
}`}
>
{tab.content}
</a.div>
))}
<a.div
style={fade}
className={`tab-pane fade ${
activeTab === activeTabProps?.to ? "show active" : ""
}`}
>
{activeTabProps?.content}
</a.div>
</TabContent>
</nav>
);
@@ -0,0 +1,25 @@
import { WeightType } from "@storyteller/components/src/api/_common/enums/WeightType";
type WeightTypeInfo = {
[key in WeightType]: { label: string; color: string };
};
const weightTypeInfo: WeightTypeInfo = {
[WeightType.TT2]: { label: "TT2", color: "teal" },
[WeightType.HIFIGAN_TT2]: { label: "HifiGAN TT2", color: "blue" },
[WeightType.RVCv2]: { label: "RVCv2", color: "orange" },
[WeightType.SD_15]: { label: "SD 1.5", color: "green" },
[WeightType.SDXL]: { label: "SDXL", color: "purple" },
[WeightType.SVC]: { label: "SVC", color: "cyan" },
[WeightType.LORA]: { label: "LoRA", color: "pink" },
[WeightType.VALL_E]: { label: "VALL-E", color: "ultramarine" },
};
export default function useWeightTypeInfo(weightsType: WeightType) {
return (
weightTypeInfo[weightsType] || {
label: "Unknown",
color: "gray",
}
);
}
@@ -6,7 +6,6 @@ import SkeletonCard from "components/common/Card/SkeletonCard";
import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
import ImageCard from "components/common/Card/ImageCard";
export default function FeaturedTab() {
const gridContainerRef = useRef<HTMLDivElement | null>(null);
@@ -59,7 +58,7 @@ export default function FeaturedTab() {
</MasonryGrid>
)}
</div>
<div>
{/* <div>
<div className="d-flex align-items-center mb-3">
<h3 className="fw-semibold mb-0 flex-grow-1">
Featured Image Weights
@@ -98,7 +97,7 @@ export default function FeaturedTab() {
})}
</MasonryGrid>
)}
</div>
</div> */}
</div>
);
}
@@ -1,149 +1,157 @@
import React, { useRef, useState } from "react";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import mockWeightsData from "./mockWeightsData";
import AudioCard from "components/common/Card/AudioCard";
import ImageCard from "components/common/Card/ImageCard";
import VideoCard from "components/common/Card/VideoCard";
import Select from "components/common/Select";
// import VideoCard from "components/common/Card/VideoCard";
import { TempSelect } 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 { ListWeights } from "@storyteller/components/src/api/weights/ListWeights";
import { Weight } from "@storyteller/components/src/api/weights/GetWeight";
import { useLazyLists } from "hooks";
import InfiniteScroll from "react-infinite-scroll-component";
import { WeightCategory } from "@storyteller/components/src/api/_common/enums/WeightCategory";
export default function WeightsTab() {
export default function MediaTab() {
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const [data] = useState(mockWeightsData);
const [isLoading] = useState(false);
const [selectedFilter, setSelectedFilter] = useState("all");
const [list, listSet] = useState<Weight[]>([]);
const weights = useLazyLists({
fetcher: ListWeights,
list,
listSet,
requestList: true,
});
const filterOptions = [
{ value: "all", label: "All Weights" },
{ value: "tts", label: "Text to Speech" },
{ value: "vc", label: "Voice to Voice" },
{ value: "sd", label: "Image Generation" },
{ value: "images", label: "Image Generation" },
{ value: "audio", label: "Audio" },
{ value: "video", label: "Video" },
];
const sortOptions = [
{ value: "newest", label: "Newest" },
{ value: "oldest", label: "Oldest" },
{ value: "mostliked", label: "Most Bookmarked" },
{ value: "mostliked", label: "Most Liked" },
];
const modelTtsOptions = [
{ value: "all", label: "All Types" },
{ value: "tt2", label: "Tacotron 2" },
];
const modelVcOptions = [
{ value: "all", label: "All Types" },
{ value: "rvc", label: "RVCv2" },
{ value: "svc", label: "SoVitsSvc" },
];
const modelSdOptions = [
{ value: "all", label: "All Types" },
{ value: "lora", label: "LoRA" },
{ value: "SD15", label: "SD 1.5" },
{ value: "SDXL", label: "SD XL" },
];
const handleFilterChange = (option: any) => {
const selectedOption = option as { value: string; label: string };
setSelectedFilter(selectedOption.value);
};
return (
<>
<div className="d-flex flex-wrap gap-3 mb-3">
<div className="d-flex gap-2 flex-grow-1">
<Select
icon={faArrowDownWideShort}
options={sortOptions}
defaultValue={sortOptions[0]}
isSearchable={false}
<TempSelect
{...{
icon: faArrowDownWideShort,
options: sortOptions,
name: "sort",
onChange: weights.onChange,
value: weights.sort,
}}
/>
<Select
icon={faFilter}
options={filterOptions}
defaultValue={filterOptions[0]}
isSearchable={false}
onChange={handleFilterChange}
<TempSelect
{...{
icon: faFilter,
options: filterOptions,
name: "filter",
onChange: weights.onChange,
value: weights.filter,
}}
/>
{selectedFilter === "tts" && (
<Select
options={modelTtsOptions}
defaultValue={modelTtsOptions[0]}
isSearchable={false}
/>
)}
{selectedFilter === "sd" && (
<Select
options={modelSdOptions}
defaultValue={modelSdOptions[0]}
isSearchable={false}
/>
)}
{selectedFilter === "vc" && (
<Select
options={modelVcOptions}
defaultValue={modelVcOptions[0]}
isSearchable={false}
/>
)}
</div>
</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;
switch (data.media_type) {
case "audio":
card = (
<AudioCard
key={index}
data={data}
type="weights"
showCreator={true}
showCover={true}
/>
);
break;
case "image":
card = (
<ImageCard
key={index}
data={data}
type="weights"
showCreator={true}
/>
);
break;
case "video":
card = <VideoCard key={index} data={data} type="weights" />;
break;
default:
card = <div key={index}>Unsupported media type</div>;
}
return (
<div key={index} className="col-12 col-sm-6 col-xl-4 grid-item">
{card}
<AudioPlayerProvider>
{isLoading ? (
<div className="row gx-3 gy-3">
{Array.from({ length: 6 }).map((_, index) => (
<SkeletonCard key={index} />
))}
</div>
) : (
<InfiniteScroll
dataLength={weights.list.length}
next={weights.getMore}
hasMore={!weights.list.length || !!weights.next}
loader={
<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"
>
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{weights.list.map((data: any, index: number) => {
let card;
switch (data.weights_category) {
case WeightCategory.TTS:
card = (
<AudioCard
key={index}
data={data}
type="weights"
showCreator={true}
/>
);
break;
case WeightCategory.VC:
card = (
<AudioCard
key={index}
data={data}
type="weights"
showCreator={true}
/>
);
break;
case WeightCategory.ZS:
card = (
<AudioCard
key={index}
data={data}
type="weights"
showCreator={true}
/>
);
break;
case WeightCategory.SD:
card = (
<ImageCard
key={index}
data={data}
type="weights"
showCreator={true}
/>
);
break;
default:
card = <div>Unsupported weight type</div>;
}
return (
<div
key={index}
className="col-12 col-sm-6 col-xl-4 grid-item"
>
{card}
</div>
);
})}
</MasonryGrid>
</InfiniteScroll>
)}
</AudioPlayerProvider>
</>
);
}