api fixes and sort only user uploads on my uploads tab, shared gallery button for both 2d and 3d

This commit is contained in:
bflatastic committed 2025-04-24 19:21:01 +07:00
1 parent e5627fd225
commit 1573c927bf
17 files changed
+356 -275

No files matched your search

+2 -3
View File
@@ -22,7 +22,6 @@
"preview": "vite preview"
},
"dependencies": {
"@devnomic/marquee": "^1.0.3",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
@@ -52,8 +51,8 @@
"@preact/signals-react-transform": "^0.4.0",
"@tauri-apps/cli": "^2.4.0",
"@types/deep-equal": "^1.0.4",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { Marquee } from "@devnomic/marquee";
// import { Marquee } from "@devnomic/marquee";
import "@devnomic/marquee/dist/index.css";
function getRandomImages(count: number) {
@@ -21,16 +21,18 @@ interface ImageColumnProps {
duration?: number;
}
function ImageColumn({
images,
delay,
reverse = false,
duration = 100,
isLoaded,
}: ImageColumnProps) {
function ImageColumn(
{
// images,
// delay,
// reverse = false,
// duration = 100,
// isLoaded,
}: ImageColumnProps,
) {
return (
<div className="flex h-screen flex-col gap-6">
<Marquee
{/* <Marquee
fade={false}
direction="up"
reverse={reverse}
@@ -59,7 +61,7 @@ function ImageColumn({
/>
</div>
))}
</Marquee>
</Marquee> */}
</div>
);
}
@@ -36,6 +36,7 @@ import { PopoverItem } from "~/components/reusable/Popover";
import { Api, ApiManager } from "~/KonvaApp/Api";
import { useJobContext } from "~/components/JobContext";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
interface ReferenceImage {
id: string;
@@ -56,6 +57,11 @@ export const PromptBox = () => {
const [prompt, setPrompt] = useState("");
const [isEnqueueing, setisEnqueueing] = useState(false);
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
[],
);
const [activeGalleryTab, setActiveGalleryTab] = useState("my-media");
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
const [uploadingImages, setUploadingImages] = useState<
{ id: string; file: File }[]
@@ -153,8 +159,40 @@ export const PromptBox = () => {
fileInputRef.current?.click();
};
const handleLibrarySelect = () => {
console.log("Opening library selector");
const handleGallerySelect = () => {
setIsGalleryModalOpen(true);
};
const handleGalleryClose = () => {
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
const handleImageSelect = (id: string) => {
setSelectedGalleryImages((prev) => {
if (prev.includes(id)) {
return prev.filter((imageId) => imageId !== id);
}
if (prev.length >= 4) {
return prev;
}
return [...prev, id];
});
};
const handleGalleryImages = (selectedItems: GalleryItem[]) => {
selectedItems.forEach((item) => {
if (!item.fullImage) return;
const referenceImage: ReferenceImage = {
id: Math.random().toString(36).substring(7),
url: item.fullImage,
file: new File([], "library-image"),
mediaToken: item.id,
};
setReferenceImages((prev) => [...prev, referenceImage]);
});
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
const handleAction = (action: string) => {
@@ -162,8 +200,8 @@ export const PromptBox = () => {
case "upload":
handleUploadClick();
break;
case "library":
handleLibrarySelect();
case "gallery":
handleGallerySelect();
break;
default:
console.log("Unknown action:", action);
@@ -292,7 +330,7 @@ export const PromptBox = () => {
);
const isDesktop = IsDesktopApp();
console.log('Is this a desktop app?', isDesktop);
console.log("Is this a desktop app?", isDesktop);
if (isDesktop) {
handleTauriEnqueue();
@@ -324,158 +362,176 @@ export const PromptBox = () => {
};
return (
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
{(referenceImages.length > 0 || uploadingImages.length > 0) && (
<div className="flex w-full gap-2">
{referenceImages.map((image) => (
<div
key={image.id}
className="glass relative aspect-square w-20 rounded-lg"
>
<img
src={image.url}
alt="Reference"
className="h-full w-full rounded-lg object-cover"
/>
<button
onClick={() => handleRemoveReference(image.id)}
className="absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-md transition-colors hover:bg-black"
>
<FontAwesomeIcon icon={faTimes} className="h-2.5 w-2.5" />
</button>
</div>
))}
{uploadingImages.map(({ id, file }) => {
const previewUrl = URL.createObjectURL(file);
return (
<>
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
{(referenceImages.length > 0 || uploadingImages.length > 0) && (
<div className="flex w-full gap-2">
{referenceImages.map((image) => (
<div
key={id}
className="glass relative aspect-square w-20 overflow-hidden rounded-lg"
key={image.id}
className="glass relative aspect-square w-20 rounded-lg"
>
<div className="absolute inset-0">
<img
src={previewUrl}
alt="Uploading preview"
className="h-full w-full object-cover blur-sm"
/>
<img
src={image.url}
alt="Reference"
className="h-full w-full rounded-lg object-cover"
/>
<button
onClick={() => handleRemoveReference(image.id)}
className="absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-md transition-colors hover:bg-black"
>
<FontAwesomeIcon icon={faTimes} className="h-2.5 w-2.5" />
</button>
</div>
))}
{uploadingImages.map(({ id, file }) => {
const previewUrl = URL.createObjectURL(file);
return (
<div
key={id}
className="glass relative aspect-square w-20 overflow-hidden rounded-lg"
>
<div className="absolute inset-0">
<img
src={previewUrl}
alt="Uploading preview"
className="h-full w-full object-cover blur-sm"
/>
</div>
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
<FontAwesomeIcon
icon={faSpinnerThird}
className="h-6 w-6 animate-spin text-white"
/>
</div>
</div>
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
);
})}
</div>
)}
<div className="glass w-[730px] rounded-xl p-4">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleFileUpload}
multiple
/>
<div className="flex justify-center gap-2">
<PopoverMenu
mode="button"
panelTitle="Add Image"
items={[
{
label: "Upload from device",
selected: false,
icon: <FontAwesomeIcon icon={faUpload} className="h-4 w-4" />,
action: "upload",
},
{
label: "Choose from library",
selected: false,
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
action: "gallery",
},
]}
onPanelAction={handleAction}
showIconsInList
buttonClassName="backdrop-blur-none bg-transparent hover:bg-transparent py-1.5 px-0 pr-1 m-0 hover:opacity-50 transition-opacity duration-100 ring-0 border-none focus:ring-0 outline-none"
triggerIcon={
<FontAwesomeIcon icon={faPlus} className="text-xl" />
}
/>
<textarea
ref={textareaRef}
rows={1}
placeholder="Describe your image..."
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}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
onFocus={() => {}}
onBlur={() => {}}
/>
</div>
<div className="mt-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Tooltip
content="Aspect ratio"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={aspectRatioList}
onSelect={handleAspectRatioSelect}
mode="toggle"
panelTitle="Aspect Ratio"
showIconsInList
triggerIcon={
<FontAwesomeIcon
icon={getCurrentAspectRatioIcon()}
className="h-4 w-4"
/>
}
/>
</Tooltip>
<Tooltip
content={
useSystemPrompt
? "Use system prompt: ON"
: "Use system prompt: OFF"
}
position="top"
className="z-50"
delay={200}
>
<ToggleButton
isActive={useSystemPrompt}
icon={faMessageXmark}
activeIcon={faMessageCheck}
onClick={() => setUseSystemPrompt(!useSystemPrompt)}
/>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={!isEnqueueing ? faSparkles : undefined}
onClick={handleEnqueue}
disabled={isEnqueueing || !prompt.trim()}
>
{isEnqueueing ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="h-6 w-6 animate-spin text-white"
className="animate-spin text-lg"
/>
</div>
</div>
);
})}
</div>
)}
<div className="glass w-[730px] rounded-xl p-4">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleFileUpload}
multiple
/>
<div className="flex justify-center gap-2">
<PopoverMenu
mode="button"
panelTitle="Add Image"
items={[
{
label: "Upload from device",
selected: false,
icon: <FontAwesomeIcon icon={faUpload} className="h-4 w-4" />,
action: "upload",
},
{
label: "Choose from library",
selected: false,
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
action: "library",
disabled: true,
},
]}
onPanelAction={handleAction}
showIconsInList
buttonClassName="backdrop-blur-none bg-transparent hover:bg-transparent py-1.5 px-0 pr-1 m-0 hover:opacity-50 transition-opacity duration-100 ring-0 border-none focus:ring-0 outline-none"
triggerIcon={<FontAwesomeIcon icon={faPlus} className="text-xl" />}
/>
<textarea
ref={textareaRef}
rows={1}
placeholder="Describe your image..."
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}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
onFocus={() => {}}
onBlur={() => {}}
/>
</div>
<div className="mt-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Tooltip
content="Aspect ratio"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={aspectRatioList}
onSelect={handleAspectRatioSelect}
mode="toggle"
panelTitle="Aspect Ratio"
showIconsInList
triggerIcon={
<FontAwesomeIcon
icon={getCurrentAspectRatioIcon()}
className="h-4 w-4"
/>
}
/>
</Tooltip>
<Tooltip
content={
useSystemPrompt
? "Use system prompt: ON"
: "Use system prompt: OFF"
}
position="top"
className="z-50"
delay={200}
>
<ToggleButton
isActive={useSystemPrompt}
icon={faMessageXmark}
activeIcon={faMessageCheck}
onClick={() => setUseSystemPrompt(!useSystemPrompt)}
/>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={!isEnqueueing ? faSparkles : undefined}
onClick={handleEnqueue}
disabled={isEnqueueing || !prompt.trim()}
>
{isEnqueueing ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin text-lg"
/>
) : (
"Generate"
)}
</Button>
) : (
"Generate"
)}
</Button>
</div>
</div>
</div>
</div>
</div>
<GalleryModal
isOpen={isGalleryModalOpen}
onClose={handleGalleryClose}
mode="select"
selectedItemIds={selectedGalleryImages}
onSelectItem={handleImageSelect}
maxSelections={4}
onUseSelected={handleGalleryImages}
tabs={[
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
]}
activeTab={activeGalleryTab}
onTabChange={setActiveGalleryTab}
/>
</>
);
};
@@ -1,15 +1,16 @@
import { faGear } from "@fortawesome/pro-solid-svg-icons";
import { faGear, faImages } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { Activity } from "~/components/ui/Activity/Activity";
// import { paperWrapperStyles } from "~/components/styles";
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
// import { ToolbarButton } from "~/components/features/ToolbarButton";
import { GalleryModal } from "@storyteller/ui-gallery-modal";
import { Button } from "@storyteller/ui-button";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { useState } from "react";
export const ToolbarTopRight = () => {
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
const [activeGalleryTab, setActiveGalleryTab] = useState("my-media");
return (
<>
<div
@@ -20,6 +21,13 @@ export const ToolbarTopRight = () => {
className="h-[42px] w-[42px] bg-[#5F5F68]/60 backdrop-blur-lg hover:bg-[#5F5F68]/90"
onClick={() => setIsSettingsModalOpen(true)}
/>
<Button
icon={faImages}
className="h-[42px] bg-[#5F5F68]/60 backdrop-blur-lg hover:bg-[#5F5F68]/90"
onClick={() => setIsGalleryModalOpen(true)}
>
My Gallery
</Button>
<Activity />
</div>
@@ -27,6 +35,18 @@ export const ToolbarTopRight = () => {
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
/>
<GalleryModal
isOpen={isGalleryModalOpen}
onClose={() => setIsGalleryModalOpen(false)}
mode="view"
tabs={[
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
]}
activeTab={activeGalleryTab}
onTabChange={setActiveGalleryTab}
/>
</>
);
};
+3
View File
@@ -34,6 +34,9 @@
{
"path": "../../libs/components/button/tsconfig.lib.json"
},
{
"path": "../../libs/components/gallery-modal/tsconfig.lib.json"
},
{
"path": "../../libs/tauri-utils/tsconfig.lib.json"
},
+3
View File
@@ -7,6 +7,9 @@
{
"path": "../../libs/components/button"
},
{
"path": "../../libs/components/gallery-modal"
},
{
"path": "../../libs/tauri-utils"
},
@@ -11,7 +11,7 @@ import { AuthButtons } from "./AuthButtons";
import { SceneTitleInput } from "./SceneTitleInput";
import { getCurrentLocationWithoutParams } from "~/utilities";
import { Activity } from "~/pages/PageEnigma/comps/GenerateModals/Activity";
import { LibraryModal } from "~/pages/PageEnigma/comps/LibraryModal/LibraryModal";
import { GalleryModal } from "@storyteller/ui-gallery-modal";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { Tooltip } from "@storyteller/ui-tooltip";
@@ -106,13 +106,13 @@ export const TopBar = ({ pageName }: Props) => {
onClose={() => setIsSettingsModalOpen(false)}
/>
<LibraryModal
<GalleryModal
isOpen={isLibraryModalOpen}
onClose={() => setIsLibraryModalOpen(false)}
mode="view"
tabs={[
{ id: "my-media", label: "My media" },
{ id: "uploads", label: "Uploads" },
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
]}
activeTab={activeLibraryTab}
onTabChange={setActiveLibraryTab}
@@ -38,8 +38,6 @@ import {
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { LibraryModal } from "../LibraryModal/LibraryModal";
import type { LibraryItem } from "../LibraryModal/LibraryModal";
import {
cameras,
selectedCameraId,
@@ -61,6 +59,7 @@ import { EngineApi } from "~/Classes/ApiManager/EngineApi";
import { UploaderStates } from "~/enums/UploaderStates";
import { CameraSettingsModal } from "../CameraSettingsModal";
import { twMerge } from "tailwind-merge";
import { GalleryModal, GalleryItem } from "@storyteller/ui-gallery-modal";
export const PromptBox = () => {
useSignals();
@@ -92,11 +91,11 @@ export const PromptBox = () => {
},
]);
const [isCameraSettingsOpen, setIsCameraSettingsOpen] = useState(false);
const [isLibraryModalOpen, setIsLibraryModalOpen] = useState(false);
const [selectedLibraryImages, setSelectedLibraryImages] = useState<string[]>(
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
[],
);
const [activeLibraryTab, setActiveLibraryTab] = useState("my-media");
const [activeGalleryTab, setActiveGalleryTab] = useState("my-media");
useEffect(() => {
if (textareaRef.current) {
@@ -359,17 +358,17 @@ export const PromptBox = () => {
fileInputRef.current?.click();
};
const handleLibrarySelect = () => {
setIsLibraryModalOpen(true);
const handleGallerySelect = () => {
setIsGalleryModalOpen(true);
};
const handleLibraryClose = () => {
setIsLibraryModalOpen(false);
setSelectedLibraryImages([]);
const handleGalleryClose = () => {
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
const handleImageSelect = (id: string) => {
setSelectedLibraryImages((prev) => {
setSelectedGalleryImages((prev) => {
if (prev.includes(id)) {
return prev.filter((imageId) => imageId !== id);
}
@@ -380,19 +379,19 @@ export const PromptBox = () => {
});
};
const handleLibraryImages = (selectedItems: LibraryItem[]) => {
const handleGalleryImages = (selectedItems: GalleryItem[]) => {
selectedItems.forEach((item) => {
if (!item.fullImage) return;
const referenceImage: ReferenceImage = {
id: Math.random().toString(36).substring(7),
url: item.fullImage,
file: new File([], "library-image"),
file: new File([], "gallery-image"),
mediaToken: item.id,
};
setReferenceImages((prev) => [...prev, referenceImage]);
});
setIsLibraryModalOpen(false);
setSelectedLibraryImages([]);
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
const handleAction = (action: string) => {
@@ -400,8 +399,8 @@ export const PromptBox = () => {
case "upload":
handleUploadClick();
break;
case "library":
handleLibrarySelect();
case "gallery":
handleGallerySelect();
break;
default:
console.log("Unknown action:", action);
@@ -581,7 +580,7 @@ export const PromptBox = () => {
label: "Choose from library",
selected: false,
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
action: "library",
action: "gallery",
},
]}
onPanelAction={handleAction}
@@ -762,20 +761,20 @@ export const PromptBox = () => {
onDeleteCamera={deleteCamera}
/>
</div>
<LibraryModal
isOpen={isLibraryModalOpen}
onClose={handleLibraryClose}
<GalleryModal
isOpen={isGalleryModalOpen}
onClose={handleGalleryClose}
mode="select"
selectedItemIds={selectedLibraryImages}
selectedItemIds={selectedGalleryImages}
onSelectItem={handleImageSelect}
maxSelections={4}
onUseSelected={handleLibraryImages}
onUseSelected={handleGalleryImages}
tabs={[
{ id: "my-media", label: "My media" },
{ id: "uploads", label: "Uploads" },
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
]}
activeTab={activeLibraryTab}
onTabChange={setActiveLibraryTab}
activeTab={activeGalleryTab}
onTabChange={setActiveGalleryTab}
/>
</>
);
+3
View File
@@ -44,6 +44,9 @@
},
{
"path": "../../libs/components/settings-modal"
},
{
"path": "../../libs/components/gallery-modal"
}
]
}
+6 -3
View File
@@ -6,12 +6,12 @@ import {
} from "lib/enums/QueryFilters.js";
interface ListUserMediaQuery {
include_user_uploads?: boolean;
username: string;
filter_media_classes?: FilterMediaClasses[];
filter_media_type?: FilterMediaType[];
filter_engine_categories?: FilterEngineCategories[];
page?: number;
limit?: number;
user_uploads_only?: boolean;
include_user_uploads?: boolean;
}
export class LibraryModalApi extends MediaFilesApi {
@@ -25,6 +25,9 @@ export class LibraryModalApi extends MediaFilesApi {
filter_media_classes: query.filter_media_classes,
filter_media_type: query.filter_media_type,
filter_engine_categories: query.filter_engine_categories,
username: query.username,
user_uploads_only: query.user_uploads_only,
include_user_uploads: query.include_user_uploads,
});
}
}
+19 -11
View File
@@ -27,6 +27,8 @@ interface ListUserMediaQuery {
filter_media_classes?: FilterMediaClasses[];
filter_media_type?: FilterMediaType[];
filter_engine_categories?: FilterEngineCategories[];
user_uploads_only?: boolean;
include_user_uploads?: boolean;
}
interface SearchFeaturedMediaQuery {
@@ -115,7 +117,7 @@ export class MediaFilesApi extends ApiManager {
}
public async ListMediaFiles(
query: ListMediaQuery,
query: ListMediaQuery
): Promise<ApiResponse<MediaFile[], PaginationInfinite>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list`;
const queryWithStrings = {
@@ -149,7 +151,7 @@ export class MediaFilesApi extends ApiManager {
}
public async ListFeaturedMediaFiles(
query: ListMediaQuery,
query: ListMediaQuery
): Promise<ApiResponse<MediaFile[], PaginationInfinite>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list_featured`;
const queryWithStrings = {
@@ -183,13 +185,13 @@ export class MediaFilesApi extends ApiManager {
}
public async ListUserMediaFiles(
query: ListUserMediaQuery,
query: ListUserMediaQuery
): Promise<ApiResponse<MediaFile[], Pagination>> {
const userName = query.username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list/user/${userName}`;
const queryWithStrings = {
...query,
include_user_uploads: true,
include_user_uploads: query.include_user_uploads,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
@@ -205,11 +207,17 @@ export class MediaFilesApi extends ApiManager {
results: MediaFile[];
pagination?: Pagination;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: response.success,
data: response.results ?? [],
pagination: response.pagination,
}))
.then((response) => {
let results = response.results ?? [];
if (query.user_uploads_only) {
results = results.filter((file) => file.origin_category === "upload");
}
return {
success: response.success,
data: results,
pagination: response.pagination,
};
})
.catch((err) => {
return {
success: false,
@@ -219,7 +227,7 @@ export class MediaFilesApi extends ApiManager {
}
public async SearchFeaturedMediaFiles(
query: SearchFeaturedMediaQuery,
query: SearchFeaturedMediaQuery
): Promise<ApiResponse<MediaFile[], Pagination>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/search_featured`;
const queryWithStrings = {
@@ -251,7 +259,7 @@ export class MediaFilesApi extends ApiManager {
}
public async SearchUserMediaFiles(
query: SearchFeaturedMediaQuery,
query: SearchFeaturedMediaQuery
): Promise<ApiResponse<MediaFile[], Pagination>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/search_session`;
const queryWithStrings = {
@@ -11,10 +11,11 @@ import {
GetCdnOrigin,
FilterMediaClasses,
LibraryModalApi,
UsersApi,
} from "@storyteller/api";
import { twMerge } from "tailwind-merge";
export interface LibraryItem {
export interface GalleryItem {
id: string;
label: string;
thumbnail: string | null;
@@ -23,7 +24,7 @@ export interface LibraryItem {
}
interface GroupedItems {
[date: string]: LibraryItem[];
[date: string]: GalleryItem[];
}
type ModalMode = "select" | "view";
@@ -35,7 +36,7 @@ interface LibraryModalProps {
selectedItemIds?: string[];
onSelectItem?: (id: string) => void;
maxSelections?: number;
onUseSelected?: (selectedItems: LibraryItem[]) => void;
onUseSelected?: (selectedItems: GalleryItem[]) => void;
tabs: { id: string; label: string }[];
activeTab: string;
onTabChange: (tabId: string) => void;
@@ -56,14 +57,16 @@ export const GalleryModal = React.memo(
}: LibraryModalProps) => {
const [groupedItems, setGroupedItems] = useState<GroupedItems>({});
const [loading, setLoading] = useState(false);
const [lightboxImage, setLightboxImage] = useState<LibraryItem | null>(
const [lightboxImage, setLightboxImage] = useState<GalleryItem | null>(
null
);
const [isLightboxVisible, setIsLightboxVisible] = useState(false);
const [failedImageUrls] = useState<Set<string>>(new Set());
const [username, setUsername] = useState<string>("");
const cdnOrigin = GetCdnOrigin();
const imageUrl = lightboxImage?.fullImage || "";
const api = useMemo(() => new LibraryModalApi(), []);
const usersApi = useMemo(() => new UsersApi(), []);
const formatDate = useCallback((date: string) => {
const d = new Date(date);
@@ -75,7 +78,7 @@ export const GalleryModal = React.memo(
}, []);
const groupItemsByDate = useCallback(
(items: LibraryItem[]) => {
(items: GalleryItem[]) => {
const grouped = items.reduce((acc: GroupedItems, item) => {
const dateKey = formatDate(item.createdAt);
if (!acc[dateKey]) {
@@ -114,15 +117,29 @@ export const GalleryModal = React.memo(
[cdnOrigin, failedImageUrls]
);
useEffect(() => {
const getUsername = async () => {
const session = await usersApi.GetSession();
if (session.success && session.data?.user) {
setUsername(session.data.user.username);
}
};
getUsername();
}, [usersApi]);
useEffect(() => {
const loadItems = async () => {
if (!username) return;
setLoading(true);
try {
const response = await api.listUserMediaFiles({
filter_media_classes: [FilterMediaClasses.IMAGE],
username: username,
include_user_uploads: activeTab === "uploads",
user_uploads_only: activeTab === "uploads",
});
if (response.success && response.data) {
const newItems = response.data.map((item) => ({
const newItems = response.data.map((item: any) => ({
id: item.token,
label: item.maybe_title || "Image Generation",
thumbnail: getImageUrl(item.public_bucket_path),
@@ -141,7 +158,7 @@ export const GalleryModal = React.memo(
loadItems();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, isOpen]);
}, [activeTab, isOpen, username]);
// useEffect(() => {
// console.log("LibraryModal render caused by:", {
@@ -152,7 +169,7 @@ export const GalleryModal = React.memo(
// }, [isOpen, activeTab, selectedItemIds]);
const handleItemClick = useCallback(
(item: LibraryItem) => {
(item: GalleryItem) => {
if (mode === "select" && onSelectItem) {
onSelectItem(item.id);
} else {
@@ -190,12 +207,13 @@ export const GalleryModal = React.memo(
mode === "view" && "h-[80vh] max-h-[870px] max-w-7xl"
)}
childPadding={false}
showClose={false}
>
<div className="flex h-full flex-col">
<div className="border-b border-white/10 p-4 py-3">
<div className="grid grid-cols-3 items-center">
<h2 className="text-xl font-semibold">
{mode === "select" ? "Select Images" : "My Gallery"}
{mode === "select" ? "Select Images" : "Gallery"}
</h2>
<div className="flex items-center justify-center">
<TabSelector
@@ -211,7 +229,7 @@ export const GalleryModal = React.memo(
</div>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex h-full items-center justify-center">
<div className="text-white/60">
@@ -219,7 +237,7 @@ export const GalleryModal = React.memo(
</div>
</div>
) : (
<div className="space-y-6">
<div className="space-y-6 p-4">
{Object.entries(groupedItems).map(([date, dateItems]) => (
<div key={date}>
<h3 className="text-md mb-2 font-medium text-white/60">
@@ -238,7 +256,7 @@ export const GalleryModal = React.memo(
"group relative aspect-square overflow-hidden rounded-md border-[3px] transition-all",
mode === "select" &&
selectedItemIds.includes(item.id)
? "border-brand-primary"
? "border-primary"
: "border-transparent hover:border-white"
)}
onClick={() => handleItemClick(item)}
@@ -269,7 +287,7 @@ export const GalleryModal = React.memo(
</div>
{mode === "select" &&
selectedItemIds.includes(item.id) && (
<div className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-brand-primary">
<div className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary">
<FontAwesomeIcon
icon={faCheck}
className="text-sm"
@@ -2,6 +2,12 @@
"files": [],
"include": [],
"references": [
{
"path": "../../api"
},
{
"path": "../loading-spinner"
},
{
"path": "../close-button"
},
@@ -43,6 +43,12 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../../api/tsconfig.lib.json"
},
{
"path": "../loading-spinner/tsconfig.lib.json"
},
{
"path": "../close-button/tsconfig.lib.json"
},
@@ -64,7 +64,7 @@ export const Modal = ({
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-[70]" onClose={onClose}>
<DialogBackdrop className={backdropClassName} />
<div className="fixed inset-0 overflow-y-auto">
<div className="fixed inset-0">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<TransitionChild
as="div"
@@ -83,7 +83,7 @@ export const Modal = ({
)}
>
<DialogPanel
className="w-full relative"
className="w-full h-full"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -60,10 +60,10 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
)}
>
<TabGroup selectedIndex={selectedIndex} onChange={handleTabChange}>
<TabList className="glass relative inline-flex min-w-fit overflow-x-auto rounded-lg p-0.5 py-1 !shadow-none">
<TabList className="glass glass-no-hover relative inline-flex min-w-fit overflow-x-auto rounded-lg p-0.5 py-1 !shadow-none">
{/* Animated indicator */}
<div
className="absolute top-1 z-10 h-[calc(100%-8px)] rounded-md bg-brand-primary/30 transition-all duration-200 ease-in-out"
className="absolute top-1 z-10 h-[calc(100%-8px)] rounded-md bg-primary/30 transition-all duration-200 ease-in-out"
style={{
left: indicatorStyle.left,
width: indicatorStyle.width,
+2 -47
View File
@@ -82,7 +82,6 @@
"apps/editor2d": {
"version": "0.0.0",
"dependencies": {
"@devnomic/marquee": "^1.0.3",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
@@ -112,8 +111,8 @@
"@preact/signals-react-transform": "^0.4.0",
"@tauri-apps/cli": "^2.4.0",
"@types/deep-equal": "^1.0.4",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
@@ -136,22 +135,6 @@
"vite-tsconfig-paths": "^5.0.1"
}
},
"apps/editor2d/node_modules/@devnomic/marquee": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@devnomic/marquee/-/marquee-1.0.3.tgz",
"integrity": "sha512-q8QUtNO8ff+UvfrKJsHBtxh4fbOfhGk6SSdK0Uu/XZFdEzrmiGJ/l0FuVHRYsPAmzgnhVVnz1dGNB5CWGLMVvw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0"
},
"peerDependencies": {
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"apps/editor2d/node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -607,25 +590,6 @@
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"apps/editor2d/node_modules/@types/react": {
"version": "18.3.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz",
"integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"apps/editor2d/node_modules/@types/react-dom": {
"version": "18.3.6",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz",
"integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"apps/editor2d/node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
@@ -699,15 +663,6 @@
"concat-map": "0.0.1"
}
},
"apps/editor2d/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"apps/editor2d/node_modules/deep-equal": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",