Compare commits

...
Author SHA1 Message Date
bflatastic 9fc99b7946 videos tab layout 2025-05-19 10:56:48 +07:00
bflatastic 48cb0334f3 updates to gallery modal and lightbox - added videos tab 2025-05-19 10:53:29 +07:00
7 changed files with 122 additions and 83 deletions
@@ -143,7 +143,7 @@ export const TopBar = ({
{currentAppTabId === "3D" ? (
<SceneTitleInput pageName={pageName} />
) : (
<h1>{currentAppTabId === "2D" ? "Canvas" : "Generate"}</h1>
<h1>{currentAppTabId === "2D" ? "Canvas" : "Generate Video"}</h1>
)}
</div>
@@ -185,8 +185,9 @@ export const TopBar = ({
onClose={() => setIsLibraryModalOpen(false)}
mode="view"
tabs={[
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
{ id: "my-media", label: "Generations" },
{ id: "uploads", label: "Uploads" },
{ id: "videos", label: "Videos" },
]}
activeTab={activeLibraryTab}
onTabChange={setActiveLibraryTab}
@@ -132,7 +132,10 @@ export const GalleryModal = React.memo(
setLoading(true);
try {
const response = await api.listUserMediaFiles({
filter_media_classes: [FilterMediaClasses.IMAGE],
filter_media_classes:
activeTab === "videos"
? [FilterMediaClasses.VIDEO]
: [FilterMediaClasses.IMAGE],
username: username,
include_user_uploads: activeTab === "uploads",
user_uploads_only: activeTab === "uploads",
@@ -145,10 +148,13 @@ export const GalleryModal = React.memo(
const newItems = response.data.map((item: any) => ({
id: item.token,
label: item.maybe_title || "Image Generation",
thumbnail: item.media_links.maybe_thumbnail_template?.replace(
"{WIDTH}",
thumbnail_size.toString()
),
thumbnail:
activeTab === "videos"
? item.media_links.maybe_video_previews.still
: item.media_links.maybe_thumbnail_template?.replace(
"{WIDTH}",
thumbnail_size.toString()
),
fullImage: item.media_links.cdn_url,
createdAt: item.created_at,
}));
@@ -252,15 +258,23 @@ export const GalleryModal = React.memo(
</h3>
<div
className={twMerge(
"grid grid-cols-5 gap-1",
mode === "view" && "grid-cols-5"
activeTab === "videos"
? "grid grid-cols-3 gap-2"
: "grid grid-cols-5 gap-1",
mode === "view" &&
(activeTab === "videos"
? "grid-cols-3"
: "grid-cols-5")
)}
>
{dateItems.map((item) => (
<button
key={item.id}
className={twMerge(
"group relative aspect-square overflow-hidden rounded-md border-[3px] transition-all",
"group relative overflow-hidden rounded-md border-[3px] transition-all",
activeTab === "videos"
? "aspect-video"
: "aspect-square",
mode === "select" &&
selectedItemIds.includes(item.id)
? "border-primary"
@@ -277,10 +291,14 @@ export const GalleryModal = React.memo(
</div>
) : (
<img
//src={item.thumbnail}
src={item.fullImage || item.thumbnail}
src={item.thumbnail || item.fullImage || ""}
alt={item.label}
className="h-full w-full object-cover"
className={twMerge(
"h-full w-full bg-black/30",
activeTab === "videos"
? "object-contain"
: "object-cover"
)}
onError={() =>
handleImageError(item.thumbnail!)
}
@@ -351,6 +369,7 @@ export const GalleryModal = React.memo(
mediaId={lightboxImage?.id}
onDownloadClicked={onDownloadClicked}
onAddToSceneClicked={onAddToSceneClicked}
activeTab={activeTab}
/>
</>
);
@@ -4,7 +4,10 @@ import { Transition, TransitionChild } from "@headlessui/react";
import { createPortal } from "react-dom";
import dayjs from "dayjs";
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
import { FalKlingImageToVideo, FalHunyuanImageTo3d } from "@storyteller/tauri-api"
import {
FalKlingImageToVideo,
FalHunyuanImageTo3d,
} from "@storyteller/tauri-api";
interface LightboxModalProps {
isOpen: boolean;
@@ -23,6 +26,7 @@ interface LightboxModalProps {
url: string,
media_id: string | undefined
) => Promise<void>;
activeTab?: string;
}
export function LightboxModal({
@@ -39,6 +43,7 @@ export function LightboxModal({
mediaId, // media id of the image
onDownloadClicked,
onAddToSceneClicked,
activeTab,
}: LightboxModalProps) {
return createPortal(
<Transition appear show={isOpen}>
@@ -89,6 +94,11 @@ export function LightboxModal({
<div className="flex h-full w-full items-center justify-center bg-gray-800">
<span className="text-white/60">Image not available</span>
</div>
) : activeTab === "videos" ? (
<video controls className="h-full w-full object-contain">
<source src={imageUrl} type="video/mp4" />
Your browser does not support the video tag.
</video>
) : (
<img
src={imageUrl}
@@ -103,7 +113,7 @@ export function LightboxModal({
<div className="flex h-full flex-col py-5 pe-5">
<div className="flex-1 space-y-4">
<div className="text-xl font-medium">
{title || "Image Generation"}
{title || (activeTab === "videos" ? "Video" : "Image")}
</div>
{createdAt && (
<div className="text-sm text-white/60">
@@ -117,50 +127,52 @@ export function LightboxModal({
{/* buttons with spacing */}
{(onAddToSceneClicked && downloadUrl) || downloadUrl ? (
<div className="mt-15 mb-15 flex justify-end gap-2">
{activeTab !== "videos" && (
<>
<Button
onClick={async (e) => {
let _result = await FalKlingImageToVideo({
image_media_token: mediaId,
//base64_image: downloadUrl,
});
//e.stopPropagation();
//await onAddToSceneClicked(downloadUrl, mediaId);
//onClose(); // close the lightbox
//onCloseGallery(); // close the gallery
}}
>
Video
</Button>
<Button
onClick={async (e) => {
let _result = await FalKlingImageToVideo({
image_media_token: mediaId,
//base64_image: downloadUrl,
});
//e.stopPropagation();
//await onAddToSceneClicked(downloadUrl, mediaId);
//onClose(); // close the lightbox
//onCloseGallery(); // close the gallery
}}
>
Video
</Button>
<Button
onClick={async (e) => {
let _result = await FalHunyuanImageTo3d({
image_media_token: mediaId,
//base64_image: downloadUrl,
});
//e.stopPropagation();
//await onAddToSceneClicked(downloadUrl, mediaId);
//onClose(); // close the lightbox
//onCloseGallery(); // close the gallery
}}
>
3D
</Button>
<Button
onClick={async (e) => {
let _result = await FalHunyuanImageTo3d({
image_media_token: mediaId,
//base64_image: downloadUrl,
});
//e.stopPropagation();
//await onAddToSceneClicked(downloadUrl, mediaId);
//onClose(); // close the lightbox
//onCloseGallery(); // close the gallery
}}
>
3D
</Button>
{onAddToSceneClicked && downloadUrl && (
<Button
onClick={async (e) => {
e.stopPropagation();
await onAddToSceneClicked(downloadUrl, mediaId);
onClose(); // close the lightbox
onCloseGallery(); // close the gallery
}}
>
Add to Current Scene
</Button>
{onAddToSceneClicked && downloadUrl && (
<Button
onClick={async (e) => {
e.stopPropagation();
await onAddToSceneClicked(downloadUrl, mediaId);
onClose(); // close the lightbox
onCloseGallery(); // close the gallery
}}
>
Add to Current Scene
</Button>
)}
</>
)}
{downloadUrl &&
(onDownloadClicked ? (
<Button
@@ -230,7 +230,7 @@ export const PopoverMenu = ({
>
<div
className={twMerge(
"z-10 w-56 mt-2 rounded-lg bg-[#46464B] p-1.5 shadow-lg",
"z-10 min-w-48 mt-2 rounded-lg bg-[#46464B] p-1.5 shadow-lg",
position === "top" ? "mb-2" : "mt-2",
panelClassName
)}
@@ -631,8 +631,8 @@ export const PromptBox2D = ({
maxSelections={4}
onUseSelected={handleGalleryImages}
tabs={[
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
{ id: "my-media", label: "Generations" },
{ id: "uploads", label: "Uploads" },
]}
activeTab={activeGalleryTab}
onTabChange={setActiveGalleryTab}
@@ -1,6 +1,5 @@
import { useState, useRef, useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { invoke } from "@tauri-apps/api/core";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { downloadFileFromUrl } from "@storyteller/api";
import {
@@ -48,7 +47,12 @@ import { twMerge } from "tailwind-merge";
import { GalleryModal, GalleryItem } from "@storyteller/ui-gallery-modal";
import { Modal } from "@storyteller/ui-modal";
import { Signal } from "@preact/signals-react";
import { CommandSuccessStatus, GetAppPreferences, SoraImageRemix, SoraImageRemixErrorType } from "@storyteller/tauri-api";
import {
CommandSuccessStatus,
GetAppPreferences,
SoraImageRemix,
SoraImageRemixErrorType,
} from "@storyteller/tauri-api";
interface ReferenceImage {
id: string;
@@ -75,10 +79,12 @@ interface PromptBox3DProps {
handleCameraFocalLengthChange: (id: string, value: number) => void;
onAspectRatioSelect: (newRatio: CameraAspectRatio) => void;
setEnginePrompt: (prompt: string) => void;
snapshotCurrentFrame: ((shouldDownload?: boolean) => {
base64Snapshot: string;
file: File;
} | null) | undefined;
snapshotCurrentFrame:
| ((shouldDownload?: boolean) => {
base64Snapshot: string;
file: File;
} | null)
| undefined;
}
export const PromptBox3D = ({
@@ -169,7 +175,7 @@ export const PromptBox3D = ({
prev.map((item) => ({
...item,
selected: item.label === selectedItem.label,
})),
}))
);
// Map the selected label to the corresponding CameraAspectRatio enum value
@@ -194,7 +200,6 @@ export const PromptBox3D = ({
onAspectRatioSelect(newRatio);
};
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
@@ -388,11 +393,11 @@ export const PromptBox3D = ({
setisEnqueueing(true);
setTimeout(() => {
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
// the backend hangs on a given request. (Sometimes Sora doesn't respond.) Ideally what we would do instead is only
// force the prompt box to be enabled again as long as another request isn't running, ie. if we just fired off two
// requests in succession, we wouldn't want to turn it off as the first one gets submitted.
console.debug('Turn off blocking of prompt box...');
console.debug("Turn off blocking of prompt box...");
setisEnqueueing(false);
}, 10000);
@@ -416,7 +421,7 @@ export const PromptBox3D = ({
disable_system_prompt: !useSystemPrompt,
prompt: prompt,
maybe_additional_images: referenceImages.map(
(image) => image.mediaToken,
(image) => image.mediaToken
),
maybe_number_of_samples: 1,
});
@@ -433,7 +438,6 @@ export const PromptBox3D = ({
toast.success("Image added to queue");
setisEnqueueing(false);
return;
} else {
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.enqueue_failure_sound;
@@ -447,19 +451,23 @@ export const PromptBox3D = ({
if ("error_type" in generateResponse) {
switch (generateResponse.error_type) {
case SoraImageRemixErrorType.SoraLoginRequired:
errorMessage = "You need to log into Sora to continue. See the settings menu.";
errorMessage =
"You need to log into Sora to continue. See the settings menu.";
break;
case SoraImageRemixErrorType.ServerError:
errorMessage = "Server error. Failed to enqueue image.";
break;
case SoraImageRemixErrorType.TooManyConcurrentTasks:
errorMessage = "You have too many Sora image generation tasks running. Please wait a moment.";
errorMessage =
"You have too many Sora image generation tasks running. Please wait a moment.";
break;
case SoraImageRemixErrorType.SoraUsernameNotYetCreated:
errorMessage = "Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
errorMessage =
"Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
break;
case SoraImageRemixErrorType.SoraIsHavingProblems:
errorMessage = "Sora is having problems. Please try again later.";
errorMessage =
"Sora is having problems. Please try again later.";
break;
}
}
@@ -467,7 +475,6 @@ export const PromptBox3D = ({
setisEnqueueing(false);
return;
}
}
try {
@@ -813,8 +820,8 @@ export const PromptBox3D = ({
maxSelections={4}
onUseSelected={handleGalleryImages}
tabs={[
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
{ id: "my-media", label: "Generations" },
{ id: "uploads", label: "Uploads" },
]}
activeTab={activeGalleryTab}
onTabChange={setActiveGalleryTab}
@@ -506,7 +506,7 @@ export const PromptBoxVideo = ({
panelTitle="Add Image"
items={[
{
label: "Choose from Assets",
label: "Choose from library",
selected: false,
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
action: "gallery",
@@ -523,7 +523,7 @@ export const PromptBoxVideo = ({
<textarea
ref={textareaRef}
rows={1}
placeholder="Choose the image from assets (+) and describe what you want to happen in the video..."
placeholder="Choose the image from library (+) and describe what you want to happen in the video..."
className="text-md mb-2 max-h-[5.5em] flex-1 resize-none overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-white placeholder-white placeholder:text-white/60 focus:outline-none"
value={prompt}
onChange={handleChange}
@@ -602,8 +602,8 @@ export const PromptBoxVideo = ({
maxSelections={4}
onUseSelected={handleGalleryImages}
tabs={[
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
{ id: "my-media", label: "Generations" },
{ id: "uploads", label: "Uploads" },
]}
activeTab={activeGalleryTab}
onTabChange={setActiveGalleryTab}