Merge branch 'feature/mvp' into fixes/select_modal_overhaul

This commit is contained in:
wil committed 2024-02-19 11:46:41 -05:00
commit 182849cd00
20 files changed
+504 -197

No files matched your search

@@ -1,64 +1,73 @@
import { ApiConfig } from "@storyteller/components";
import { UserDetailsLight } from "../_common/UserDetailsLight";
export interface ListCommentsSuccessResponse {
success: boolean,
comments: Array<Comment>
success: boolean;
comments: Array<Comment>;
}
export interface Comment {
token: string,
token: string;
user: UserDetailsLight;
user_token: string;
username: string;
user_display_name: string;
user_gravatar_hash: string;
user_token: string,
username: string,
user_display_name: string,
user_gravatar_hash: string,
comment_markdown: string;
comment_rendered_html: string;
comment_markdown: string,
comment_rendered_html: string,
created_at: Date,
updated_at: Date,
maybe_edited_at?: Date,
created_at: Date;
updated_at: Date;
maybe_edited_at?: Date;
}
export interface ListCommentsErrorResponse {
success: boolean,
success: boolean;
}
type ListCommentsResponse = ListCommentsSuccessResponse | ListCommentsErrorResponse;
type ListCommentsResponse =
| ListCommentsSuccessResponse
| ListCommentsErrorResponse;
export function ListCommentsIsOk(response: ListCommentsResponse): response is ListCommentsSuccessResponse {
export function ListCommentsIsOk(
response: ListCommentsResponse
): response is ListCommentsSuccessResponse {
return response?.success === true;
}
export function ListCommentsIsError(response: ListCommentsResponse): response is ListCommentsErrorResponse {
export function ListCommentsIsError(
response: ListCommentsResponse
): response is ListCommentsErrorResponse {
return response?.success === false;
}
export async function ListComments(entityType: string, entityToken: string) : Promise<ListCommentsResponse>
{
export async function ListComments(
entityType: string,
entityToken: string
): Promise<ListCommentsResponse> {
const endpoint = new ApiConfig().commentList(entityType, entityToken);
return await fetch(endpoint, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
credentials: 'include',
})
.then(res => res.json())
.then(res => {
if (!res) {
return { success : false }; // TODO: This loses error semantics and is deprecated
}
if (res && 'success' in res) {
return res;
} else {
return { success : false };
}
return await fetch(endpoint, {
method: "GET",
headers: {
Accept: "application/json",
},
credentials: "include",
})
.catch(e => {
return { success : false };
});
.then(res => res.json())
.then(res => {
if (!res) {
return { success: false }; // TODO: This loses error semantics and is deprecated
}
if (res && "success" in res) {
return res;
} else {
return { success: false };
}
})
.catch(e => {
return { success: false };
});
}
@@ -8,6 +8,7 @@ export interface MediaFile {
token: string;
media_type: MediaFileType;
public_bucket_path: string;
maybe_batch_token: string;
maybe_creator_user: UserDetailsLight | null;
creator_set_visibility: string;
created_at: Date;
@@ -25,11 +26,17 @@ export interface MediaFile {
export interface GetMediaRequest {}
export interface GetMediaResponse {
success: boolean,
media_file?: MediaFile
success: boolean;
media_file?: MediaFile;
}
export const GetMedia = MakeRequest<string, GetMediaRequest, GetMediaResponse,{}>({
export const GetMedia = MakeRequest<
string,
GetMediaRequest,
GetMediaResponse,
{}
>({
method: "GET",
routingFunction: (mediaFileToken: string) => `/v1/media_files/file/${mediaFileToken}`,
});
routingFunction: (mediaFileToken: string) =>
`/v1/media_files/file/${mediaFileToken}`,
});
@@ -0,0 +1,22 @@
import MakeRequest from "../MakeRequest";
import { MediaFile } from "./GetMedia";
export interface GetMediaBatchImagesRequest {}
export interface GetMediaBatchImagesResponse {
success: boolean;
results: MediaFile[];
}
export interface GetMediaBatchImagesParams {}
export const GetMediaBatchImages = MakeRequest<
string,
GetMediaBatchImagesRequest,
GetMediaBatchImagesResponse,
GetMediaBatchImagesParams
>({
method: "GET",
routingFunction: (batchToken: string) =>
`/v1/media_files/batch/${batchToken}`,
});
@@ -1,21 +1,26 @@
import MakeRequest from "../MakeRequest";
import { MediaFile } from "./GetMedia";
import { MediaFileType } from "../_common/enums/MediaFileType";
import { Pagination } from "../_common/SharedFetchTypes";
export interface GetMediaRequest {}
export interface GetMediaListResponse {
pagination: Pagination,
success: boolean,
results: MediaFile[]
pagination: Pagination;
success: boolean;
results: MediaFile[];
}
export interface GetMediaParams {
page_index: number,
page_index: number;
}
export const GetMediaByUser = MakeRequest<string, GetMediaRequest, GetMediaListResponse, GetMediaParams>({
export const GetMediaByUser = MakeRequest<
string,
GetMediaRequest,
GetMediaListResponse,
GetMediaParams
>({
method: "GET",
routingFunction: (userToken: string) => `/v1/media_files/list/user/${userToken}`,
});
routingFunction: (userToken: string) =>
`/v1/media_files/list/user/${userToken}`,
});
@@ -22,6 +22,7 @@ export interface MediaFile {
maybe_cover_image_public_bucket_path: string;
};
maybe_text_transcript?: string;
maybe_batch_token?: string;
}
export enum MediaFileLookupError {
@@ -20,6 +20,7 @@ export interface ByQueueStats {
pending_svc_jobs: number,
pending_tacotron2_jobs: number,
pending_voice_designer: number,
pending_stable_diffusion: number,
}
export interface LegacyTtsInfo {
+1
View File
@@ -75,6 +75,7 @@
"react-twitch-embed": "^3.0.1",
"react-wavesurfer.js": "^0.0.8",
"sass": "^1.57.1",
"swiper": "^11.0.5",
"typescript": "^3.7.5",
"universal-cookie": "^6.1.1",
"uuid": "^8.3.2",
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+127 -118
View File
@@ -60,11 +60,9 @@ import { VoiceConversionModelUploadJob } from "@storyteller/components/src/jobs/
import { VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
import HttpBackend from "i18next-http-backend";
import {
InferenceJobs,
ModalProvider,
SessionProvider,
} from "components/providers";
import { SessionProvider, InferenceJobs } from "components/providers";
//this caused an error for me (Bombay) so i had to import it as a default
import ModalProvider from "components/providers/ModalProvider";
// NB: We're transitioning over to this instance of i18n-next that loads translations over HTTP from Json Files.
// The old i18n-next instance (see below) bakes in translations into the compiled javascript blob.
@@ -210,8 +208,8 @@ function initInferenceJobsByCategoryMap(): Map<
inferenceJobsByCategory.set(FrontendInferenceJobType.VideoMotionCapture, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.ImageGeneration, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.ConvertFbxtoGltf, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.VideoWorkflow, [])
inferenceJobsByCategory.set(FrontendInferenceJobType.VideoStyleTransfer, [])
inferenceJobsByCategory.set(FrontendInferenceJobType.VideoWorkflow, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.VideoStyleTransfer, []);
return inferenceJobsByCategory;
}
@@ -348,7 +346,7 @@ class App extends React.Component<Props, State> {
// state needs to be set here, or if instead can be refreshed locally on
// the page where the new "state" needed
if (this.state.sessionFetched === false){
if (this.state.sessionFetched === false) {
this.setState({ sessionFetched: true });
}
@@ -856,7 +854,7 @@ class App extends React.Component<Props, State> {
{...{
enqueue: this.enqueueInferenceJob,
byCategory: this.state.inferenceJobsByCategory,
inferenceJobs: this.state.inferenceJobs
inferenceJobs: this.state.inferenceJobs,
}}
>
<SessionProvider
@@ -867,115 +865,126 @@ class App extends React.Component<Props, State> {
sessionFetched: this.state.sessionFetched,
}}
>
<ModalProvider>
<Switch>
<Route path="/">
<PageContainer
sessionWrapper={this.state.sessionWrapper}
querySessionAction={this.querySession}
sessionSubscriptionsWrapper={
this.state.sessionSubscriptionsWrapper
}
querySessionSubscriptionsAction={
this.querySessionSubscriptions
}
isShowingVocodesNotice={
this.state.isShowingVocodesNotice
}
clearVocodesNotice={this.clearVocodesNotice}
isShowingLangaugeNotice={
this.state.isShowingLanguageNotice
}
clearLanguageNotice={this.clearLanguageNotice}
displayLanguage={this.state.displayLanguage}
primaryLanguageCode={this.state.primaryLanguageCode}
isShowingTwitchTtsNotice={
this.state.isShowingTwitchTtsNotice
}
clearTwitchTtsNotice={this.clearTwitchTtsNotice}
isShowingPleaseFollowNotice={
this.state.isShowingPleaseFollowNotice
}
clearPleaseFollowNotice={this.clearPleaseFollowNotice}
isShowingBootstrapLanguageNotice={
this.state.isShowingBootstrapLanguageNotice
}
clearBootstrapLanguageNotice={
this.clearBootstrapLanguageNotice
}
enqueueInferenceJob={this.enqueueInferenceJob}
inferenceJobs={this.state.inferenceJobs}
inferenceJobsByCategory={
this.state.inferenceJobsByCategory
}
enqueueTtsJob={this.enqueueTtsJob}
ttsInferenceJobs={this.state.ttsInferenceJobs}
enqueueW2lJob={this.enqueueW2lJob}
w2lInferenceJobs={this.state.w2lInferenceJobs}
enqueueTtsModelUploadJob={this.enqueueTtsModelUploadJob}
ttsModelUploadJobs={this.state.ttsModelUploadJobs}
enqueueW2lTemplateUploadJob={
this.enqueueW2lTemplateUploadJob
}
w2lTemplateUploadJobs={this.state.w2lTemplateUploadJobs}
enqueueVocoderUploadJob={this.enqueueVocoderUploadJob}
vocoderUploadJobs={this.state.vocoderUploadJobs}
enqueueVoiceConversionModelUploadJob={
this.enqueueVoiceConversionModelUploadJob
}
voiceConversionModelUploadJobs={
this.state.voiceConversionModelUploadJobs
}
textBuffer={this.state.textBuffer}
setTextBuffer={this.setTextBuffer}
clearTextBuffer={this.clearTextBuffer}
ttsModels={this.props.allTtsModels}
setTtsModels={this.props.setAllTtsModels}
allTtsCategories={this.props.allTtsCategories}
setAllTtsCategories={this.props.setAllTtsCategories}
computedTtsCategoryAssignments={
this.props.computedTtsCategoryAssignments
}
setComputedTtsCategoryAssignments={
this.props.setComputedTtsCategoryAssignments
}
allTtsCategoriesByTokenMap={
this.props.allTtsCategoriesByTokenMap
}
allTtsModelsByTokenMap={
this.props.allTtsModelsByTokenMap
}
ttsModelsByCategoryToken={
this.props.ttsModelsByCategoryToken
}
dropdownCategories={this.props.dropdownCategories}
setDropdownCategories={this.props.setDropdownCategories}
selectedCategories={this.props.selectedCategories}
setSelectedCategories={this.props.setSelectedCategories}
maybeSelectedTtsModel={this.props.maybeSelectedTtsModel}
setMaybeSelectedTtsModel={
this.props.setMaybeSelectedTtsModel
}
selectedTtsLanguageScope={
this.props.selectedTtsLanguageScope
}
setSelectedTtsLanguageScope={
this.props.setSelectedTtsLanguageScope
}
voiceConversionModels={this.state.voiceConversionModels}
setVoiceConversionModels={
this.setAllVoiceConversionModels
}
maybeSelectedVoiceConversionModel={
this.state.maybeSelectedVoiceConversionModel
}
setMaybeSelectedVoiceConversionModel={
this.setMaybeSelectedVoiceConversionModel
}
/>
</Route>
</Switch>
<ModalProvider>
<Switch>
<Route path="/">
<PageContainer
sessionWrapper={this.state.sessionWrapper}
querySessionAction={this.querySession}
sessionSubscriptionsWrapper={
this.state.sessionSubscriptionsWrapper
}
querySessionSubscriptionsAction={
this.querySessionSubscriptions
}
isShowingVocodesNotice={
this.state.isShowingVocodesNotice
}
clearVocodesNotice={this.clearVocodesNotice}
isShowingLangaugeNotice={
this.state.isShowingLanguageNotice
}
clearLanguageNotice={this.clearLanguageNotice}
displayLanguage={this.state.displayLanguage}
primaryLanguageCode={this.state.primaryLanguageCode}
isShowingTwitchTtsNotice={
this.state.isShowingTwitchTtsNotice
}
clearTwitchTtsNotice={this.clearTwitchTtsNotice}
isShowingPleaseFollowNotice={
this.state.isShowingPleaseFollowNotice
}
clearPleaseFollowNotice={this.clearPleaseFollowNotice}
isShowingBootstrapLanguageNotice={
this.state.isShowingBootstrapLanguageNotice
}
clearBootstrapLanguageNotice={
this.clearBootstrapLanguageNotice
}
enqueueInferenceJob={this.enqueueInferenceJob}
inferenceJobs={this.state.inferenceJobs}
inferenceJobsByCategory={
this.state.inferenceJobsByCategory
}
enqueueTtsJob={this.enqueueTtsJob}
ttsInferenceJobs={this.state.ttsInferenceJobs}
enqueueW2lJob={this.enqueueW2lJob}
w2lInferenceJobs={this.state.w2lInferenceJobs}
enqueueTtsModelUploadJob={
this.enqueueTtsModelUploadJob
}
ttsModelUploadJobs={this.state.ttsModelUploadJobs}
enqueueW2lTemplateUploadJob={
this.enqueueW2lTemplateUploadJob
}
w2lTemplateUploadJobs={
this.state.w2lTemplateUploadJobs
}
enqueueVocoderUploadJob={this.enqueueVocoderUploadJob}
vocoderUploadJobs={this.state.vocoderUploadJobs}
enqueueVoiceConversionModelUploadJob={
this.enqueueVoiceConversionModelUploadJob
}
voiceConversionModelUploadJobs={
this.state.voiceConversionModelUploadJobs
}
textBuffer={this.state.textBuffer}
setTextBuffer={this.setTextBuffer}
clearTextBuffer={this.clearTextBuffer}
ttsModels={this.props.allTtsModels}
setTtsModels={this.props.setAllTtsModels}
allTtsCategories={this.props.allTtsCategories}
setAllTtsCategories={this.props.setAllTtsCategories}
computedTtsCategoryAssignments={
this.props.computedTtsCategoryAssignments
}
setComputedTtsCategoryAssignments={
this.props.setComputedTtsCategoryAssignments
}
allTtsCategoriesByTokenMap={
this.props.allTtsCategoriesByTokenMap
}
allTtsModelsByTokenMap={
this.props.allTtsModelsByTokenMap
}
ttsModelsByCategoryToken={
this.props.ttsModelsByCategoryToken
}
dropdownCategories={this.props.dropdownCategories}
setDropdownCategories={
this.props.setDropdownCategories
}
selectedCategories={this.props.selectedCategories}
setSelectedCategories={
this.props.setSelectedCategories
}
maybeSelectedTtsModel={
this.props.maybeSelectedTtsModel
}
setMaybeSelectedTtsModel={
this.props.setMaybeSelectedTtsModel
}
selectedTtsLanguageScope={
this.props.selectedTtsLanguageScope
}
setSelectedTtsLanguageScope={
this.props.setSelectedTtsLanguageScope
}
voiceConversionModels={
this.state.voiceConversionModels
}
setVoiceConversionModels={
this.setAllVoiceConversionModels
}
maybeSelectedVoiceConversionModel={
this.state.maybeSelectedVoiceConversionModel
}
setMaybeSelectedVoiceConversionModel={
this.setMaybeSelectedVoiceConversionModel
}
/>
</Route>
</Switch>
</ModalProvider>
</SessionProvider>
</InferenceJobs>
@@ -146,6 +146,7 @@ export default function SideNav({
pending_svc_jobs: 0,
pending_tacotron2_jobs: 0,
pending_voice_designer: 0,
pending_stable_diffusion: 0,
},
},
legacy_tts: {
@@ -192,6 +193,7 @@ export default function SideNav({
variant="secondary"
onClick={() => {
history.push("/login");
handleNavLinkClick();
}}
/>
</>
@@ -204,6 +206,7 @@ export default function SideNav({
small
onClick={() => {
history.push("/signup");
handleNavLinkClick();
}}
/>
</>
@@ -228,6 +231,7 @@ export default function SideNav({
variant="secondary"
onClick={() => {
history.push(url);
handleNavLinkClick();
}}
/>
</>
@@ -242,6 +246,7 @@ export default function SideNav({
variant="danger"
onClick={async () => {
await logoutHandler();
handleNavLinkClick();
}}
/>
</>
@@ -312,10 +317,7 @@ export default function SideNav({
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faFilms}
className="sidebar-heading-icon"
/>
<FontAwesomeIcon icon={faFilms} className="sidebar-heading-icon" />
{t("videoStryleTransfer")}
</NavLink>
</li>
@@ -566,6 +568,12 @@ export default function SideNav({
{queueStats.inference.by_queue.pending_svc_jobs}
</span>
</div>
<div>
Image Generation:{" "}
<span className="text-red">
{queueStats.inference.by_queue.pending_stable_diffusion}
</span>
</div>
<div>
{t("queueFaceAnimator")}:{" "}
<span className="text-red">
@@ -26,6 +26,12 @@ export default function SideNavJobs({ byQueue, t, ttsQueuedCount }: Props) {
{ byQueue.pending_svc_jobs }
</span>
</div>
<div>
Image Generation:{" "}
<span className="text-red">
{ byQueue.pending_stable_diffusion }
</span>
</div>
<div>
{t("queueFaceAnimator")}:{" "}
<span className="text-red">
@@ -2,7 +2,6 @@ import React from "react";
import { formatDistance } from "date-fns";
import { Comment } from "@storyteller/components/src/api/comments/ListComments";
import { SafeDeleteCommentButton } from "./SafeDeleteCommentButton";
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
import { Link } from "react-router-dom";
import { useSession } from "hooks";
@@ -57,12 +56,22 @@ function CommentList(props: Props) {
}
let profileLink = `/profile/${comment.username}`;
let username = comment?.user.username;
let gravatarHash = comment?.user.gravatar_hash;
let avatarIndex = comment?.user.default_avatar.image_index;
let backgroundColorIndex = comment?.user.default_avatar.color_index;
rows.push(
<tr key={comment.token}>
<td className="px-0">
<div className="d-flex gap-3 py-3">
<Gravatar size={42} email_hash={comment.user_gravatar_hash} />
<Gravatar
email_hash={gravatarHash || ""}
username={username || ""}
avatarIndex={avatarIndex || 0}
backgroundIndex={backgroundColorIndex || 0}
size={40}
/>
<div>
<div className="d-flex gap-2 align-items-center flex-wrap">
<Link to={profileLink} className="fw-medium text-white">
@@ -24,9 +24,8 @@ function CreateCommentForm(props: Props) {
const { entityType, entityToken, loadComments } = props;
const [commentMarkdown, setCommentMarkdown] = useState<string>("");
const [uuidIdempotencyToken, setUuidIdempotencyToken] = useState<string>(
uuidv4()
);
const [uuidIdempotencyToken, setUuidIdempotencyToken] =
useState<string>(uuidv4());
const [buttonVisible, setButtonVisible] = useState(false);
const postComment = useCallback(async () => {
@@ -91,8 +90,21 @@ function CreateCommentForm(props: Props) {
let gravatarHash = user?.email_gravatar_hash;
let gravatar = <span />;
let username = user?.username;
let emailHash = user?.email_gravatar_hash;
let avatarIndex = user?.core_info.default_avatar.image_index;
let backgroundColorIndex = user?.core_info.default_avatar.color_index;
if (gravatarHash !== undefined) {
gravatar = <Gravatar email_hash={gravatarHash} size={40} />;
gravatar = (
<Gravatar
email_hash={emailHash || ""}
username={username || ""}
avatarIndex={avatarIndex || 0}
backgroundIndex={backgroundColorIndex || 0}
size={40}
/>
);
}
let commentButton = <></>;
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import MediaAudioComponent from "./MediaAudioComponent";
import MediaVideoComponent from "./MediaVideoComponent";
@@ -32,9 +32,10 @@ import { Badge, Input, Modal } from "components/common";
import BookmarkButton from "components/common/BookmarkButton";
import LikeButton from "components/common/LikeButton";
import { useBookmarks, useMedia, useRatings, useSession } from "hooks";
import SdCoverImagePanel from "../weight/cover_image_panels/SdCoverImagePanel";
import { WeightCategory } from "@storyteller/components/src/api/_common/enums/WeightCategory";
import Iframe from "react-iframe";
import SdBatchMediaPanel from "./components/SdBatchMediaPanel/SdBatchMediaPanel";
import { GetMediaBatchImages } from "@storyteller/components/src/api/media_files/GetMediaBatchImages";
export default function MediaPage() {
const { canEditTtsModel, user } = useSession();
@@ -51,11 +52,31 @@ export default function MediaPage() {
ratings.gather({ res, key: "token" });
},
});
const batchToken = mediaFile?.maybe_batch_token;
const [images, setImages] = useState<string[]>([]);
const bucketConfig = new BucketConfig();
const timeCreated = moment(mediaFile?.created_at || "").fromNow();
const dateCreated = moment(mediaFile?.created_at || "").format("LLL");
const [buttonLabel, setButtonLabel] = useState("Copy");
useEffect(() => {
if (batchToken) {
GetMediaBatchImages(batchToken, {}, {})
.then(response => {
if (response.success) {
const imageUrls = response.results.map(result =>
bucketConfig.getGcsUrl(result.public_bucket_path)
);
setImages(imageUrls);
} else {
console.error("Failed to fetch batch images");
}
})
.catch(err => console.error("Error fetching batch images:", err));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [batchToken]);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const closeDeleteModal = () => setIsDeleteModalOpen(false);
@@ -98,11 +119,17 @@ export default function MediaPage() {
);
case MediaFileType.Image:
let sdMediaImage = "/images/avatars/default-pfp.png";
let sdMediaImage = ["/images/avatars/default-pfp.png"];
if (mediaFile.public_bucket_path) {
sdMediaImage = bucketConfig.getGcsUrl(mediaFile.public_bucket_path);
sdMediaImage = [bucketConfig.getGcsUrl(mediaFile.public_bucket_path)];
}
return <SdCoverImagePanel src={sdMediaImage} />;
return (
<SdBatchMediaPanel
key={images.length}
images={mediaFile.maybe_batch_token ? images : sdMediaImage}
/>
);
case MediaFileType.BVH:
const bvhUrl = bucketConfig.getGcsUrl(mediaFile.public_bucket_path);
return (
@@ -335,8 +362,6 @@ export default function MediaPage() {
const subtitleDivider = <span className="opacity-25 fs-5 fw-light">|</span>;
const bucketConfig = new BucketConfig();
let weightUsedCoverImage = "/images/avatars/default-pfp.png";
if (
mediaFile?.maybe_model_weight_info !== null &&
@@ -438,7 +463,9 @@ export default function MediaPage() {
}}
/>
) : null}
{mediaFile?.media_type === MediaFileType.BVH || mediaFile?.media_type === MediaFileType.GLTF || mediaFile?.media_type === MediaFileType.GLB ? (
{mediaFile?.media_type === MediaFileType.BVH ||
mediaFile?.media_type === MediaFileType.GLTF ||
mediaFile?.media_type === MediaFileType.GLB ? (
<Button
{...{
icon: faVideoPlus,
@@ -0,0 +1,84 @@
@import "scss/colours.scss";
.swiper {
width: 100%;
height: 300px;
margin: 0;
margin-left: auto;
margin-right: auto;
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
}
.swiper-slide {
background-size: contain;
background-position: center;
}
.secondSwiper {
width: 100%;
}
.landscape {
height: 100%;
}
.portrait {
height: 70vh;
}
// .secondSwiper .swiper-slide {
// height: 100%;
// }
.firstSwiper {
height: 20%;
box-sizing: border-box;
}
.firstSwiper .swiper-slide {
transition: 0.2s;
width: 25%;
height: 100%;
opacity: 0.4;
}
.firstSwiper .swiper-slide-thumb-active {
opacity: 1;
}
.swiper-slide img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 0.5rem;
cursor: pointer;
transition: 0.2s;
background-color: $panel;
}
.swiper-button-prev,
.swiper-button-next {
color: $white;
opacity: 0.5;
transition: 0.2s;
&:hover {
opacity: 1;
}
}
@media screen and (max-width: 768px) {
.portrait {
height: 550px;
}
}
@media screen and (max-width: 500px) {
.portrait {
height: 400px;
}
}
@@ -0,0 +1,90 @@
import { Panel } from "components/common";
import React, { useEffect, useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/thumbs";
import { FreeMode, Navigation, Thumbs } from "swiper/modules";
import { Swiper as SwiperType } from "swiper/types";
import "./SdBatchMediaPanel.scss";
interface SdCoverImagePanelProps {
images: string[];
}
export default function SdBatchMediaPanel({ images }: SdCoverImagePanelProps) {
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
const [forceUpdateKey, setForceUpdateKey] = useState(0);
const [isPortrait, setIsPortrait] = useState(false);
useEffect(() => {
images.forEach(imgSrc => {
const img = new Image();
img.onload = () => {
// If any image is portrait, update isPortrait to true
if (img.height > img.width && !isPortrait) {
setIsPortrait(true);
}
};
img.src = imgSrc;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [images]);
useEffect(() => {
if (images.length > 0) {
// Force re-render of the Swiper component by updating a key
setForceUpdateKey(prevKey => prevKey + 1);
}
}, [images]);
const handleSwiper = (swiper: SwiperType) => {
setThumbsSwiper(swiper);
};
const secondSwiperClass = `secondSwiper ${
isPortrait ? "portrait" : "landscape"
}`;
return (
<Panel padding={false} clear className="d-flex flex-column gap-3">
<Swiper
key={forceUpdateKey}
loop={images.length > 1}
spaceBetween={10}
navigation={images.length > 1}
thumbs={{ swiper: images.length > 1 ? thumbsSwiper : null }}
modules={[FreeMode, Navigation, Thumbs]}
className={secondSwiperClass}
slidesPerView={1}
initialSlide={0}
>
{images.map((imgSrc, index) => (
<SwiperSlide key={index}>
<img src={imgSrc} alt={`Slide ${index + 1}`} />
</SwiperSlide>
))}
</Swiper>
{images.length > 1 && (
<Swiper
onSwiper={handleSwiper}
loop={true}
spaceBetween={10}
slidesPerView={5}
freeMode={true}
watchSlidesProgress={true}
modules={[FreeMode, Navigation, Thumbs]}
className="firstSwiper"
initialSlide={0}
>
{images.map((imgSrc, index) => (
<SwiperSlide key={index}>
<img src={imgSrc} alt={`Thumbnail ${index + 1}`} />
</SwiperSlide>
))}
</Swiper>
)}
</Panel>
);
}
@@ -197,6 +197,12 @@ export default function WeightPage({
return (
<div className="d-flex flex-column gap-3">
<SdCoverImagePanel src={sdCoverImage} />
{descriptionMD !== "" && (
<Panel padding={true}>
<h5 className="fw-semibold mb-2">Description</h5>
<p className="fs-7">{descriptionMD}</p>
</Panel>
)}
{imageGenPanel}
</div>
);
@@ -488,14 +494,15 @@ export default function WeightPage({
<div className="row g-4">
<div className="col-12 col-xl-8 d-flex flex-column gap-3">
<div className="media-wrapper">{renderWeightComponent(weight)}</div>
{descriptionMD !== "" &&
weight.weight_category !== WeightCategory.SD && (
<Panel padding={true}>
<h5 className="fw-semibold mb-2">Description</h5>
<p className="fs-7">{descriptionMD}</p>
</Panel>
)}
{descriptionMD !== "" && (
<Panel padding={true}>
<h4 className="fw-semibold mb-3">Description</h4>
<p>{descriptionMD}</p>
</Panel>
)}
<div className="media-wrapper">{renderWeightComponent(weight)}</div>
<div className="panel p-3 py-4 p-md-4 d-none d-xl-block">
<h4 className="fw-semibold mb-3">Comments</h4>
+1
View File
@@ -6,6 +6,7 @@ MASTER_BRANCH='master'
PRODUCTION_BRANCH='production-fakeyou'
git stash
git branch --set-upstream-to="origin/${MASTER_BRANCH}" ${MASTER_BRANCH}
git checkout $MASTER_BRANCH
git pull
+8
View File
@@ -4484,6 +4484,7 @@ __metadata:
react-twitch-embed: ^3.0.1
react-wavesurfer.js: ^0.0.8
sass: ^1.57.1
swiper: ^11.0.5
typescript: ^3.7.5
universal-cookie: ^6.1.1
uuid: ^8.3.2
@@ -20178,6 +20179,13 @@ __metadata:
languageName: node
linkType: hard
"swiper@npm:^11.0.5":
version: 11.0.5
resolution: "swiper@npm:11.0.5"
checksum: 83b125d16733a66ed5f58627c81263116e39eea5cead92667d2e24692ff94632de9aa05fec1ed80dc352779c0d7525e9a18424fce1c39c5e970823414b212edb
languageName: node
linkType: hard
"swiper@npm:^8.3.2":
version: 8.3.2
resolution: "swiper@npm:8.3.2"