From c25bb7cbafc83edc014d50c3ecca82286eb40c3c Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Sat, 25 Apr 2026 18:32:56 +0300 Subject: [PATCH] feat: SWF editor enhancements --- src/components/views/SwfView.tsx | 382 ++++++++++++++++++++++--------- src/pages/App.tsx | 10 +- src/services/SwfService.ts | 345 ++++++++++++++++++---------- 3 files changed, 504 insertions(+), 233 deletions(-) diff --git a/src/components/views/SwfView.tsx b/src/components/views/SwfView.tsx index b73420a..dcecbbf 100644 --- a/src/components/views/SwfView.tsx +++ b/src/components/views/SwfView.tsx @@ -1,41 +1,90 @@ -import React, { useState, useCallback } from "react"; -import { motion } from "framer-motion"; +import React, { useState, useCallback, useMemo, useRef, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { useConfig, useAudio, useUI } from "../../context/LauncherContext"; -import { SwfImage, SwfService } from "../../services/SwfService"; - +import { SwfImage, SwfService, SwfTag } from "../../services/SwfService"; export default function SwfView() { const { animationsEnabled } = useConfig(); - const { playBackSound } = useAudio(); + const { playBackSound, playPressSound } = useAudio(); const { setActiveView } = useUI(); - + const [swfData, setSwfData] = useState<{ version: number, compressed: boolean, frameHeader: Uint8Array, tags: SwfTag[] } | null>(null); const [images, setImages] = useState([]); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); + const [selectedImageId, setSelectedImageId] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); const [fileName, setFileName] = useState(null); + const [imageUrls, setImageUrls] = useState>({}); + const [notification, setNotification] = useState<{ message: string, type: "success" | "error" } | null>(null); + const fileInputRef = useRef(null); + const replaceInputRef = useRef(null); + const showNotification = (message: string, type: "success" | "error" = "success") => { + setNotification({ message, type }); + setTimeout(() => setNotification(null), 3000); + }; + + const filteredImages = useMemo(() => { + return images.filter(img => + img.id.toString().includes(searchTerm) || + (img.name && img.name.toLowerCase().includes(searchTerm.toLowerCase())) + ); + }, [images, searchTerm]); + + const selectedImage = useMemo(() => { + return images.find(img => img.id === selectedImageId) || null; + }, [images, selectedImageId]); const handleBack = useCallback(() => { playBackSound(); setActiveView("devtools"); }, [playBackSound, setActiveView]); + const loadUrl = async (img: SwfImage) => { + if (imageUrls[img.id]) return imageUrls[img.id]; + let url = ""; + if (img.type === "jpeg") { + const blob = new Blob([img.data as any], { type: "image/jpeg" }); + url = URL.createObjectURL(blob); + } else if (img.type === "lossless") { + const rgba = await SwfService.decodeLosslessToRGBA(img); + const canvas = document.createElement("canvas"); + canvas.width = img.width!; + canvas.height = img.height!; + const ctx = canvas.getContext("2d"); + if (ctx) { + const imageData = new ImageData(new Uint8ClampedArray(rgba), img.width!, img.height!); + ctx.putImageData(imageData, 0, 0); + url = canvas.toDataURL(); + } + } + if (url) { + setImageUrls(prev => ({ ...prev, [img.id]: url })); + } + return url; + }; + + useEffect(() => { + if (selectedImage) { + loadUrl(selectedImage); + } + }, [selectedImage]); + const processFile = async (file: File) => { - setLoading(true); - setError(null); setFileName(file.name); + setImageUrls({}); try { const buffer = await file.arrayBuffer(); const bytes = new Uint8Array(buffer); - const extracted = await SwfService.extractImages(bytes); + const swf = SwfService.parse(bytes); + setSwfData(swf); + const extracted = SwfService.extractImages(swf.tags); setImages(extracted); - if (extracted.length === 0) { - setError("No supported images found in SWF."); + if (extracted.length > 0) { + setSelectedImageId(extracted[0].id); } + showNotification(`Loaded ${file.name}`); } catch (e: any) { console.error(e); - setError(e.message || "Failed to process SWF"); + showNotification("Failed to process SWF", "error"); setImages([]); - } finally { - setLoading(false); + setSwfData(null); } }; @@ -45,37 +94,82 @@ export default function SwfView() { } }; - const getImageSrc = (img: SwfImage) => { - if (img.type === "jpeg" || img.type === "png" || img.type === "gif") { - const mime = img.type === "jpeg" ? "image/jpeg" : `image/${img.type}`; - const blob = new Blob([new Uint8Array(img.data)], { type: mime }); - return URL.createObjectURL(blob); - } - return ""; - }; - - const handleDownload = (img: SwfImage) => { + const handleDownload = async (img: SwfImage) => { + playPressSound(); let blob: Blob; - let ext: string; + let ext = "png"; if (img.type === "jpeg") { - blob = new Blob([new Uint8Array(img.data)], { type: "image/jpeg" }); + blob = new Blob([img.data as any], { type: "image/jpeg" }); ext = "jpg"; - } else if (img.type === "png" || img.type === "gif") { - blob = new Blob([new Uint8Array(img.data)], { type: `image/${img.type}` }); - ext = img.type; + } else if (img.type === "lossless") { + const rgba = await SwfService.decodeLosslessToRGBA(img); + const canvas = document.createElement("canvas"); + canvas.width = img.width!; + canvas.height = img.height!; + const ctx = canvas.getContext("2d"); + if (ctx) { + const imageData = new ImageData(new Uint8ClampedArray(rgba), img.width!, img.height!); + ctx.putImageData(imageData, 0, 0); + const dataUrl = canvas.toDataURL("image/png"); + const res = await fetch(dataUrl); + blob = await res.blob(); + } else { + blob = new Blob([rgba as any], { type: "application/octet-stream" }); + ext = "bin"; + } } else { - blob = new Blob([new Uint8Array(img.data)], { type: "application/octet-stream" }); + blob = new Blob([img.data as any], { type: "application/octet-stream" }); ext = "bin"; } - + const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${img.name || `image_${img.id}`}.${ext}`; - document.body.appendChild(a); a.click(); - document.body.removeChild(a); URL.revokeObjectURL(url); + showNotification(`Exported: ${img.name || img.id}`); + }; + + const handleReplace = async (e: React.ChangeEvent) => { + if (!swfData || !selectedImageId || !e.target.files?.[0]) return; + const file = e.target.files[0]; + playPressSound(); + const buffer = await file.arrayBuffer(); + const newData = new Uint8Array(buffer); + const newTags = SwfService.updateImageTag( + swfData.tags, + selectedImageId, + newData, + selectedImage?.type || "unknown" + ); + + const newSwfData = { ...swfData, tags: newTags }; + setSwfData(newSwfData); + const extracted = SwfService.extractImages(newTags); + setImages(extracted); + setImageUrls(prev => { + const next = { ...prev }; + delete next[selectedImageId]; + return next; + }); + + showNotification("Image Replaced", "success"); + e.target.value = ""; + }; + + const handleSaveSwf = () => { + if (!swfData) return; + playPressSound(); + const result = SwfService.serialize(swfData.version, swfData.compressed, swfData.frameHeader, swfData.tags); + const blob = new Blob([result as any], { type: "application/x-shockwave-flash" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName || "output.swf"; + a.click(); + URL.revokeObjectURL(url); + showNotification("SWF Saved Successfully"); }; return ( @@ -84,92 +178,158 @@ export default function SwfView() { animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} transition={{ duration: animationsEnabled ? 0.3 : 0 }} - className="flex flex-col items-center w-full max-w-4xl outline-none" + className="flex flex-col items-center w-full max-w-6xl h-[85vh] outline-none" > -
-

- SWF Image Viewer +
+

+ SWF Editor

- - +
+ + +
-
-
-
- -
- {fileName &&
{fileName}
} -
+ + - {error && ( -
- {error} + {!swfData ? ( +
+ +

Open an SWF file to begin editing

+
+ ) : ( +
+
+
+ setSearchTerm(e.target.value)} + className="w-full bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" + />
- )} - {loading && ( -
Loading...
- )} +
+ {filteredImages.map(img => ( +
{ playPressSound(); setSelectedImageId(img.id); }} + className={`flex items-center gap-3 p-3 cursor-pointer transition-all border-l-4 ${selectedImageId === img.id + ? "bg-[#FFFF55]/10 border-[#FFFF55] text-[#FFFF55]" + : "border-transparent hover:bg-white/5 text-white/60 hover:text-white" + }`} + > + +
+ ID: {img.id} {img.name ? `- ${img.name}` : ""} + {img.type} {img.width ? `(${img.width}x${img.height})` : ""} +
+
+ ))} +
+
-
- {images.length > 0 && ( -
- Found {images.length} Image(s) -
- )} -
- {images.map((img, i) => ( -
handleDownload(img)} - className="cursor-pointer bg-black/60 border-2 border-[#373737] flex flex-col p-2 relative group hover:border-white transition-colors" - title="Click to download" - > -
- {img.type === "jpeg" || img.type === "png" || img.type === "gif" ? ( - {`Tag - ) : ( - - Raw Bytes
{img.type}
({img.data.length} b) -
- )} -
-
-
- ID: {img.id} - {img.type} +
+ + {!selectedImage ? ( +
+ Select an image to view details +
+ ) : ( + +
+
+

+ {selectedImage.name || `Bitmap ${selectedImage.id}`} +

+
+ Character ID: {selectedImage.id} + Type: {selectedImage.type} + {selectedImage.width && Size: {selectedImage.width}x{selectedImage.height}} +
- {img.name && ( - - {img.name} - +
+ +
+ {imageUrls[selectedImage.id] ? ( + {`Bitmap + ) : ( +
Loading Preview...
)}
-
- ))} -
-
-
+ +
+ + +
+ + )} + +
+
+ )} + + + + + {notification && ( + + + {notification.message} + + + )} + +