diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx index 48e8d8d..011b42d 100644 --- a/src/components/Componentes/question-file.tsx +++ b/src/components/Componentes/question-file.tsx @@ -1,7 +1,7 @@ "use client"; import Image from "next/image"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import type { QuestionField } from "@/lib/schema-adapter"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; import { getApiRequestUrl } from "@/lib/http"; @@ -83,6 +83,7 @@ export function QuestionFile({ const [filePreviewUrl, setFilePreviewUrl] = useState( initialFileUrl, ); + const isInitiatorRef = useRef(false); const [isFlutterPicking, setIsFlutterPicking] = useState(false); const acceptedFiles = (question.extras?.options ?? []) @@ -93,6 +94,8 @@ export function QuestionFile({ onSuccess: (response) => { if (response.path) { setAnswerValue(question, response.path); + setSelectedFileName(response.name ?? response.path.split("/").pop() ?? "uploaded"); + setFilePreviewUrl(response.path); } }, onError: (error) => { @@ -108,6 +111,8 @@ export function QuestionFile({ const unsubscribe = window.addFlutterResponseListener?.((event) => { if (event.action !== "upload_file") return; + if (event.requestId && String(event.requestId) !== String(question.id)) return; + if (!event.requestId && !isInitiatorRef.current) return; switch (event.status) { case "picking": @@ -117,20 +122,23 @@ export function QuestionFile({ if (event.data?.files?.[0]) { const fileName = event.data.files[0].name ?? null; setSelectedFileName(fileName); - if (fileName) { - setAnswerValue(question, fileName); - } } break; case "progress": break; case "completed": { setIsFlutterPicking(false); + isInitiatorRef.current = false; const file = event.data?.files?.[0]; - if (file?.url) { - setAnswerValue(question, file.url); - setSelectedFileName(file.name ?? "uploaded"); - setFilePreviewUrl(file.url); + const remoteUrl = + file?.path || + file?.url || + (file?.data?.path as string | undefined) || + (file?.data?.url as string | undefined); + if (remoteUrl) { + setAnswerValue(question, remoteUrl); + setSelectedFileName(file?.name ?? remoteUrl.split("/").pop() ?? "uploaded"); + setFilePreviewUrl(remoteUrl); } else if (file?.base64) { const b64 = file.base64; setSelectedFileName(file.name ?? "upload"); @@ -148,9 +156,11 @@ export function QuestionFile({ } case "cancelled": setIsFlutterPicking(false); + isInitiatorRef.current = false; break; case "failed": setIsFlutterPicking(false); + isInitiatorRef.current = false; console.error("upload_file failed:", event.message); break; } @@ -168,7 +178,11 @@ export function QuestionFile({ ); const mediaType = resolveMediaType(extensions); + isInitiatorRef.current = true; + setIsFlutterPicking(true); + uploadFile({ + requestId: String(question.id), mediaType, source: "gallery", returnAs: "upload", diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index 374b127..0a965de 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -1,7 +1,7 @@ "use client"; import Image from "next/image"; -import { type ReactNode, useCallback, useEffect, useId, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useId, useRef, useState } from "react"; import type { QuestionField } from "@/lib/schema-adapter"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; import { getApiRequestUrl } from "@/lib/http"; @@ -34,10 +34,12 @@ export function QuestionPhoto({ const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); const storedValue = getAnswerValue(question); + const isInitiatorRef = useRef(false); const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { if (response.path) { setAnswerValue(question, response.path); + setLocalPreviewUrl(response.path); } }, onError: (error) => { @@ -53,6 +55,8 @@ export function QuestionPhoto({ const unsubscribe = window.addFlutterResponseListener?.((event) => { if (event.action !== "upload_file") return; + if (event.requestId && String(event.requestId) !== String(question.id)) return; + if (!event.requestId && !isInitiatorRef.current) return; switch (event.status) { case "picking": @@ -67,10 +71,16 @@ export function QuestionPhoto({ break; case "completed": { setIsFlutterPicking(false); + isInitiatorRef.current = false; const file = event.data?.files?.[0]; - if (file?.url) { - setAnswerValue(question, file.url); - setLocalPreviewUrl(file.url); + const remoteUrl = + file?.path || + file?.url || + (file?.data?.path as string | undefined) || + (file?.data?.url as string | undefined); + if (remoteUrl) { + setAnswerValue(question, remoteUrl); + setLocalPreviewUrl(remoteUrl); } else if (file?.base64) { const b64 = file.base64; setLocalPreviewUrl(b64); @@ -89,6 +99,7 @@ export function QuestionPhoto({ case "cancelled": case "failed": setIsFlutterPicking(false); + isInitiatorRef.current = false; break; } }); @@ -103,7 +114,11 @@ export function QuestionPhoto({ o.replace(/^\./, "").toLowerCase(), ); + isInitiatorRef.current = true; + setIsFlutterPicking(true); + uploadFile({ + requestId: String(question.id), mediaType: "image", source: "gallery", returnAs: "upload", diff --git a/src/lib/webview-actions.ts b/src/lib/webview-actions.ts index 7ef7f5a..057192c 100644 --- a/src/lib/webview-actions.ts +++ b/src/lib/webview-actions.ts @@ -115,6 +115,7 @@ export function openExternalUrl(options: OpenExternalUrlOptions): boolean { // ─── upload_file ───────────────────────────────────────── export interface UploadFileOptions { + requestId?: string | number; mediaType: "image" | "video" | "image+video" | "audio" | "file"; source?: "gallery" | "camera" | "any"; multiple?: boolean; diff --git a/src/types/window.d.ts b/src/types/window.d.ts index 6b3eacf..e5f6cad 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -9,6 +9,7 @@ declare global { interface FlutterResponseEvent { action: string; success: boolean; + requestId?: string | number; /** Compatibility payload used by the uppercase Flutter event protocol. */ payload?: FlutterResponseEvent["data"]; /** Top-level status for multi-step actions (download_file, upload_file, …) */ @@ -67,10 +68,13 @@ declare global { source?: string; files?: Array<{ url?: string; + path?: string; + apath?: string; base64?: string; name?: string; size?: number; mimeType?: string; + data?: Record; }>; // copy_to_clipboard label?: string;