fix delete (and add second button) (#995)

- delete hamburger icon fixed
- delete button added to gallery
- flows through tauri instead of javascript API
- ondelete toast events (tauri sent, frontend consumed)
This commit is contained in:
Brandon Thomas
2026-01-19 20:02:56 -05:00
committed by GitHub
parent 2f92e0187f
commit c923a5ed95
14 changed files with 166 additions and 80 deletions
@@ -1,5 +1,7 @@
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::media_file_deleted_event::MediaFileDeletedEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -23,6 +25,7 @@ pub struct MediaFileDeleteRequest {
#[derive(Serialize)]
pub struct MediaFileDeleteSuccessResponse {
pub success: bool,
}
impl SerializeMarker for MediaFileDeleteSuccessResponse {}
@@ -49,7 +52,9 @@ pub async fn media_file_delete_command(
return Err("delete failed".into())
}
Ok(MediaFileDeleteSuccessResponse{}.into())
Ok(MediaFileDeleteSuccessResponse {
success: true,
}.into())
}
pub async fn handle_request(
@@ -67,7 +72,9 @@ pub async fn handle_request(
&request.media_file_token
).await?;
// TODO: Send event to frontend that the file was deleted.
MediaFileDeletedEvent {
media_file_token: request.media_file_token,
}.send_infallible(app);
Ok(())
}
@@ -0,0 +1,16 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use enums::tauri::ux::tauri_event_name::TauriEventName;
use serde_derive::Serialize;
use tokens::tokens::media_files::MediaFileToken;
use url::Url;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct MediaFileDeletedEvent {
pub media_file_token: MediaFileToken,
}
impl BasicSendableEvent for MediaFileDeletedEvent {
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::MediaFileDeletedEvent;
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success;
}
@@ -2,6 +2,7 @@ pub mod canvas_background_removal_complete_event;
pub mod credits_balance_changed_event;
pub mod gaussian_generation_complete_event;
pub mod image_edit_complete_event;
pub mod media_file_deleted_event;
pub mod object_generation_complete_event;
pub mod refresh_account_state_event;
pub mod show_provider_billing_modal_event;
@@ -48,8 +48,13 @@ pub enum TauriEventName {
#[serde(rename = "subscription_plan_changed_event")]
SubscriptionPlanChangedEvent,
/// Informational event:
/// A media file was deleted (and we'll send the token)
#[serde(rename = "media_file_deleted_event")]
MediaFileDeletedEvent,
/// Special event:
/// Background removal complete
/// Background removal is complete
#[serde(rename = "canvas_bg_removed_event")]
CanvasBgRemovedEvent,
@@ -119,6 +124,7 @@ impl TauriEventName {
Self::GenerationFailedEvent => "generation-failed-event",
Self::CreditsBalanceChangedEvent => "credits_balance_changed_event",
Self::SubscriptionPlanChangedEvent => "subscription_plan_changed_event",
Self::MediaFileDeletedEvent => "media_file_deleted_event",
Self::CanvasBgRemovedEvent => "canvas_bg_removed_event",
Self::TextToImageGenerationCompleteEvent => "text_to_image_generation_complete_event",
Self::ImageEditCompleteEvent => "image_edit_complete_event",
@@ -141,6 +147,7 @@ impl TauriEventName {
"generation-failed-event" => Ok(Self::GenerationFailedEvent),
"credits_balance_changed_event" => Ok(Self::CreditsBalanceChangedEvent),
"subscription_plan_changed_event" => Ok(Self::SubscriptionPlanChangedEvent),
"media_file_deleted_event" => Ok(Self::MediaFileDeletedEvent),
"canvas_bg_removed_event" => Ok(Self::CanvasBgRemovedEvent),
"text_to_image_generation_complete_event" => Ok(Self::TextToImageGenerationCompleteEvent),
"image_edit_complete_event" => Ok(Self::ImageEditCompleteEvent),
@@ -166,6 +173,7 @@ impl TauriEventName {
Self::GenerationFailedEvent,
Self::CreditsBalanceChangedEvent,
Self::SubscriptionPlanChangedEvent,
Self::MediaFileDeletedEvent,
Self::CanvasBgRemovedEvent,
Self::TextToImageGenerationCompleteEvent,
Self::ImageEditCompleteEvent,
@@ -197,6 +205,7 @@ mod tests {
assert_serialization(TauriEventName::GenerationFailedEvent, "generation-failed-event");
assert_serialization(TauriEventName::CreditsBalanceChangedEvent, "credits_balance_changed_event");
assert_serialization(TauriEventName::SubscriptionPlanChangedEvent, "subscription_plan_changed_event");
assert_serialization(TauriEventName::MediaFileDeletedEvent, "media_file_deleted_event");
assert_serialization(TauriEventName::CanvasBgRemovedEvent, "canvas_bg_removed_event");
assert_serialization(TauriEventName::TextToImageGenerationCompleteEvent, "text_to_image_generation_complete_event");
assert_serialization(TauriEventName::ImageEditCompleteEvent, "image_edit_complete_event");
@@ -218,6 +227,7 @@ mod tests {
assert_eq!(TauriEventName::GenerationFailedEvent.to_str(), "generation-failed-event");
assert_eq!(TauriEventName::CreditsBalanceChangedEvent.to_str(), "credits_balance_changed_event");
assert_eq!(TauriEventName::SubscriptionPlanChangedEvent.to_str(), "subscription_plan_changed_event");
assert_eq!(TauriEventName::MediaFileDeletedEvent.to_str(), "media_file_deleted_event");
assert_eq!(TauriEventName::CanvasBgRemovedEvent.to_str(), "canvas_bg_removed_event");
assert_eq!(TauriEventName::TextToImageGenerationCompleteEvent.to_str(), "text_to_image_generation_complete_event");
assert_eq!(TauriEventName::ImageEditCompleteEvent.to_str(), "image_edit_complete_event");
@@ -239,6 +249,7 @@ mod tests {
assert_eq!(TauriEventName::from_str("generation-failed-event").unwrap(), TauriEventName::GenerationFailedEvent);
assert_eq!(TauriEventName::from_str("credits_balance_changed_event").unwrap(), TauriEventName::CreditsBalanceChangedEvent);
assert_eq!(TauriEventName::from_str("subscription_plan_changed_event").unwrap(), TauriEventName::SubscriptionPlanChangedEvent);
assert_eq!(TauriEventName::from_str("media_file_deleted_event").unwrap(), TauriEventName::MediaFileDeletedEvent);
assert_eq!(TauriEventName::from_str("canvas_bg_removed_event").unwrap(), TauriEventName::CanvasBgRemovedEvent);
assert_eq!(TauriEventName::from_str("text_to_image_generation_complete_event").unwrap(), TauriEventName::TextToImageGenerationCompleteEvent);
assert_eq!(TauriEventName::from_str("image_edit_complete_event").unwrap(), TauriEventName::ImageEditCompleteEvent);
@@ -255,13 +266,14 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = TauriEventName::all_variants();
assert_eq!(variants.len(), 17);
assert_eq!(variants.len(), 18);
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueSuccessEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueFailureEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationCompleteEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationFailedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::CreditsBalanceChangedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::SubscriptionPlanChangedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::MediaFileDeletedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::CanvasBgRemovedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::TextToImageGenerationCompleteEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::ImageEditCompleteEvent));
@@ -42,8 +42,8 @@ GC_ENABLED_PUBLIC_BUCKET_NAME="REPLACE_ME"
AUDIO_UPLOADS_BUCKET_ROOT=/user_uploaded_w2l_audio
# Fake API key for FAL
#FAL_WEBHOOK_URL="https://macaroni-1.tailce84f.ts.net/v1/webhooks/fal"
FAL_WEBHOOK_URL="https://halide.tailce84f.ts.net/v1/webhooks/fal"
FAL_WEBHOOK_URL="https://macaroni-1.tailce84f.ts.net/v1/webhooks/fal"
#FAL_WEBHOOK_URL="https://halide.tailce84f.ts.net/v1/webhooks/fal"
#FAL_WEBHOOK_URL="https://s1.pancake-chimaera.ts.net/api/fal/webhook"
STRIPE_ARTCRAFT_CHECKOUT_SUCCESS_URL="https://getartcraft.com/checkout_success"
@@ -42,39 +42,6 @@ interface SearchFeaturedMediaQuery {
}
export class MediaFilesApi extends ApiManager {
public async DeleteMediaFileByToken({
mediaFileToken,
asMod = true,
setDelete = true,
}: {
mediaFileToken: string;
asMod?: boolean;
setDelete?: boolean;
}): Promise<ApiResponse<MediaFile>> {
const endpoint = `${this.getApiSchemeAndHost()}/v1/media_files/file/${mediaFileToken}`;
const body = { as_mod: asMod, set_delete: setDelete };
return await this.delete<
{
as_mod: boolean;
set_delete: boolean;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListMediaFilesByTokens({
mediaTokens,
@@ -31,7 +31,7 @@ import {
actionReminderProps,
ActionReminderModal,
} from "@storyteller/ui-action-reminder-modal";
import { useFlashFileDownloadErrorEvent, useFlashUserInputErrorEvent } from "@storyteller/tauri-events";
import { useFlashFileDownloadErrorEvent, useFlashUserInputErrorEvent, useMediaFileDeletedEvent } from "@storyteller/tauri-events";
import { useGenerationCompleteEvent } from "@storyteller/tauri-events";
import { useGenerationEnqueueFailureEvent } from "@storyteller/tauri-events";
import { useGenerationEnqueueSuccessEvent } from "@storyteller/tauri-events";
@@ -115,6 +115,11 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
toast.error(event.message || "File download failed");
});
useMediaFileDeletedEvent(async (event) => {
console.log("Media file deleted event received:", event);
toast.error("File deleted.");
});
useEffect(() => {
console.log("installing event listeners");
InstallSounds();
@@ -1,4 +1,4 @@
import React, { useMemo, useRef } from "react";
import React, { useRef } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faCheck,
@@ -15,7 +15,6 @@ import {
showActionReminder,
isActionReminderOpen,
} from "@storyteller/ui-action-reminder-modal";
import { MediaFilesApi } from "@storyteller/api";
import { MediaFileDelete } from "@storyteller/tauri-api";
type ModalMode = "select" | "view";
@@ -49,7 +48,6 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
}) => {
const imgRef = useRef<HTMLImageElement>(null);
const dragStarted = useRef(false);
const mediaFilesApi = useMemo(() => new MediaFilesApi(), []);
const handleDelete = () => {
showActionReminder({
@@ -65,16 +63,9 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
secondaryActionText: "Cancel",
primaryActionBtnClassName: "bg-red text-white hover:bg-red/90",
onPrimaryAction: async () => {
console.log(">>> Delete media file via Tauri: ", item.id);
try {
await MediaFileDelete(item.id);
onDeleted?.(item.id);
//const res = await mediaFilesApi.DeleteMediaFileByToken({
// mediaFileToken: item.id,
//});
//if (res.success) {
// onDeleted?.(item.id);
//}
} finally {
isActionReminderOpen.value = false;
}
@@ -12,6 +12,7 @@ import {
import {
EnqueueImageTo3dObject,
EnqueueImageTo3dObjectModel,
MediaFileDelete,
} from "@storyteller/tauri-api";
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
import { Viewer3D } from "@storyteller/ui-viewer-3d";
@@ -29,6 +30,10 @@ import { toast } from "@storyteller/ui-toaster";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCopy, faLink, faCheck } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import {
showActionReminder,
isActionReminderOpen,
} from "@storyteller/ui-action-reminder-modal";
import {
getModelCreatorIcon,
getModelDisplayName,
@@ -308,6 +313,30 @@ export function LightboxModal({
[emblaMainApi, emblaThumbsApi],
);
const onDeleteClicked = (mediaToken: string) => {
showActionReminder({
reminderType: "default",
title: "Delete this media?",
message: (
<p className="text-sm text-white/70">
This will permanently remove the media from your library. This action
cannot be undone.
</p>
),
primaryActionText: "Delete",
secondaryActionText: "Cancel",
primaryActionBtnClassName: "bg-red text-white hover:bg-red/90",
onPrimaryAction: async () => {
try {
await MediaFileDelete(mediaToken);
} finally {
isActionReminderOpen.value = false;
onClose();
}
},
});
};
const onSelect = useCallback(() => {
if (!emblaMainApi || !emblaThumbsApi) return;
const index = emblaMainApi.selectedScrollSnap();
@@ -803,7 +832,7 @@ export function LightboxModal({
{selectedMediaToken && (
<Button
className="w-full col-span-2"
className="w-full"
variant="secondary"
icon={shareCopied ? faCheck : faLink}
onClick={async (e) => {
@@ -829,7 +858,21 @@ export function LightboxModal({
}
}}
>
{shareCopied ? "Link Copied!" : "Copy Share Link"}
{shareCopied ? "Link Copied!" : "Share"}
</Button>
)}
{selectedMediaToken && onDeleteClicked && (
<Button
className="w-full"
variant="destructive"
onClick={async (e) => {
e.stopPropagation();
gtagEvent("delete_media_clicked");
await onDeleteClicked(selectedMediaToken);
}}
>
Delete
</Button>
)}
</div>
+3 -3
View File
@@ -1,6 +1,5 @@
export * from "./lib/artcraft/ArtcraftGetCredits.js";
export * from "./lib/artcraft/ArtcraftGetSubscription.js";
export * from "./lib/artcraft/media_files/MediaFileDelete.js";
export * from "./lib/artcraft/task_queue/GetTaskQueue.js";
export * from "./lib/artcraft/task_queue/MarkTaskAsDismissed.js";
export * from "./lib/artcraft/task_queue/TasksNukeAll.js";
@@ -23,9 +22,11 @@ export * from "./lib/fal/FalKlingImageToVideo.js";
export * from "./lib/fal/GetFalApiKey.js";
export * from "./lib/fal/SetFalApiKey.js";
export * from "./lib/grok/GrokGetCredentialInfo.js";
export * from "./lib/media_files/MediaFileDeleteCommand.js";
export * from "./lib/midjourney/MidjourneyGetCredentialInfo.js";
export * from "./lib/openai/GetOpenAIApiKey.js";
export * from "./lib/openai/SetOpenAIApiKey.js";
export * from "./lib/opener/OpenUrl.js";
export * from "./lib/providers/GetProviderOrder.js";
export * from "./lib/providers/Provider.js";
export * from "./lib/providers/SetProviderOrder.js";
@@ -39,5 +40,4 @@ export * from "./lib/sora/useSoraLoginListener.js";
export * from "./lib/sora/waitForSoraLogin.js";
export * from "./lib/system/GetAppInfo.js";
export * from "./lib/util/LoadWithoutCors.js";
export * from "./lib/worldlabs/WorldLabsGetCredentialInfo.js";
export * from "./lib/opener/OpenUrl.js";
export * from "./lib/worldlabs/WorldLabsGetCredentialInfo.js";
@@ -1,21 +0,0 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../../common/CommandStatus";
export interface MediaFileDeleteSuccess extends CommandResult {
payload: MediaFileDeletePayload;
}
export interface MediaFileDeletePayload {
}
export const MediaFileDelete = async (media_file_token: string) : Promise<MediaFileDeleteSuccess> => {
try {
return await invoke("media_file_delete_command", {
request: {
media_file_token: media_file_token
}
}) as MediaFileDeleteSuccess;
} catch (error) {
throw error;
}
}
@@ -0,0 +1,26 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
export interface MediaFileDeleteError extends CommandResult {
error_message?: string;
}
export interface MediaFileDeletePayload {
success: boolean;
}
export interface MediaFileDeleteSuccess extends CommandResult {
payload: MediaFileDeletePayload;
}
export type MediaFileDeleteResult = MediaFileDeleteSuccess | MediaFileDeleteError;
export const MediaFileDelete = async (media_file_token: string) : Promise<MediaFileDeleteResult> => {
const result = await invoke("media_file_delete_command", {
request: {
media_file_token: media_file_token,
}
});
return (result as MediaFileDeleteResult);
}
+5 -4
View File
@@ -1,16 +1,17 @@
export * from "./lib/common/BasicEventWrapper";
export * from "./lib/events/functional/CreditsBalanceChangedEvent";
export * from "./lib/events/functional/ImageEditCompleteEvent";
export * from "./lib/events/functional/MediaFileDeletedEvent";
export * from "./lib/events/functional/ObjectGenerationCompleteEvent";
export * from "./lib/events/functional/RefreshAccountStateEvent";
export * from "./lib/events/functional/ShowProviderBillingModalEvent";
export * from "./lib/events/functional/ShowProviderLoginModalEvent";
export * from "./lib/events/functional/TextToImageGenerationCompleteEvent";
export * from "./lib/events/functional/ObjectGenerationCompleteEvent";
export * from "./lib/events/functional/VideoGenerationCompleteEvent";
export * from "./lib/events/functional/SubscriptionPlanChangedEvent";
export * from "./lib/events/functional/TextToImageGenerationCompleteEvent";
export * from "./lib/events/functional/VideoGenerationCompleteEvent";
export * from "./lib/events/generation/useGenerationCompleteEvent";
export * from "./lib/events/generation/useGenerationEnqueueFailureEvent";
export * from "./lib/events/generation/useGenerationEnqueueSuccessEvent";
export * from "./lib/events/generation/useGenerationFailedEvent";
export * from "./lib/events/warning/FlashUserInputErrorEvent";
export * from "./lib/events/warning/FlashFileDownloadErrorEvent";
export * from "./lib/events/warning/FlashUserInputErrorEvent";
@@ -0,0 +1,38 @@
import { listen, UnlistenFn } from "@tauri-apps/api/event";
import { BasicEventWrapper } from "../../common/BasicEventWrapper";
import { useEffect } from "react";
const EVENT_NAME: string = "media_file_deleted_event";
export interface MediaFileDeletedEvent {
media_file_token: string;
}
export const useMediaFileDeletedEvent = (
asyncCallback: (event: MediaFileDeletedEvent) => Promise<void>
) => {
useEffect(() => {
let isUnmounted = false;
let unlisten: Promise<UnlistenFn>;
const setup = async () => {
unlisten = listen<BasicEventWrapper<MediaFileDeletedEvent>>(
EVENT_NAME,
async (wrappedEvent) => {
await asyncCallback(wrappedEvent.payload.data);
}
);
if (isUnmounted) {
unlisten.then((f) => f()); // Unsubscribe if unmounted early.
}
};
setup();
return () => {
isUnmounted = true;
unlisten.then((f) => f());
};
}, []);
};