mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-23 10:09:29 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fc99b7946 | ||
|
|
48cb0334f3 |
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user