diff --git a/public/content-script.js b/public/content-script.js index 5010905..698e67c 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -551,7 +551,32 @@ const testAsync = async (sendResponse)=> { sendResponse({ result: null, error: "Testing" }); } +const showSaveFilePicker = async (data) => { + try { + const {filename, mimeType, fileHandleOptions, fileId} = data + const blob = await retrieveFileFromIndexedDB(fileId) + const fileHandle = await window.showSaveFilePicker({ + suggestedName: filename, + types: [ + { + description: mimeType, + ...fileHandleOptions + } + ] + }) + const writeFile = async (fileHandle, contents) => { + const writable = await fileHandle.createWritable() + await writable.write(contents) + await writable.close() + } + writeFile(fileHandle, blob).then(() => console.log("FILE SAVED")) +} catch (error) { + FileSaver.saveAs(blob, filename) +} +} + chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { + console.log('message', message) if (message.type === "LOGOUT") { // Notify the web page window.postMessage( @@ -571,6 +596,8 @@ chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) }, "*" ); + } else if(message.action === "SHOW_SAVE_FILE_PICKER"){ + showSaveFilePicker(message?.data) } else if (message.action === "getFileFromIndexedDB") { @@ -696,9 +723,27 @@ async function storeFilesInIndexedDB(obj) { obj.fileId = fileId; delete obj.file; } + if (obj.blob instanceof Blob) { + const fileId = "objFile_qortalfile"; + + // Store the file in IndexedDB + const fileData = { + id: fileId, + data: obj.blob, + }; + objectStore.put(fileData); + + // Replace the file object with the file ID in the original object + let blobObj = { + type: obj.blob?.type + } + obj.fileId = fileId; + delete obj.blob; + obj.blob = blobObj + } // Iterate through resources to find files and save them to IndexedDB - for (let resource of obj.resources) { + for (let resource of (obj?.resources || [])) { if (resource.file instanceof File) { const fileId = resource.identifier + "_qortalfile"; @@ -729,7 +774,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP'] if (!window.hasAddedQortalListener) { console.log("Listener added"); @@ -768,7 +813,7 @@ if (!window.hasAddedQortalListener) { { action: event.data.action, type: 'qortalRequest', payload: event.data }, event.ports[0] ); - } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE') { + } 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') { let data; try { data = await storeFilesInIndexedDB(event.data); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 91a3737..20db7b0 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, createPoll, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, joinGroup, publishMultipleQDNResources, publishQDNResource, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; +import { addListItems, createPoll, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; @@ -78,7 +78,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "ENCRYPT_DATA": { const data = request.payload; - encryptData(data) + encryptData(data, sender) .then((res) => { sendResponse(res); }) @@ -207,7 +207,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { } case "JOIN_GROUP": { const data = request.payload; - console.log('data', data) + joinGroup(data) .then((res) => { sendResponse(res); @@ -218,6 +218,19 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "SAVE_FILE": { + const data = request.payload; + + saveFile(data, sender) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 49fad5a..d155337 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -25,6 +25,7 @@ import { publishData } from "../qdn/publish/pubish"; import { getPermission, setPermission } from "../qortalRequests"; import { createTransaction } from "../transactions/transactions"; import { fileToBase64 } from "../utils/fileReading"; +import { mimeToExtensionMap } from "../utils/memeTypes"; const _createPoll = async (pollName, pollDescription, options) => { const fee = await getFee("CREATE_POLL"); @@ -127,6 +128,15 @@ function getFileFromContentScript(fileId, sender) { ); }); } +function sendToSaveFilePicker(data, sender) { + console.log("sender", sender); + + chrome.tabs.sendMessage( + sender.tab.id, + { action: "SHOW_SAVE_FILE_PICKER", data } + ); + + } async function getUserPermission(payload: any) { function waitForWindowReady(windowId) { @@ -251,11 +261,11 @@ export const getUserAccount = async () => { } }; -export const encryptData = async (data) => { +export const encryptData = async (data, sender) => { let data64 = data.data64; let publicKeys = data.publicKeys || []; - if (data.file) { - data64 = await fileToBase64(data.file); + if (data.fileId) { + data64 = await getFileFromContentScript(data.fileId, sender) } if (!data64) { throw new Error("Please include data to encrypt"); @@ -509,7 +519,6 @@ export const publishQDNResource = async (data: any, sender) => { const tag3 = data.tag3; const tag4 = data.tag4; const tag5 = data.tag5; - let feeAmount = null; if (data.identifier == null) { identifier = "default"; } @@ -1114,6 +1123,69 @@ export const joinGroup = async (data) => { }; + export const saveFile = async (data, sender) => { + try { + const requiredFields = ['filename', 'fileId'] + const missingFields: string[] = [] + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field) + } + }) + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + const filename = data.filename + const blob = data.blob + const fileId = data.fileId + const resPermission = await getUserPermission({ + text1: "Would you like to download:", + highlightedText: `${filename}`, + }); + const { accepted } = resPermission; + + if(accepted){ + + const mimeType = blob.type || data.mimeType + let backupExention = filename.split('.').pop() + if (backupExention) { + backupExention = '.' + backupExention + } + const fileExtension = mimeToExtensionMap[mimeType] || backupExention + let fileHandleOptions = {} + if (!mimeType) { + + throw new Error('A mimeType could not be derived') + } + if (!fileExtension) { + const obj = {} + throw new Error('A file extension could not be derived') + } + if (fileExtension && mimeType) { + fileHandleOptions = { + accept: { + [mimeType]: [fileExtension] + } + } + } + sendToSaveFilePicker( { + filename, mimeType, blob, fileId, fileHandleOptions + } ,sender) + return true + } else { + throw new Error("User declined add to list"); + + } + + } catch (error) { + + throw new Error(error?.message || 'Failed to initiate download') + } + + }; + export const sendCoin = async () => { try { const wallet = await getSaveWallet(); diff --git a/src/utils/memeTypes.ts b/src/utils/memeTypes.ts new file mode 100644 index 0000000..2bc5873 --- /dev/null +++ b/src/utils/memeTypes.ts @@ -0,0 +1,56 @@ +export const mimeToExtensionMap = { + // Documents + "application/pdf": ".pdf", + "application/msword": ".doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx", + "application/vnd.ms-excel": ".xls", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx", + "application/vnd.ms-powerpoint": ".ppt", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx", + "application/vnd.oasis.opendocument.text": ".odt", + "application/vnd.oasis.opendocument.spreadsheet": ".ods", + "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", + + // Images + "image/jpeg": ".jpg", + "image/png": ".png", + "image/gif": ".gif", + "image/webp": ".webp", + "image/svg+xml": ".svg", + "image/tiff": ".tif", + "image/bmp": ".bmp", + + // Audio + "audio/mpeg": ".mp3", + "audio/ogg": ".ogg", + "audio/wav": ".wav", + "audio/webm": ".weba", + "audio/aac": ".aac", + + // Video + "video/mp4": ".mp4", + "video/webm": ".webm", + "video/ogg": ".ogv", + "video/x-msvideo": ".avi", + "video/quicktime": ".mov", + "video/x-ms-wmv": ".wmv", + "video/mpeg": ".mpeg", + "video/3gpp": ".3gp", + "video/3gpp2": ".3g2", + "video/x-matroska": ".mkv", + "video/x-flv": ".flv", + + // Archives + "application/zip": ".zip", + "application/x-rar-compressed": ".rar", + "application/x-tar": ".tar", + "application/x-7z-compressed": ".7z", + "application/x-gzip": ".gz", + "application/x-bzip2": ".bz2", +} \ No newline at end of file