@@ -24,25 +24,44 @@ const MESSAGE_ACTIONS = {
2424 SHOW_SUCCESS_TOAST : "showDownloadSuccessToast" ,
2525 SHOW_ERROR_TOAST : "showDownloadErrorToast" ,
2626} ;
27+ let lastClickPosition = { x : null , y : null } ;
2728
2829// ============================================================================
2930// CONTEXT MENU MANAGEMENT
3031// ============================================================================
3132async function createLocalSaveContextMenus ( suffixes ) {
3233 await removeLocalSaveContextMenus ( ) ;
3334
35+ const isFirefox = DetectBrowser . isFirefox ( ) ;
36+
37+ const icons = {
38+ 16 : "../icons/folder.png" ,
39+ 32 : "../icons/folder.png" ,
40+ 48 : "../icons/folder.png" ,
41+ 128 : "../icons/folder.png" ,
42+ } ;
43+
44+ const menuCreateOptions = {
45+ documentUrlPatterns : [ "https://*/*" , "http://*/*" ] ,
46+ contexts : [ "frame" , "image" , "page" ] ,
47+ } ;
48+
49+ if ( isFirefox ) {
50+ menuCreateOptions . icons = icons ;
51+ }
52+
3453 browser . contextMenus . create ( {
3554 id : CONTEXT_MENU_IDS . LOCAL_SAVE_PARENT ,
3655 title : "Salvar imagem (local)" ,
37- contexts : [ "image" ] ,
56+ ... menuCreateOptions ,
3857 } ) ;
3958
4059 suffixes . forEach ( ( suffix ) => {
4160 browser . contextMenus . create ( {
4261 id : `${ CONTEXT_MENU_IDS . LOCAL_SAVE_PREFIX } ${ suffix } ` ,
4362 parentId : CONTEXT_MENU_IDS . LOCAL_SAVE_PARENT ,
4463 title : suffix ,
45- contexts : [ "image" ] ,
64+ ... menuCreateOptions ,
4665 } ) ;
4766 } ) ;
4867}
@@ -52,6 +71,12 @@ function createDropboxSaveContextMenu() {
5271 id : CONTEXT_MENU_IDS . DROPBOX_SAVE ,
5372 title : "Salvar no Dropbox" ,
5473 contexts : [ "image" ] ,
74+ icons : {
75+ 16 : "../icons/dropbox.png" ,
76+ 32 : "../icons/dropbox.png" ,
77+ 48 : "../icons/dropbox.png" ,
78+ 128 : "../icons/dropbox.png" ,
79+ } ,
5580 } ) ;
5681}
5782
@@ -97,7 +122,108 @@ async function updateContextMenusBasedOnSettings(settings) {
97122}
98123
99124// ============================================================================
100- // IMAGE DOWNLOAD
125+ // BLOB HANDLING
126+ // ============================================================================
127+ function isBlobUrl ( url ) {
128+ return url && url . startsWith ( "blob:" ) ;
129+ }
130+
131+ async function downloadBlobUrl ( blobUrl , tabId ) {
132+ try {
133+ // Enviar mensagem para o content script fazer o fetch
134+ const response = await browser . tabs . sendMessage ( tabId , {
135+ action : "FETCH_BLOB_DATA" ,
136+ blobUrl : blobUrl ,
137+ } ) ;
138+
139+ if ( response . success ) {
140+ return {
141+ arrayBuffer : response . arrayBuffer ,
142+ mimeType : response . mimeType ,
143+ extension : response . extension ,
144+ } ;
145+ } else {
146+ throw new Error ( response . error ) ;
147+ }
148+ } catch ( error ) {
149+ console . error ( "Erro ao processar blob URL:" , error ) ;
150+ throw error ;
151+ }
152+ }
153+
154+ async function CreateBlobUrl ( arrayBuffer , mimeType ) {
155+ let bufferToUse ;
156+ if ( Array . isArray ( arrayBuffer ) ) {
157+ bufferToUse = new Uint8Array ( arrayBuffer ) ;
158+ } else {
159+ bufferToUse = arrayBuffer ;
160+ }
161+
162+ const blob = new Blob ( [ bufferToUse ] , { type : mimeType } ) ;
163+ const downloadUrl = URL . createObjectURL ( blob ) ;
164+
165+ console . log ( "Blob URL created:" , downloadUrl ) ;
166+
167+ return downloadUrl ;
168+ }
169+
170+ async function resolveImagemInput ( info , tab ) {
171+ if ( ! info ?. mediaType || info . mediaType !== "image" ) {
172+ const response = await browser . tabs . sendMessage ( tab . id , {
173+ action : "download-image" ,
174+ position : lastClickPosition ,
175+ } ) ;
176+
177+ console . log ( { response } ) ;
178+
179+ if ( response && response . url ) {
180+ const { url : imageUrl , type } = response ;
181+ if ( type === "blob" ) {
182+ const blobData = await downloadBlobUrl ( imageUrl , tab . id ) ;
183+ const url = await CreateBlobUrl (
184+ blobData . arrayBuffer ,
185+ blobData . mimeType
186+ ) ;
187+
188+ return {
189+ url : url ,
190+ fileExtension : blobData . extension ,
191+ } ;
192+ } else {
193+ const url = imageUrl ;
194+
195+ return {
196+ url : url ,
197+ fileExtension : getFileExtension ( url ) ,
198+ } ;
199+ }
200+ }
201+ }
202+
203+ if ( ! info . srcUrl ) {
204+ return null ;
205+ }
206+
207+ const isBlob = isBlobUrl ( info . srcUrl ) ;
208+
209+ if ( isBlob ) {
210+ const blobData = await downloadBlobUrl ( info . srcUrl , tab . id ) ;
211+ const url = await CreateBlobUrl ( blobData . arrayBuffer , blobData . mimeType ) ;
212+
213+ return {
214+ url : url ,
215+ fileExtension : blobData . extension ,
216+ } ;
217+ }
218+
219+ return {
220+ url : info . srcUrl ,
221+ fileExtension : getFileExtension ( info . srcUrl ) ,
222+ } ;
223+ }
224+
225+ // ============================================================================
226+ // IMAGE DOWNLOAD (UPDATED)
101227// ============================================================================
102228function buildDownloadFilename ( suffix , fileExtension ) {
103229 const randomName = generateRandomName ( 10 ) ;
@@ -125,6 +251,7 @@ async function sendSuccessNotification(tabId, message) {
125251async function downloadImageToLocal (
126252 imageUrl ,
127253 folderSuffix = "direct" ,
254+ fileExtension = null ,
128255 tabId = null
129256) {
130257 if ( ! imageUrl ) {
@@ -133,19 +260,56 @@ async function downloadImageToLocal(
133260 }
134261
135262 try {
136- const fileExtension = getFileExtension ( imageUrl ) ;
263+ let downloadUrl = imageUrl ;
264+
265+ // Handle data URLs by converting to blob URLs
266+ if ( downloadUrl . startsWith ( "data:" ) ) {
267+ try {
268+ const response = await fetch ( downloadUrl ) ;
269+ const blob = await response . blob ( ) ;
270+ downloadUrl = URL . createObjectURL ( blob ) ;
271+ } catch ( fetchError ) {
272+ console . error ( "Error converting data URL to blob URL:" , fetchError ) ;
273+ await browser . tabs . sendMessage ( tabId , {
274+ action : MESSAGE_ACTIONS . SHOW_ERROR_TOAST ,
275+ message : "Erro ao processar a imagem. Tente novamente." ,
276+ } ) ;
277+ return ;
278+ }
279+ }
280+
137281 const filename = buildDownloadFilename ( folderSuffix , fileExtension ) ;
138282
139283 await browser . downloads . download ( {
140- url : imageUrl ,
284+ url : downloadUrl ,
141285 filename : filename ,
142286 conflictAction : "uniquify" ,
143287 saveAs : false ,
144288 } ) ;
145289
290+ // Clean up any blob URLs we created
291+ if (
292+ ( isBlobUrl ( downloadUrl ) || downloadUrl . startsWith ( "blob:" ) ) &&
293+ downloadUrl !== imageUrl
294+ ) {
295+ setTimeout ( ( ) => URL . revokeObjectURL ( downloadUrl ) , 1000 ) ;
296+ }
297+
146298 await sendSuccessNotification ( tabId , "Imagem salva com sucesso!" ) ;
147299 } catch ( error ) {
148300 console . error ( "Erro ao baixar imagem:" , error ) ;
301+
302+ // Send error notification to user
303+ if ( tabId ) {
304+ try {
305+ await browser . tabs . sendMessage ( tabId , {
306+ action : MESSAGE_ACTIONS . SHOW_ERROR_TOAST ,
307+ message : "Erro ao salvar imagem. Tente novamente." ,
308+ } ) ;
309+ } catch ( msgError ) {
310+ console . error ( "Erro ao enviar mensagem de erro:" , msgError ) ;
311+ }
312+ }
149313 }
150314}
151315
@@ -216,20 +380,37 @@ async function handleStorageChanges(changes) {
216380// ============================================================================
217381// EVENT HANDLERS
218382// ============================================================================
219- function handleContextMenuClick ( info , tab ) {
383+
384+ async function handleContextMenuClick ( info , tab ) {
220385 const menuItemId = info . menuItemId ;
221386
222387 if ( menuItemId . startsWith ( CONTEXT_MENU_IDS . LOCAL_SAVE_PREFIX ) ) {
223388 const folderSuffix = menuItemId . replace (
224389 CONTEXT_MENU_IDS . LOCAL_SAVE_PREFIX ,
225390 ""
226391 ) ;
227- downloadImageToLocal ( info . srcUrl , folderSuffix , tab ?. id ) ;
392+
393+ const resolvedImage = await resolveImagemInput ( info , tab ) ;
394+ if ( ! resolvedImage || ! resolvedImage . url ) {
395+ console . error ( "Failed to resolve image input." ) ;
396+ return ;
397+ }
398+
399+ console . log ( { resolvedImage } ) ;
400+
401+ const { url, fileExtension } = resolvedImage ;
402+ downloadImageToLocal ( url , folderSuffix , fileExtension , tab ?. id ) ;
228403 return ;
229404 }
230405
231406 if ( menuItemId === CONTEXT_MENU_IDS . DROPBOX_SAVE ) {
232- saveImageToDropbox ( info . srcUrl , tab ?. id ) ;
407+ const resolvedImage = await resolveImagemInput ( info , tab ) ;
408+ if ( ! resolvedImage || ! resolvedImage . url ) {
409+ console . error ( "Failed to resolve image input for Dropbox." ) ;
410+ return ;
411+ }
412+
413+ saveImageToDropbox ( resolvedImage . url , tab ?. id ) ;
233414 return ;
234415 }
235416}
@@ -240,7 +421,16 @@ async function handleDoubleClickDownload(imageUrl, tabId) {
240421 ) ;
241422
242423 if ( doubleClickEnabled ) {
243- await downloadImageToLocal ( imageUrl , "direct" , tabId ) ;
424+ const resolvedImage = await resolveImagemInput (
425+ { srcUrl : imageUrl , mediaType : "image" } ,
426+ tabId
427+ ) ;
428+ await downloadImageToLocal (
429+ resolvedImage . url ,
430+ "direct" ,
431+ resolvedImage . fileExtension ,
432+ tabId
433+ ) ;
244434 } else {
245435 console . log ( "Download por duplo clique desativado nas configurações." ) ;
246436 await sendSuccessNotification (
@@ -271,6 +461,13 @@ async function handleRuntimeMessage(message, sender, sendResponse) {
271461 } ) ;
272462 break ;
273463
464+ case "context-menu-position" :
465+ lastClickPosition = {
466+ x : message . x ,
467+ y : message . y ,
468+ } ;
469+ break ;
470+
274471 default :
275472 console . warn ( `Unknown action: ${ action } ` ) ;
276473 }
0 commit comments