diff --git a/src/App.tsx b/src/App.tsx index 6142e9d..26d398f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -716,10 +716,8 @@ function App() { message?.isFromExtension ) { qortalRequestPermissonFromExtension(message, event); - } else if(message?.action === 'SHOW_SAVE_FILE_PICKER'){ - showSaveFilePicker(message?.payload) - - } else if(message?.action === 'getFileFromIndexedDB'){ + } + else if(message?.action === 'getFileFromIndexedDB'){ handleGetFileFromIndexedDB(event); } }; @@ -1521,7 +1519,11 @@ function App() { show, message, rootHeight, - showInfo + showInfo, + openSnackGlobal: openSnack, + setOpenSnackGlobal: setOpenSnack, + infoSnackCustom: infoSnack, + setInfoSnackCustom: setInfoSnack }} > { - const base64Prefix = 'data:video/mp4;base64,'; +export const saveFileInChunks = async ( + blob: Blob, + fileName: string, + chunkSize = 1024 * 1024 +) => { try { let offset = 0; let isFirstChunk = true; - const fullFileName = fileName + Date.now() + '.mp4' + + // Extract the MIME type from the blob + const mimeType = blob.type || 'application/octet-stream'; + + // Create the dynamic base64 prefix + const base64Prefix = `data:${mimeType};base64,`; + + // Function to extract extension from fileName + const getExtensionFromFileName = (name: string): string => { + const lastDotIndex = name.lastIndexOf('.'); + if (lastDotIndex !== -1) { + return name.substring(lastDotIndex); // includes the dot + } + return ''; + }; + + // Extract existing extension from fileName + const existingExtension = getExtensionFromFileName(fileName); + + // Remove existing extension from fileName to avoid duplication + if (existingExtension) { + fileName = fileName.substring(0, fileName.lastIndexOf('.')); + } + + // Map MIME type to file extension + const mimeTypeToExtension = (mimeType: string): string => { + + return mimeToExtensionMap[mimeType] || existingExtension || ''; // Use existing extension if MIME type not found + }; + + // Determine the final extension to use + const extension = mimeTypeToExtension(mimeType); + + // Construct the full file name with timestamp and extension + const fullFileName = `${fileName}_${Date.now()}${extension}`; + // Read the blob in chunks while (offset < blob.size) { // Extract the current chunk @@ -28,7 +69,7 @@ export const saveFileInChunks = async (blob: Blob, fileName: string, chunkSize = data: isFirstChunk ? base64Prefix + base64Chunk : base64Chunk, directory: Directory.Documents, recursive: true, - append: !isFirstChunk // Append after the first chunk + append: !isFirstChunk, // Append after the first chunk }); // Update offset and flag @@ -36,13 +77,14 @@ export const saveFileInChunks = async (blob: Blob, fileName: string, chunkSize = isFirstChunk = false; } - console.log("File saved successfully in chunks:", fileName); + console.log('File saved successfully in chunks:', fullFileName); } catch (error) { - console.error("Error saving file in chunks:", error); + console.error('Error saving file in chunks:', error); } }; + // Helper function to convert a Blob to a Base64 string const blobToBase64 = (blob: Blob): Promise => { return new Promise((resolve, reject) => { @@ -220,19 +262,42 @@ const UIQortalRequests = [ - export const showSaveFilePicker = async (data) => { - let blob; - let fileName; + export const showSaveFilePicker = async (data, {openSnackGlobal, + setOpenSnackGlobal, + infoSnackCustom, + setInfoSnackCustom}) => { + try { - const { filename, mimeType, fileId } = data; + const { filename, mimeType, blob } = data; - // Retrieve file from IndexedDB or any other source - blob = await retrieveFileFromIndexedDB(fileId); - fileName = filename; + setInfoSnackCustom({ + type: "info", + message: + "Saving file...", + }); + + + setOpenSnackGlobal(true); + + await saveFileInChunks(blob, filename) + setInfoSnackCustom({ + type: "success", + message: + "Saving file success!", + }); + - await saveFileInChunks(blob, fileName) + setOpenSnackGlobal(true); } catch (error) { + setInfoSnackCustom({ + type: "error", + message: + error?.message ? `Error saving file: ${error?.message}` : 'Error saving file', + }); + + + setOpenSnackGlobal(true); console.error("Error saving file:", error); } @@ -323,6 +388,10 @@ currentIndex: -1, isDOMContentLoaded: false }) const setHasSettingsChangedAtom = useSetRecoilState(navigationControllerAtom); + const { openSnackGlobal, + setOpenSnackGlobal, + infoSnackCustom, + setInfoSnackCustom } = useContext(MyContext); useEffect(()=> { @@ -391,10 +460,23 @@ isDOMContentLoaded: false { action: event.data.action, type: 'qortalRequest', payload: event.data, isExtension: true }, event.ports[0] ); + } else if(event?.data?.action === 'SAVE_FILE' + ){ + try { + const res = await saveFile( event.data, null, true, { + openSnackGlobal, + setOpenSnackGlobal, + infoSnackCustom, + setInfoSnackCustom + }); + + } catch (error) { + + } } else if ( event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE' || - event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE' + event?.data?.action === 'ENCRYPT_DATA' ) { let data; diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 3ca1c9e..d6df7a1 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -303,25 +303,7 @@ function setLocalStorage(key, data) { break; } - case "SAVE_FILE": { - try { - const res = await saveFile(request.payload, event.source, isFromExtension); - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - payload: res, - type: "backgroundMessageResponse", - }, event.origin); - } catch (error) { - event.source.postMessage({ - requestId: request.requestId, - action: request.action, - error: error.message, - type: "backgroundMessageResponse", - }, event.origin); - } - break; - } + case "DEPLOY_AT": { try { diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 88c4091..b53fa5a 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -15,6 +15,7 @@ import { isUsingLocal } from "../background"; import { getNameInfo } from "../backgroundFunctions/encryption"; +import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener"; import { QORT_DECIMALS } from "../constants/constants"; import Base58 from "../deps/Base58"; import { @@ -223,12 +224,12 @@ function getFileFromContentScript(fileId) { } -function sendToSaveFilePicker(data) { - window.postMessage({ - action: "SHOW_SAVE_FILE_PICKER", - payload: data, - }, "*"); -} +// function sendToSaveFilePicker(data) { +// window.postMessage({ +// action: "SHOW_SAVE_FILE_PICKER", +// payload: data, +// }, "*"); +// } const responseResolvers = new Map(); @@ -1149,9 +1150,9 @@ export const joinGroup = async (data, isFromExtension) => { } }; -export const saveFile = async (data, sender, isFromExtension) => { +export const saveFile = async (data, sender, isFromExtension, snackMethods) => { try { - const requiredFields = ["filename", "fileId"]; + const requiredFields = ['filename', 'blob'] const missingFields: string[] = []; requiredFields.forEach((field) => { if (!data[field]) { @@ -1194,15 +1195,20 @@ export const saveFile = async (data, sender, isFromExtension) => { }, }; } - sendToSaveFilePicker( - { - filename, - mimeType, - blob, - fileId, - fileHandleOptions, - } - ); + + showSaveFilePicker( { + filename, + mimeType, + blob + }, snackMethods) + // sendToSaveFilePicker( + // { + // filename, + // mimeType, + // blob, + // fileId + // } + // ); return true; } else { throw new Error("User declined to save file"); diff --git a/src/utils/memeTypes.ts b/src/utils/memeTypes.ts index 2bc5873..2bd8ac0 100644 --- a/src/utils/memeTypes.ts +++ b/src/utils/memeTypes.ts @@ -12,10 +12,13 @@ export const mimeToExtensionMap = { "application/vnd.oasis.opendocument.presentation": ".odp", "text/plain": ".txt", "text/csv": ".csv", - "text/html": ".html", "application/xhtml+xml": ".xhtml", "application/xml": ".xml", - "application/json": ".json", + "application/rtf": ".rtf", + "application/vnd.apple.pages": ".pages", + "application/vnd.google-apps.document": ".gdoc", + "application/vnd.google-apps.spreadsheet": ".gsheet", + "application/vnd.google-apps.presentation": ".gslides", // Images "image/jpeg": ".jpg", @@ -25,6 +28,11 @@ export const mimeToExtensionMap = { "image/svg+xml": ".svg", "image/tiff": ".tif", "image/bmp": ".bmp", + "image/x-icon": ".ico", + "image/heic": ".heic", + "image/heif": ".heif", + "image/apng": ".apng", + "image/avif": ".avif", // Audio "audio/mpeg": ".mp3", @@ -32,6 +40,11 @@ export const mimeToExtensionMap = { "audio/wav": ".wav", "audio/webm": ".weba", "audio/aac": ".aac", + "audio/flac": ".flac", + "audio/x-m4a": ".m4a", + "audio/x-ms-wma": ".wma", + "audio/midi": ".midi", + "audio/x-midi": ".mid", // Video "video/mp4": ".mp4", @@ -45,6 +58,7 @@ export const mimeToExtensionMap = { "video/3gpp2": ".3g2", "video/x-matroska": ".mkv", "video/x-flv": ".flv", + "video/x-ms-asf": ".asf", // Archives "application/zip": ".zip", @@ -53,4 +67,57 @@ export const mimeToExtensionMap = { "application/x-7z-compressed": ".7z", "application/x-gzip": ".gz", "application/x-bzip2": ".bz2", -} \ No newline at end of file + "application/x-apple-diskimage": ".dmg", + "application/vnd.android.package-archive": ".apk", + "application/x-iso9660-image": ".iso", + + // Code Files + "text/javascript": ".js", + "text/css": ".css", + "text/html": ".html", + "application/json": ".json", + "text/xml": ".xml", + "application/x-sh": ".sh", + "application/x-csh": ".csh", + "text/x-python": ".py", + "text/x-java-source": ".java", + "application/java-archive": ".jar", + "application/vnd.microsoft.portable-executable": ".exe", + "application/x-msdownload": ".msi", + "text/x-c": ".c", + "text/x-c++": ".cpp", + "text/x-go": ".go", + "application/x-perl": ".pl", + "text/x-php": ".php", + "text/x-ruby": ".rb", + "text/x-sql": ".sql", + "application/x-httpd-php": ".php", + "application/x-python-code": ".pyc", + + // ROM Files + "application/x-nintendo-nes-rom": ".nes", + "application/x-snes-rom": ".smc", + "application/x-gameboy-rom": ".gb", + "application/x-gameboy-advance-rom": ".gba", + "application/x-n64-rom": ".n64", + "application/x-sega-genesis-rom": ".gen", + "application/x-sega-master-system-rom": ".sms", + "application/x-psx-rom": ".iso", // PlayStation ROMs + "application/x-bios-rom": ".rom", + "application/x-flash-rom": ".bin", + "application/x-eeprom": ".eep", + "application/x-c64-rom": ".prg", + + // Miscellaneous + "application/octet-stream": ".bin", // General binary files + "application/x-shockwave-flash": ".swf", + "application/x-silverlight-app": ".xap", + "application/x-ms-shortcut": ".lnk", + "application/vnd.ms-fontobject": ".eot", + "font/woff": ".woff", + "font/woff2": ".woff2", + "font/ttf": ".ttf", + "font/otf": ".otf", + "application/vnd.visio": ".vsd", + "application/vnd.ms-project": ".mpp", +};