Last active
March 8, 2026 04:31
-
-
Save tam710562/67ce3c4387d72e94a83cf0ae9f890cec to your computer and use it in GitHub Desktop.
Revisions
-
tam710562 revised this gist
Dec 24, 2024 . 1 changed file with 25 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -366,6 +366,27 @@ return id; }, }, object: { isObject(item) { return (item && typeof item === 'object' && !Array.isArray(item)); }, merge(target, source) { let output = Object.assign({}, target); if (this.isObject(target) && this.isObject(source)) { for (const key in source) { if (this.isObject(source[key])) { if (!(key in target)) Object.assign(output, { [key]: source[key] }); else output[key] = this.merge(target[key], source[key]); } else { Object.assign(output, { [key]: source[key] }); } } } return output; }, }, }; const nameKey = 'easy-files'; @@ -726,7 +747,7 @@ colorBg = gnoh.string.toColorRgb(extension); } const isLightBg = gnoh.color.isLight(colorBg.r, colorBg.g, colorBg.b); const colorBgLighter = gnoh.color.shadeColor(colorBg.r, colorBg.g, colorBg.b, isLightBg ? 0.4 : -0.4); const fileIcon = gnoh.createElement('div', { class: 'file-icon', @@ -835,15 +856,15 @@ async function showDialogChooseFile({ info, sender, clipboardFiles, downloadedFiles }) { let disconnectResizeObserver; const buttonShowAllFilesElement = gnoh.object.merge(gnoh.constant.dialogButtons.submit, { label: langs.showMore, click() { showAllFiles(sender); disconnectResizeObserver && disconnectResizeObserver(); }, }); const buttonCancelElement = gnoh.object.merge(gnoh.constant.dialogButtons.cancel, { click() { disconnectResizeObserver && disconnectResizeObserver(); }, @@ -1056,4 +1077,4 @@ } }); }, () => window.vivaldiWindowId != null); })(); -
tam710562 revised this gist
Dec 24, 2024 . 1 changed file with 16 additions and 8 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -31,6 +31,10 @@ .map(x => x.trim()) .filter(x => !!x && (x.startsWith('.') || /\w+\/([-+.\w]+|\*)/.test(x))); if (!mimeTypes.length) { return true; } for (const mt of mimeTypes) { if ( mt.startsWith('.') @@ -175,10 +179,13 @@ return this.getLuminance(r, g, b) < 156; }, shadeColor(r, g, b, percent) { const t = percent < 0 ? 0 : 255 * percent; const p = percent < 0 ? 1 + percent : 1 - percent; return { r: Math.round(parseInt(r) * p + t), g: Math.round(parseInt(g) * p + t), b: Math.round(parseInt(b) * p + t), }; }, }, get constant() { @@ -266,7 +273,8 @@ button.value = button.label; delete button.label; } button.element = this.createElement('input', button); buttonElements.push(button.element); } const focusModal = this.createElement('span', { @@ -484,8 +492,8 @@ pointerPosition.y = event.clientY; } document.addEventListener('click', handleClick); document.addEventListener('mousedown', handleMouseDown); function changeFile(dataTransfer) { fileInput.files = dataTransfer.files; @@ -1048,4 +1056,4 @@ } }); }, () => window.vivaldiWindowId != null); })(); -
tam710562 revised this gist
Nov 9, 2024 . 1 changed file with 115 additions and 60 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -7,7 +7,18 @@ 'use strict'; const gnoh = { stream: { async compress(input, outputType = 'arrayBuffer', format = 'gzip') { const compressedStream = new Response(input).body .pipeThrough(new CompressionStream(format)); return await new Response(compressedStream)[outputType](); }, }, file: { readableFileSize(size) { const i = Math.floor(Math.log(size) / Math.log(1024)); return `${(size / Math.pow(1024, i)).toFixed(2)} ${['B', 'kB', 'MB', 'GB', 'TB'][i]}`; }, getFileExtension(fileName) { return /(?:\.([^.]+))?$/.exec(fileName)[1]; }, @@ -65,7 +76,7 @@ }, element: { getStyle(element) { return getComputedStyle(element); }, }, createElement(tagName, attribute, parent, inner, options) { @@ -207,10 +218,12 @@ } } function onClickCloseDialog(windowId, mousedown, button, clientX, clientY) { if ( config.autoClose && windowId === vivaldiWindowId && mousedown && !document.elementFromPoint(clientX, clientY).closest('.dialog-custom[data-dialog-id="' + id + '"]') ) { closeDialog(true); } @@ -224,11 +237,11 @@ modalBg.remove(); } vivaldi.tabsPrivate.onKeyboardShortcut.removeListener(onKeyCloseDialog); vivaldi.tabsPrivate.onWebviewClickCheck.addListener(onClickCloseDialog); } vivaldi.tabsPrivate.onKeyboardShortcut.addListener(onKeyCloseDialog); vivaldi.tabsPrivate.onWebviewClickCheck.addListener(onClickCloseDialog); const buttonElements = []; for (let button of buttons) { @@ -300,15 +313,15 @@ close: closeDialog, }; }, timeOut(callback, condition, timeOut = 300) { let timeOutId = setTimeout(function wait() { let result; if (!condition) { result = document.getElementById('browser'); } else if (typeof condition === 'string') { result = document.querySelector(condition); } else if (typeof condition === 'function') { result = condition(); } else { return; } @@ -350,14 +363,20 @@ const nameKey = 'easy-files'; const langs = { showMore: gnoh.i18n.getMessage('Show more'), chooseAFile: gnoh.i18n.getMessage('Choose a File...'), clipboard: gnoh.i18n.getMessage('Clipboard'), downloads: gnoh.i18n.getMessage('Downloads'), }; const chunkSize = 1024 * 1024 * 10; // 10MB const maxAllowedSize = 1024 * 1024 * 5; // 5MB const pointerPosition = { x: 0, y: 0, }; gnoh.addStyle([ `.${nameKey}.dialog-custom .dialog-content { flex-flow: wrap; gap: 18px; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper { overflow: hidden; margin: -2px; padding: 2px; }`, @@ -392,6 +411,30 @@ y: 0, }; async function decompressArrayBuffer(input) { const decompressedStream = new Response(input).body .pipeThrough(new DecompressionStream('gzip')); return await new Response(decompressedStream).arrayBuffer(); } function getRect(element) { const rect = element.getBoundingClientRect().toJSON(); while (element = element.offsetParent) { if (getComputedStyle(element).overflow !== 'visible') { const parentRect = element.getBoundingClientRect(); rect.left = Math.max(rect.left, parentRect.left); rect.top = Math.max(rect.top, parentRect.top); rect.right = Math.min(rect.right, parentRect.right); rect.bottom = Math.min(rect.bottom, parentRect.bottom); rect.width = rect.right - rect.left; rect.height = rect.bottom - rect.top; rect.x = rect.left; rect.y = rect.top; } } return rect; } function handleClick(event) { if (event.target.matches('input[type=file]:not([webkitdirectory])')) { event.preventDefault(); @@ -407,7 +450,7 @@ contentVisibilityAuto: true, }) ) { elementClickedRect = getRect(fileInput); } const attributes = {}; @@ -435,7 +478,7 @@ } function handleMouseDown(event) { elementClickedRect = getRect(event.target); pointerPosition.x = event.clientX; pointerPosition.y = event.clientY; @@ -450,16 +493,19 @@ fileInput.dispatchEvent(new Event('change', { bubbles: true })); } chrome.runtime.onMessage.addListener(async (info, sender, sendResponse) => { if (info.type === nameKey) { switch (info.action) { case 'file': fileData[info.file.fileDataIndex] = info.file.fileData; if (Object.entries(fileData).length === info.file.fileDataLength) { const dataTransfer = new DataTransfer(); const base64String = fileData.join(''); const unit8Array = Uint8Array.from(atob(base64String), c => c.charCodeAt(0)); const decompressedArrayBuffer = await decompressArrayBuffer(unit8Array); dataTransfer.items.add(new File( [decompressedArrayBuffer], info.file.fileName, { type: info.file.mimeType }, )); @@ -545,19 +591,23 @@ } } if (checkType && (!maxAllowedSize || file.size <= maxAllowedSize)) { let blob = new Blob([file], { type: file.type }); if (!item.isRealFile && supportedType.mimeType === 'image/jpeg') { blob = await convertPngToJpeg(blob); } const arrayBuffer = await blob.arrayBuffer(); const compressedArrayBuffer = await gnoh.stream.compress(arrayBuffer); const compressedBase64String = btoa(new Uint8Array(compressedArrayBuffer) .reduce((data, byte) => data + String.fromCharCode(byte), '')); const fileData = gnoh.array.chunks(compressedBase64String, chunkSize); const clipboardFile = { fileData: fileData, fileDataLength: fileData.length, mimeType: blob.type, size: blob.size, category: 'clipboard', }; @@ -575,7 +625,7 @@ case 'image/gif': case 'image/bmp': clipboardFile.previewUrl = await vivaldi.utilities.storeImage({ data: arrayBuffer, mimeType: blob.type, }); break; @@ -630,13 +680,14 @@ downloadedFile && downloadedFile.exists === true && downloadedFile.state === 'complete' && (!maxAllowedSize || downloadedFile.fileSize <= maxAllowedSize) && !result[downloadedFile.filename] ) { const file = { mimeType: downloadedFile.mime, path: downloadedFile.filename, fileName: downloadedFile.filename.replace(/^.*[\\/]/, ''), size: downloadedFile.fileSize, category: 'downloaded-file', }; @@ -688,7 +739,7 @@ async function createSelectbox(sender, file, dialog) { const selectbox = gnoh.createElement('button', { title: `${file.fileName ? file.fileName + '\n' : ''}Size: ${gnoh.file.readableFileSize(file.size)}`, class: 'selectbox', events: { async click(event) { @@ -698,8 +749,11 @@ switch (file.category) { case 'downloaded-file': if (!file.fileData) { const arrayBuffer = await vivaldi.mailPrivate.readFileToBuffer(file.path); const compressedArrayBuffer = await gnoh.stream.compress(arrayBuffer); const compressedBase64String = btoa(new Uint8Array(compressedArrayBuffer) .reduce((data, byte) => data + String.fromCharCode(byte), '')); file.fileData = gnoh.array.chunks(compressedBase64String, chunkSize); file.fileDataLength = file.fileData.length; } break; @@ -774,7 +828,7 @@ let disconnectResizeObserver; const buttonShowAllFilesElement = Object.assign({}, gnoh.constant.dialogButtons.submit, { label: langs.showMore, click() { showAllFiles(sender); disconnectResizeObserver && disconnectResizeObserver(); @@ -806,39 +860,41 @@ dialog.modalBg.style.right = 'unset'; dialog.modalBg.style.bottom = 'unset'; function setPosition(entries) { for (const entry of entries) { const dialogRect = entry.contentRect; if (info.elementClickedRect.left < 0) { dialog.modalBg.style.left = '0px'; } else if (info.elementClickedRect.right > window.innerWidth) { dialog.modalBg.style.right = '0px'; } else if (info.elementClickedRect.left + dialogRect.width > window.innerWidth) { dialog.modalBg.style.left = Math.max((info.elementClickedRect.right - dialogRect.width), 0) + 'px'; } else { dialog.modalBg.style.left = info.elementClickedRect.left + 'px'; } if (info.elementClickedRect.bottom < 0) { dialog.modalBg.style.top = '0px'; } else if (info.elementClickedRect.bottom + dialogRect.height > window.innerHeight) { dialog.modalBg.style.top = Math.max((info.elementClickedRect.top - dialogRect.height), 0) + 'px'; } else { dialog.modalBg.style.top = info.elementClickedRect.bottom + 'px'; } } } const resizeObserver = new ResizeObserver(setPosition); resizeObserver.observe(dialog.dialog); disconnectResizeObserver = () => resizeObserver.unobserve(dialog.dialog); if (clipboardFiles.length) { const selectboxWrapperClipboard = gnoh.createElement('div', { class: 'selectbox-wrapper', }); const h3Clipboard = gnoh.createElement('h3', { text: langs.clipboard, }, selectboxWrapperClipboard); const selectboxContainerClipboard = gnoh.createElement('div', { @@ -858,7 +914,7 @@ }); const h3Downloaded = gnoh.createElement('h3', { text: langs.downloads, }, selectboxWrapperDownloaded); const selectboxContainerDownloaded = gnoh.createElement('div', { @@ -884,13 +940,13 @@ }); } function chooseFile(sender, file) { if (!file.fileData.length) { file.fileData.push([]); } for (const [index, chunk] of file.fileData.entries()) { chrome.tabs.sendMessage(sender.tab.id, { type: nameKey, action: 'file', tabId: sender.tab.id, @@ -908,15 +964,16 @@ } } vivaldi.tabsPrivate.onWebviewClickCheck.addListener((windowId, mousedown, button, clientX, clientY) => { if ( windowId === vivaldiWindowId && mousedown && button === 0 ) { pointerPosition.x = clientX; pointerPosition.y = clientY; } }); chrome.runtime.onMessage.addListener(async (info, sender, sendResponse) => { if ( @@ -990,7 +1047,5 @@ }); } }); }, () => window.vivaldiWindowId != null); })(); -
tam710562 revised this gist
Sep 22, 2024 . 1 changed file with 183 additions and 68 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -3,11 +3,14 @@ * Written by Tam710562 */ (() => { 'use strict'; const gnoh = { file: { getFileExtension(fileName) { return /(?:\.([^.]+))?$/.exec(fileName)[1]; }, verifyAccept({ fileName, mimeType }, accept) { if (!accept) { return true; @@ -33,9 +36,7 @@ i18n: { getMessageName(message, type) { message = (type ? type + '\x04' : '') + message; return message.replace(/[^a-z0-9]/g, (i) => '_' + i.codePointAt(0) + '_') + '0'; }, getMessage(message, type) { return chrome.i18n.getMessage(this.getMessageName(message, type)) || message; @@ -62,6 +63,11 @@ return result; }, }, element: { getStyle(element) { return window.getComputedStyle(element); }, }, createElement(tagName, attribute, parent, inner, options) { if (typeof tagName === 'undefined') { return; @@ -129,11 +135,16 @@ }).content; }, string: { toHashCode(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; // Convert to 32bit integer } return hash; }, toColorRgb(str) { let hash = this.toHashCode(str); let r = (hash >> (0 * 8)) & 0xff; let g = (hash >> (1 * 8)) & 0xff; @@ -150,7 +161,7 @@ return 0.2126 * r + 0.7152 * g + 0.0722 * b; }, isLight(r, g, b) { return this.getLuminance(r, g, b) < 156; }, shadeColor(r, g, b, percent) { r = Math.max(Math.min(255, r + percent), 0); @@ -170,14 +181,7 @@ label: this.i18n.getMessage('Cancel'), cancel: true }, }, }; }, dialog(title, content, buttons = [], config) { @@ -288,9 +292,10 @@ class: 'slide', }, inner, [focusModal.cloneNode(true), div, focusModal.cloneNode(true)]); return { dialog, dialogHeader, dialogContent, modalBg, buttons: buttonElements, close: closeDialog, }; @@ -328,14 +333,14 @@ generate(ids) { let d = Date.now() + performance.now(); let r; const id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); if (Array.isArray(ids) && ids.includes(id)) { return this.generate(ids); } return id; }, @@ -380,6 +385,12 @@ const fileData = []; let fileInput = null; let elementClickedRect = null; const pointerPosition = { x: 0, y: 0, }; function handleClick(event) { if (event.target.matches('input[type=file]:not([webkitdirectory])')) { @@ -388,6 +399,17 @@ fileInput = event.target; if ( event.isTrusted && fileInput.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true, }) ) { elementClickedRect = fileInput.getBoundingClientRect(); } const attributes = {}; for (const attr of fileInput.attributes) { @@ -396,15 +418,31 @@ fileData.length = 0; elementClickedRect.left = elementClickedRect.left - pointerPosition.x; elementClickedRect.top = elementClickedRect.top - pointerPosition.y; elementClickedRect.right = elementClickedRect.right - pointerPosition.x; elementClickedRect.bottom = elementClickedRect.bottom - pointerPosition.y; elementClickedRect.x = elementClickedRect.x - pointerPosition.x; elementClickedRect.y = elementClickedRect.y - pointerPosition.y; chrome.runtime.sendMessage({ type: nameKey, action: 'click', attributes, elementClickedRect, }); } } function handleMouseDown(event) { elementClickedRect = event.target.getBoundingClientRect(); pointerPosition.x = event.clientX; pointerPosition.y = event.clientY; } window.addEventListener('click', handleClick); window.addEventListener('mousedown', handleMouseDown); function changeFile(dataTransfer) { fileInput.files = dataTransfer.files; @@ -432,8 +470,6 @@ case 'picker': fileInput.showPicker(); break; } } }); @@ -473,7 +509,6 @@ document.execCommand('paste'); }); } async function readClipboard(accept) { @@ -627,7 +662,10 @@ } function createFileIcon(extension) { let colorBg = { r: 255, g: 255, b: 255 }; if (extension) { colorBg = gnoh.string.toColorRgb(extension); } const isLightBg = gnoh.color.isLight(colorBg.r, colorBg.g, colorBg.b); const colorBgLighter = gnoh.color.shadeColor(colorBg.r, colorBg.g, colorBg.b, isLightBg ? 80 : -80); @@ -653,15 +691,15 @@ title: file.fileName || '', class: 'selectbox', events: { async click(event) { event.preventDefault(); dialog.close(); switch (file.category) { case 'downloaded-file': if (!file.fileData) { const uint8Array = new Uint8Array(await vivaldi.mailPrivate.readFileToBuffer(file.path)); file.fileData = gnoh.array.chunks(uint8Array, chunkSize).map(a => Array.from(a)); file.fileDataLength = file.fileData.length; } break; @@ -700,7 +738,7 @@ src: file.previewUrl, }, selectboxImage); } else { const extension = file.extension || gnoh.file.getFileExtension(file.fileName); selectboxImage.append(createFileIcon(extension)); } @@ -713,30 +751,41 @@ }, selectboxTitle); if (file.fileName) { const extension = file.extension || gnoh.file.getFileExtension(file.fileName); const name = extension ? file.fileName.substring(0, file.fileName.length - extension.length - 1) : file.fileName; const filenameContainer = gnoh.createElement('div', { class: 'filename-text', text: name, }, filenameText); if (extension) { const filenameExtension = gnoh.createElement('div', { class: 'filename-extension', text: '.' + extension, }, filenameText); } } return selectbox; } async function showDialogChooseFile({ info, sender, clipboardFiles, downloadedFiles }) { let disconnectResizeObserver; const buttonShowAllFilesElement = Object.assign({}, gnoh.constant.dialogButtons.submit, { label: langs.showAllFiles, click() { showAllFiles(sender); disconnectResizeObserver && disconnectResizeObserver(); }, }); const buttonCancelElement = Object.assign({}, gnoh.constant.dialogButtons.cancel, { click() { disconnectResizeObserver && disconnectResizeObserver(); }, }); const dialog = gnoh.dialog( langs.chooseAFile, @@ -748,7 +797,42 @@ ); dialog.dialog.style.maxWidth = 570 + 'px'; dialog.modalBg.style.height = 'fit-content'; dialog.modalBg.style.position = 'fixed'; dialog.modalBg.style.margin = 'unset'; dialog.modalBg.style.minWidth = 'unset'; dialog.modalBg.style.left = 'unset'; dialog.modalBg.style.top = 'unset'; dialog.modalBg.style.right = 'unset'; dialog.modalBg.style.bottom = 'unset'; function setPosition() { const dialogRect = dialog.dialog.getBoundingClientRect(); if (info.elementClickedRect.left < 0) { dialog.modalBg.style.left = '0px'; } else if (info.elementClickedRect.left + dialogRect.width > window.innerWidth) { dialog.modalBg.style.left = Math.max((info.elementClickedRect.right - dialogRect.width), 0) + 'px'; } else { dialog.modalBg.style.left = info.elementClickedRect.left + 'px'; } if (info.elementClickedRect.bottom < 0) { dialog.modalBg.style.top = '0px'; } else if (info.elementClickedRect.bottom + dialogRect.height > window.innerHeight) { dialog.modalBg.style.top = Math.max((info.elementClickedRect.top - dialogRect.height), 0) + 'px'; } else { dialog.modalBg.style.top = info.elementClickedRect.bottom + 'px'; } } const resizeObserver = new ResizeObserver(setPosition); resizeObserver.observe(dialog.dialog); disconnectResizeObserver = () => { resizeObserver.unobserve(dialog.dialog); } if (clipboardFiles.length) { const selectboxWrapperClipboard = gnoh.createElement('div', { class: 'selectbox-wrapper', }); @@ -761,14 +845,14 @@ class: 'selectbox-container', }, selectboxWrapperClipboard); for (const clipboardFile of clipboardFiles) { selectboxContainerClipboard.append(await createSelectbox(sender, clipboardFile, dialog)); } dialog.dialogContent.append(selectboxWrapperClipboard); } if (downloadedFiles.length) { const selectboxWrapperDownloaded = gnoh.createElement('div', { class: 'selectbox-wrapper', }); @@ -781,8 +865,8 @@ class: 'selectbox-container', }, selectboxWrapperDownloaded); for (const downloadedFile of downloadedFiles) { selectboxContainerDownloaded.append(await createSelectbox(sender, downloadedFile, dialog)); } dialog.dialogContent.append(selectboxWrapperDownloaded); @@ -824,15 +908,48 @@ } } const pointerPosition = { x: 0, y: 0, }; window.addEventListener('mousedown', (event) => { pointerPosition.x = event.clientX; pointerPosition.y = event.clientY; }) chrome.runtime.onMessage.addListener(async (info, sender, sendResponse) => { if ( sender.tab.windowId === vivaldiWindowId && info.type === nameKey ) { switch (info.action) { case 'click': const [clipboardFiles, downloadedFiles] = await Promise.all([ readClipboard(info.attributes.accept), getDownloadedFiles(info.attributes.accept), ]); if (clipboardFiles.length || downloadedFiles.length) { const webview = window[sender.tab.id] || document.elementFromPoint(pointerPosition.x, pointerPosition.y); const zoom = parseFloat(gnoh.element.getStyle(webview).getPropertyValue('--uiZoomLevel')); const webviewZoom = await new Promise((resolve) => { webview.getZoom((res) => { resolve(res); }); }); const ratio = webviewZoom / zoom; info.elementClickedRect.left = info.elementClickedRect.left * ratio + pointerPosition.x; info.elementClickedRect.top = info.elementClickedRect.top * ratio + pointerPosition.y; info.elementClickedRect.right = info.elementClickedRect.right * ratio + pointerPosition.x; info.elementClickedRect.bottom = info.elementClickedRect.bottom * ratio + pointerPosition.y; info.elementClickedRect.width = info.elementClickedRect.width * ratio; info.elementClickedRect.height = info.elementClickedRect.height * ratio; info.elementClickedRect.x = info.elementClickedRect.x * ratio + pointerPosition.x; info.elementClickedRect.y = info.elementClickedRect.y * ratio + pointerPosition.y; showDialogChooseFile({ info, sender, @@ -850,30 +967,28 @@ gnoh.timeOut(() => { chrome.tabs.query({ windowId: window.vivaldiWindowId, windowType: 'normal' }, (tabs) => { tabs.forEach((tab) => { chrome.scripting.executeScript({ target: { tabId: tab.id, allFrames: true, }, func: inject, args: [nameKey], }); }); }); chrome.webNavigation.onCommitted.addListener((details) => { if (details.tabId !== -1) { chrome.scripting.executeScript({ target: { tabId: details.tabId, frameIds: [details.frameId], }, func: inject, args: [nameKey], }); } }); }, () => { return window.vivaldiWindowId != null; -
tam710562 revised this gist
Mar 20, 2024 . 1 changed file with 248 additions and 200 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -8,7 +8,7 @@ const gnoh = { file: { verifyAccept({ fileName, mimeType }, accept) { if (!accept) { return true; } @@ -19,8 +19,9 @@ for (const mt of mimeTypes) { if ( mt.startsWith('.') ? new RegExp(mt.replace('.', '.+\\.') + '$').test(fileName) : new RegExp(mt.replace('*', '.+')).test(mimeType) ) { return true; } @@ -30,17 +31,17 @@ }, }, i18n: { getMessageName(message, type) { message = (type ? type + '\x04' : '') + message; return message.replace(/[^a-z0-9]/g, function (i) { return '_' + i.codePointAt(0) + '_'; }) + '0'; }, getMessage(message, type) { return chrome.i18n.getMessage(this.getMessageName(message, type)) || message; }, }, addStyle(css, id, isNotMin) { this.styles = this.styles || {}; if (Array.isArray(css)) { css = css.join(isNotMin === true ? '\n' : ''); @@ -52,7 +53,16 @@ }, document.head); return this.styles[id]; }, array: { chunks(arr, n) { const result = []; for (let i = 0; i < arr.length; i += n) { result.push(arr.slice(i, i + n)); } return result; }, }, createElement(tagName, attribute, parent, inner, options) { if (typeof tagName === 'undefined') { return; } @@ -113,11 +123,42 @@ } return el; }, createElementFromHTML(html) { return this.createElement('template', { html: (html || '').trim(), }).content; }, string: { toColor(str) { let hash = 0; str.split('').forEach(char => { hash = char.charCodeAt(0) + ((hash << 5) - hash); }); let r = (hash >> (0 * 8)) & 0xff; let g = (hash >> (1 * 8)) & 0xff; let b = (hash >> (2 * 8)) & 0xff; return { r, g, b }; }, }, color: { rgbToHex(r, g, b) { return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); }, getLuminance(r, g, b) { return 0.2126 * r + 0.7152 * g + 0.0722 * b; }, isLight(r, g, b) { return gnoh.color.getLuminance(r, g, b) < 156; }, shadeColor(r, g, b, percent) { r = Math.max(Math.min(255, r + percent), 0); g = Math.max(Math.min(255, g + percent), 0); b = Math.max(Math.min(255, b + percent), 0); return { r, g, b }; }, }, get constant() { return { dialogButtons: { @@ -139,11 +180,12 @@ } }; }, dialog(title, content, buttons = [], config) { let modalBg; let dialog; let cancelEvent; const id = this.uuid.generate(); const inner = document.querySelector('#main > .inner, #main > .webpageview'); if (!config) { config = {}; @@ -152,14 +194,20 @@ config.autoClose = true; } function onKeyCloseDialog(windowId, key) { if ( windowId === vivaldiWindowId && key === 'Esc' ) { closeDialog(true); } } function onClickCloseDialog(event) { if ( config.autoClose && !event.target.closest('.dialog-custom[data-dialog-id="' + id + '"]') ) { closeDialog(true); } } @@ -186,7 +234,7 @@ cancelEvent = clickEvent; } button.events = { click(event) { event.preventDefault(); if (typeof clickEvent === 'function') { clickEvent.bind(this)(); @@ -226,6 +274,9 @@ }, dialog, '<h1>' + (title || '') + '</h1>'); const dialogContent = this.createElement('div', { class: 'dialog-content', style: { maxHeight: '65vh', }, }, dialog, content); if (buttons && buttons.length > 0) { const dialogFooter = this.createElement('footer', { @@ -235,11 +286,7 @@ modalBg = this.createElement('div', { id: 'modal-bg', class: 'slide', }, inner, [focusModal.cloneNode(true), div, focusModal.cloneNode(true)]); return { dialog: dialog, dialogHeader: dialogHeader, @@ -248,7 +295,7 @@ close: closeDialog, }; }, timeOut(callback, conditon, timeOut = 300) { let timeOutId = setTimeout(function wait() { let result; if (!conditon) { @@ -278,7 +325,7 @@ }; }, uuid: { generate(ids) { let d = Date.now() + performance.now(); let r; const id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { @@ -307,15 +354,20 @@ const maxAllowedSize = 1024 * 1024 * 5; // 5MB gnoh.addStyle([ `.${nameKey}.dialog-custom .dialog-content { flex-flow: wrap; gap: 18px; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper { overflow: hidden; margin: -2px; padding: 2px; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container { overflow: auto; margin: -2px; padding: 2px; flex: 0 1 auto; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image { background-color: var(--colorBgLighter); width: 120px; height: 120px; display: flex; justify-content: center; align-items: center; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image:hover { box-shadow: 0 0 0 2px var(--colorHighlightBg); }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image.preview img { object-fit: cover; width: 120px; height: 120px; flex: 0 0 auto; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image.icon .file-icon { width: 54px; height: 69px; padding: 15px 0 0; position: relative; font-family: sans-serif; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image.icon .file-icon:before { position: absolute; content: ''; left: 0; top: 0; height: 15px; left: 0; background-color: var(--colorFileIconBg, #007bff); right: 15px; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image.icon .file-icon:after { position: absolute; content: ''; width: 0; height: 0; border-style: solid; border-width: 15.5px 0 0 15.5px; border-color: transparent transparent transparent var(--colorFileIconBgLighter, #66b0ff); top: 0; right: 0; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image.icon .file-icon .file-icon-content { background-color: var(--colorFileIconBg, #007bff); top: 15px; color: var(--colorFileIconFg, #fff); position: absolute; left: 0; bottom: 0; right: 0; padding: 24.75px 0.3em 0; font-size: 19.5px; font-weight: 500; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-title { width: 120px; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-title .filename-container { display: flex; flex-direction: row; overflow: hidden; width: 120px; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-title .filename-container .filename-text { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-title .filename-container .filename-extension { white-space: nowrap; }`, ], nameKey); function inject(nameKey) { @@ -325,7 +377,7 @@ window.easyFiles = true; } const fileData = []; let fileInput = null; @@ -338,7 +390,7 @@ const attributes = {}; for (const attr of fileInput.attributes) { attributes[attr.name] = attr.value; } @@ -364,12 +416,12 @@ if (info.type === nameKey) { switch (info.action) { case 'file': fileData[info.file.fileDataIndex] = info.file.fileData; if (Object.entries(fileData).length === info.file.fileDataLength) { const dataTransfer = new DataTransfer(); dataTransfer.items.add(new File( [new Uint8Array(fileData.flat())], info.file.fileName, { type: info.file.mimeType }, )); @@ -387,92 +439,121 @@ }); } async function simulatePaste() { return new Promise((resolve, reject) => { document.addEventListener('paste', (e) => { e.preventDefault(); const items = []; let isRealFile = true; for (const item of e.clipboardData.items) { const file = item.getAsFile(); const entry = item.webkitGetAsEntry(); if (file) { if (!entry || entry.isFile) { items.push({ file, isFile: true, isRealFile: !!entry, }); } else if (entry.isDirectory) { items.push({ file, isDirectory: true, }); } } } resolve({ items, isRealFile, }); }, { once: true }); document.execCommand('paste'); }); } async function readClipboard(accept) { const clipboardFiles = []; try { const supportedTypes = [ { extension: 'png', mimeType: 'image/png', }, { extension: 'jpeg', mimeType: 'image/jpeg', }, { extension: 'jpg', mimeType: 'image/jpeg', }, ]; const supportedType = supportedTypes.find(s => gnoh.file.verifyAccept({ fileName: 'image.' + s.extension, mimeType: s.mimeType }, accept)); const pasteData = await simulatePaste(); for (const item of pasteData.items) { const file = item.file; let checkType = false; if (item.isFile) { if (item.isRealFile) { checkType = gnoh.file.verifyAccept({ fileName: file.name, mimeType: file.type }, accept); } else { checkType = supportedType && file.type === 'image/png'; } } if (checkType && file.size <= maxAllowedSize) { let blob = new Blob([file], { type: file.type }); if (!item.isRealFile && supportedType.mimeType === 'image/jpeg') { blob = await convertPngToJpeg(blob); } const uint8Array = new Uint8Array(await blob.arrayBuffer()); const fileData = gnoh.array.chunks(uint8Array, chunkSize).map(a => Array.from(a)); const clipboardFile = { fileData: fileData, fileDataLength: fileData.length, mimeType: blob.type, category: 'clipboard', }; if (item.isRealFile) { clipboardFile.fileName = file.name; } else { clipboardFile.extension = supportedType.extension; } switch (clipboardFile.mimeType) { case 'image/jpeg': case 'image/png': case 'image/svg+xml': case 'image/webp': case 'image/gif': case 'image/bmp': clipboardFile.previewUrl = await vivaldi.utilities.storeImage({ data: uint8Array, mimeType: blob.type, }); break; } clipboardFiles.push(clipboardFile); } } } catch (error) { console.error(error); } return clipboardFiles; } async function convertPngToJpeg(blob) { @@ -508,7 +589,6 @@ && downloadedFile.mime !== 'application/x-msdownload' && gnoh.file.verifyAccept({ fileName: downloadedFile.filename, mimeType: downloadedFile.mime }, accept) ) { downloadedFile = (await chrome.downloads.search({ id: downloadedFile.id }))[0]; if ( @@ -519,7 +599,6 @@ && !result[downloadedFile.filename] ) { const file = { mimeType: downloadedFile.mime, path: downloadedFile.filename, fileName: downloadedFile.filename.replace(/^.*[\\/]/, ''), @@ -533,7 +612,7 @@ case 'image/webp': case 'image/gif': case 'image/bmp': file.previewUrl = await vivaldi.utilities.storeImage({ url: file.path, }); break; @@ -547,6 +626,28 @@ return Object.values(result); } function createFileIcon(extension) { const colorBg = gnoh.string.toColor(extension); const isLightBg = gnoh.color.isLight(colorBg.r, colorBg.g, colorBg.b); const colorBgLighter = gnoh.color.shadeColor(colorBg.r, colorBg.g, colorBg.b, isLightBg ? 80 : -80); const fileIcon = gnoh.createElement('div', { class: 'file-icon', style: { '--colorFileIconBg': gnoh.color.rgbToHex(colorBg.r, colorBg.g, colorBg.b), '--colorFileIconBgLighter': gnoh.color.rgbToHex(colorBgLighter.r, colorBgLighter.g, colorBgLighter.b), '--colorFileIconFg': isLightBg ? '#f6f6f6' : '#111111', } }); const fileIconContent = gnoh.createElement('div', { class: 'file-icon-content', text: extension, }, fileIcon); return fileIcon; } async function createSelectbox(sender, file, dialog) { const selectbox = gnoh.createElement('button', { title: file.fileName || '', @@ -560,20 +661,22 @@ case 'downloaded-file': if (!file.fileData) { const fileData = await vivaldi.utilities.readImage(file.path); file.fileData = gnoh.array.chunks(fileData.data, chunkSize); file.fileDataLength = file.fileData.length; } break; case 'clipboard': if (!file.fileName) { const d = new Date(); const year = d.getFullYear(); const month = (d.getMonth() + 1).toString().padStart(2, '0'); const date = d.getDate().toString().padStart(2, '0'); const hour = d.getHours().toString().padStart(2, '0'); const minute = d.getMinutes().toString().padStart(2, '0'); const second = d.getSeconds().toString().padStart(2, '0'); const millisecond = d.getMilliseconds().toString().padStart(3, '0'); file.fileName = `image_${year}-${month}-${date}_${hour}${minute}${second}${millisecond}.${file.extension}`; } break; } @@ -586,24 +689,20 @@ class: 'selectbox-image', }, selectbox); if (file.previewUrl) { selectboxImage.classList.add('preview'); } else { selectboxImage.classList.add('icon'); } if (file.previewUrl) { const image = gnoh.createElement('img', { src: file.previewUrl, }, selectboxImage); } else { const extension = file.extension || file.fileName.split('.').pop(); selectboxImage.append(createFileIcon(extension)); } const selectboxTitle = gnoh.createElement('div', { class: 'selectbox-title', @@ -614,7 +713,7 @@ }, selectboxTitle); if (file.fileName) { const extension = file.extension || file.fileName.split('.').pop(); const name = file.fileName.substring(0, file.fileName.length - extension.length - 1); const filenameContainer = gnoh.createElement('div', { @@ -649,12 +748,9 @@ ); dialog.dialog.style.maxWidth = 570 + 'px'; if (data.clipboardFiles.length) { const selectboxWrapperClipboard = gnoh.createElement('div', { class: 'selectbox-wrapper', }); const h3Clipboard = gnoh.createElement('h3', { @@ -665,8 +761,8 @@ class: 'selectbox-container', }, selectboxWrapperClipboard); for (const clipboardFile of data.clipboardFiles) { selectboxContainerClipboard.append(await createSelectbox(data.sender, clipboardFile, dialog)); } dialog.dialogContent.append(selectboxWrapperClipboard); @@ -705,15 +801,20 @@ } async function chooseFile(sender, file) { if (!file.fileData.length) { file.fileData.push([]); } for (const [index, chunk] of file.fileData.entries()) { await chrome.tabs.sendMessage(sender.tab.id, { type: nameKey, action: 'file', tabId: sender.tab.id, frameId: sender.frameId, file: { fileData: chunk, fileDataIndex: index, fileDataLength: file.fileData.length, fileName: file.fileName, mimeType: file.mimeType, }, @@ -723,72 +824,19 @@ } } chrome.runtime.onMessage.addListener(async (info, sender, sendResponse) => { if (sender.tab.windowId === vivaldiWindowId && info.type === nameKey) { switch (info.action) { case 'click': const clipboardFiles = await readClipboard(info.attributes.accept); const downloadedFiles = await getDownloadedFiles(info.attributes.accept); if (clipboardFiles.length || downloadedFiles.length) { showDialogChooseFile({ info, sender, clipboardFiles, downloadedFiles, }) } else { -
tam710562 created this gist
Mar 6, 2024 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,833 @@ /* * Easy Files * Written by Tam710562 */ (function () { 'use strict'; const gnoh = { file: { verifyAccept: function ({ fileName, mimeType }, accept) { if (!accept) { return true; } const mimeTypes = accept.split(',') .map(x => x.trim()) .filter(x => !!x && (x.startsWith('.') || /\w+\/([-+.\w]+|\*)/.test(x))); for (const mt of mimeTypes) { if ( mt.startsWith('.') && new RegExp(mt.replace('.', '.+\\.') + '$').test(fileName) || new RegExp(mt.replace('*', '.+')).test(mimeType) ) { return true; } } return false; }, }, i18n: { getMessageName: function (message, type) { message = (type ? type + '\x04' : '') + message; return message.replace(/[^a-z0-9]/g, function (i) { return '_' + i.codePointAt(0) + '_'; }) + '0'; }, getMessage: function (message, type) { return chrome.i18n.getMessage(this.getMessageName(message, type)) || message; }, }, addStyle: function (css, id, isNotMin) { this.styles = this.styles || {}; if (Array.isArray(css)) { css = css.join(isNotMin === true ? '\n' : ''); } id = id || this.uuid.generate(Object.keys(this.styles)); this.styles[id] = this.createElement('style', { html: css || '', 'data-id': id, }, document.head); return this.styles[id]; }, createElement: function (tagName, attribute, parent, inner, options) { if (typeof tagName === 'undefined') { return; } if (typeof options === 'undefined') { options = {}; } if (typeof options.isPrepend === 'undefined') { options.isPrepend = false; } const el = document.createElement(tagName); if (!!attribute && typeof attribute === 'object') { for (const key in attribute) { if (key === 'text') { el.textContent = attribute[key]; } else if (key === 'html') { el.innerHTML = attribute[key]; } else if (key === 'style' && typeof attribute[key] === 'object') { for (const css in attribute.style) { el.style.setProperty(css, attribute.style[css]); } } else if (key === 'events' && typeof attribute[key] === 'object') { for (const event in attribute.events) { if (typeof attribute.events[event] === 'function') { el.addEventListener(event, attribute.events[event]); } } } else if (typeof el[key] !== 'undefined') { el[key] = attribute[key]; } else { if (typeof attribute[key] === 'object') { attribute[key] = JSON.stringify(attribute[key]); } el.setAttribute(key, attribute[key]); } } } if (!!inner) { if (!Array.isArray(inner)) { inner = [inner]; } for (let i = 0; i < inner.length; i++) { if (inner[i].nodeName) { el.append(inner[i]); } else { el.append(this.createElementFromHTML(inner[i])); } } } if (typeof parent === 'string') { parent = document.querySelector(parent); } if (!!parent) { if (options.isPrepend) { parent.prepend(el); } else { parent.append(el); } } return el; }, createElementFromHTML: function (html) { return this.createElement('template', { html: (html || '').trim(), }).content; }, get constant() { return { dialogButtons: { submit: { label: this.i18n.getMessage('OK'), type: 'submit' }, cancel: { label: this.i18n.getMessage('Cancel'), cancel: true }, primary: { class: 'primary' }, danger: { class: 'danger' }, default: {}, } }; }, dialog: function (title, content, buttons = [], config) { let modalBg; let dialog; let cancelEvent; const id = this.uuid.generate(); if (!config) { config = {}; } if (typeof config.autoClose === 'undefined') { config.autoClose = true; } function onKeyCloseDialog(key) { if (key === 'Esc') { closeDialog(true); } } function onClickCloseDialog(event) { if (config.autoClose && !event.target.closest('.dialog-custom[data-dialog-id="' + id + '"]')) { closeDialog(true); } } function closeDialog(isCancel) { if (isCancel === true && cancelEvent) { cancelEvent.bind(this)(); } if (modalBg) { modalBg.remove(); } vivaldi.tabsPrivate.onKeyboardShortcut.removeListener(onKeyCloseDialog); document.removeEventListener('mousedown', onClickCloseDialog); } vivaldi.tabsPrivate.onKeyboardShortcut.addListener(onKeyCloseDialog); document.addEventListener('mousedown', onClickCloseDialog); const buttonElements = []; for (let button of buttons) { button.type = button.type || 'button'; const clickEvent = button.click; if (button.cancel === true && typeof clickEvent === 'function') { cancelEvent = clickEvent; } button.events = { click: function (event) { event.preventDefault(); if (typeof clickEvent === 'function') { clickEvent.bind(this)(); } if (button.closeDialog !== false) { closeDialog(); } } }; delete button.click; if (button.label) { button.value = button.label; delete button.label; } buttonElements.push(this.createElement('input', button)); } const focusModal = this.createElement('span', { class: 'focus_modal', tabindex: '0', }); const div = this.createElement('div', { style: { width: config.width ? config.width + 'px' : '', margin: '0 auto', } }); dialog = this.createElement('form', { 'data-dialog-id': id, class: 'dialog-custom modal-wrapper', }, div); if (config.class) { dialog.classList.add(config.class); } const dialogHeader = this.createElement('header', { class: 'dialog-header', }, dialog, '<h1>' + (title || '') + '</h1>'); const dialogContent = this.createElement('div', { class: 'dialog-content', }, dialog, content); if (buttons && buttons.length > 0) { const dialogFooter = this.createElement('footer', { class: 'dialog-footer', }, dialog, buttonElements); } modalBg = this.createElement('div', { id: 'modal-bg', class: 'slide', }, undefined, [focusModal.cloneNode(true), div, focusModal.cloneNode(true)]); const inner = document.querySelector('#main .inner'); if (inner) { inner.prepend(modalBg); } return { dialog: dialog, dialogHeader: dialogHeader, dialogContent: dialogContent, buttons: buttonElements, close: closeDialog, }; }, timeOut: function (callback, conditon, timeOut = 300) { let timeOutId = setTimeout(function wait() { let result; if (!conditon) { result = document.getElementById('browser'); } else if (typeof conditon === 'string') { result = document.querySelector(conditon); } else if (typeof conditon === 'function') { result = conditon(); } else { return; } if (result) { callback(result); } else { timeOutId = setTimeout(wait, timeOut); } }, timeOut); function stop() { if (timeOutId) { clearTimeout(timeOutId); } } return { stop, }; }, uuid: { generate: function (ids) { let d = Date.now() + performance.now(); let r; const id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); if (Array.isArray(ids) && ids.includes(id)) { return this.uuid.generate(ids); } return id; }, }, }; const nameKey = 'easy-files'; const langs = { showAllFiles: gnoh.i18n.getMessage('Show all files...'), downloaded: gnoh.i18n.getMessage('Downloaded'), chooseAFile: gnoh.i18n.getMessage('Choose a File...'), }; const chunkSize = 1024 * 1024 * 10; // 10MB const maxAllowedSize = 1024 * 1024 * 5; // 5MB gnoh.addStyle([ `.${nameKey}.dialog-custom .dialog-content { flex-direction: row; gap: 18px }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper { overflow: hidden; margin: -2px; padding: 2px }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container { overflow: auto; margin: -2px; padding: 2px }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image:hover { box-shadow: 0 0 0 2px var(--colorHighlightBg) }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-image img { width: 120px; height: 120px }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-title { width: 120px }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-title .filename-container { display: flex; flex-direction: row; overflow: hidden; width: 120px }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-title .filename-container .filename-text { white-space: nowrap; text-overflow: ellipsis; overflow: hidden }`, `.${nameKey}.dialog-custom .dialog-content .selectbox-wrapper .selectbox-container .selectbox-title .filename-container .filename-extension { white-space: nowrap }`, ], nameKey); function inject(nameKey) { if (window.easyFiles) { return; } else { window.easyFiles = true; } let fileData = []; let fileInput = null; function handleClick(event) { if (event.target.matches('input[type=file]:not([webkitdirectory])')) { event.preventDefault(); event.stopPropagation(); fileInput = event.target; const attributes = {}; for (attr of fileInput.attributes) { attributes[attr.name] = attr.value; } fileData.length = 0; chrome.runtime.sendMessage({ type: nameKey, action: 'click', attributes, }); } } window.addEventListener('click', handleClick); function changeFile(dataTransfer) { fileInput.files = dataTransfer.files; fileInput.dispatchEvent(new Event('input', { bubbles: true })); fileInput.dispatchEvent(new Event('change', { bubbles: true })); } chrome.runtime.onMessage.addListener((info, sender, sendResponse) => { if (info.type === nameKey) { switch (info.action) { case 'file': fileData = [...fileData, ...info.file.fileData]; if (fileData.length === info.file.fileDataLength) { const dataTransfer = new DataTransfer(); dataTransfer.items.add(new File( [new Uint8Array(fileData)], info.file.fileName, { type: info.file.mimeType }, )); changeFile(dataTransfer); } break; case 'picker': fileInput.showPicker(); break; default: return false; } } }); } function chunks(arr, n) { const result = []; for (let i = 0; i < arr.length; i += n) { result.push(arr.slice(i, i + n)); } return result; } async function readClipboardOnFocus() { if (document.hasFocus()) { return await navigator.clipboard.read(); } else { return new Promise((resolve, reject) => { const activeElement = document.activeElement; const addressField = document.querySelector('input.url.vivaldi-addressfield'); addressField.addEventListener('focus', async () => { try { const value = await navigator.clipboard.read(); resolve(value); } catch (error) { reject(error); } activeElement.focus(); }, { once: true }); addressField.focus(); }); } } async function readClipboard(accept) { const images = []; const supportedTypes = [ { extension: 'png', mimeType: 'image/png', }, { extension: 'jpeg', mimeType: 'image/jpeg', }, { extension: 'jpg', mimeType: 'image/jpeg', }, ] const supportedType = supportedTypes.find(s => gnoh.file.verifyAccept({ fileName: 'image.' + s.extension, mimeType: s.mimeType }, accept)); if (!supportedType) { return images; } try { const clipboardItems = await readClipboardOnFocus(); for (const clipboardItem of clipboardItems) { if (clipboardItem.types.includes('image/png')) { let blob = await clipboardItem.getType('image/png'); if (blob.size <= maxAllowedSize) { if (supportedType.mimeType === 'image/jpeg') { blob = await convertPngToJpeg(blob); } const uint8Array = new Uint8Array(await blob.arrayBuffer()); images.push({ previewDataUrl: await vivaldi.utilities.storeImage({ data: uint8Array, mimeType: supportedType.mimeType, }), fileData: chunks(uint8Array, chunkSize).map(a => Array.from(a)), fileDataLength: uint8Array.length, extension: supportedType.extension, mimeType: supportedType.mimeType, category: 'clipboard', }); } } } return images; } catch (error) { console.error(error); return images; } } async function convertPngToJpeg(blob) { const image = gnoh.createElement('img', { src: URL.createObjectURL(blob), }); await image.decode(); const canvas = gnoh.createElement('canvas', { width: image.width, height: image.height, }); const ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0); return new Promise((resolve) => { canvas.toBlob(blob => { URL.revokeObjectURL(image.src); if (blob) { resolve(blob); } }, 'image/jpeg'); }); } async function getDownloadedFiles(accept) { const downloadedFiles = await chrome.downloads.search({ exists: true, state: 'complete', orderBy: ['-startTime'] }); const result = {}; for (let downloadedFile of downloadedFiles) { if ( downloadedFile.mime && downloadedFile.mime !== 'application/x-msdownload' && gnoh.file.verifyAccept({ fileName: downloadedFile.filename, mimeType: downloadedFile.mime }, accept) ) { const fileIcon = await chrome.downloads.getFileIcon(downloadedFile.id); downloadedFile = (await chrome.downloads.search({ id: downloadedFile.id }))[0]; if ( downloadedFile && downloadedFile.exists === true && downloadedFile.state === 'complete' && downloadedFile.fileSize <= maxAllowedSize && !result[downloadedFile.filename] ) { const file = { previewDataUrl: fileIcon, mimeType: downloadedFile.mime, path: downloadedFile.filename, fileName: downloadedFile.filename.replace(/^.*[\\/]/, ''), category: 'downloaded-file', }; switch (file.mimeType) { case 'image/jpeg': case 'image/png': case 'image/svg+xml': case 'image/webp': case 'image/gif': case 'image/bmp': file.previewDataUrl = await vivaldi.utilities.storeImage({ url: file.path, }); break; } result[downloadedFile.filename] = file; } } } return Object.values(result); } async function createSelectbox(sender, file, dialog) { const selectbox = gnoh.createElement('button', { title: file.fileName || '', class: 'selectbox', events: { click: async (event) => { event.preventDefault(); dialog.close(); switch (file.category) { case 'downloaded-file': if (!file.fileData) { const fileData = await vivaldi.utilities.readImage(file.path); file.fileData = chunks(fileData.data, chunkSize); file.fileDataLength = fileData.data.length; } break; case 'clipboard': const d = new Date(); const year = d.getFullYear(); const month = (d.getMonth() + 1).toString().padStart(2, '0'); const date = d.getDate().toString().padStart(2, '0'); const hour = d.getHours().toString().padStart(2, '0'); const minute = d.getMinutes().toString().padStart(2, '0'); const second = d.getSeconds().toString().padStart(2, '0'); const millisecond = d.getMilliseconds().toString().padStart(3, '0'); file.fileName = `image_${year}-${month}-${date}_${hour}${minute}${second}${millisecond}.${file.extension}`; break; } chooseFile(sender, file); }, }, }); const selectboxImage = gnoh.createElement('div', { class: 'selectbox-image', }, selectbox); const image = gnoh.createElement('img', null, selectboxImage); switch (file.mimeType) { case 'image/jpeg': case 'image/png': case 'image/svg+xml': case 'image/webp': case 'image/gif': case 'image/bmp': image.style.setProperty('object-fit', 'cover'); break; default: image.style.setProperty('object-fit', 'none'); break; } image.src = file.previewDataUrl; const selectboxTitle = gnoh.createElement('div', { class: 'selectbox-title', }, selectbox); const filenameText = gnoh.createElement('div', { class: 'filename-container', }, selectboxTitle); if (file.fileName) { const extension = file.fileName.split('.').pop(); const name = file.fileName.substring(0, file.fileName.length - extension.length - 1); const filenameContainer = gnoh.createElement('div', { class: 'filename-text', text: name, }, filenameText); const filenameExtension = gnoh.createElement('div', { class: 'filename-extension', text: '.' + extension, }, filenameText); } return selectbox; } async function showDialogChooseFile(data) { const buttonShowAllFilesElement = Object.assign({}, gnoh.constant.dialogButtons.submit, { label: langs.showAllFiles, click: () => showAllFiles(data.sender), }); const buttonCancelElement = Object.assign({}, gnoh.constant.dialogButtons.cancel); const dialog = gnoh.dialog( langs.chooseAFile, null, [buttonShowAllFilesElement, buttonCancelElement], { class: nameKey, } ); dialog.dialog.style.maxWidth = 570 + 'px'; if (data.images.length) { const selectboxWrapperClipboard = gnoh.createElement('div', { class: 'selectbox-wrapper', style: { 'flex': '0 0 auto', }, }); const h3Clipboard = gnoh.createElement('h3', { text: 'Clipboard', }, selectboxWrapperClipboard); const selectboxContainerClipboard = gnoh.createElement('div', { class: 'selectbox-container', }, selectboxWrapperClipboard); for (const image of data.images) { selectboxContainerClipboard.append(await createSelectbox(data.sender, image, dialog)); } dialog.dialogContent.append(selectboxWrapperClipboard); } if (data.downloadedFiles.length) { const selectboxWrapperDownloaded = gnoh.createElement('div', { class: 'selectbox-wrapper', }); const h3Downloaded = gnoh.createElement('h3', { text: 'Downloaded', }, selectboxWrapperDownloaded); const selectboxContainerDownloaded = gnoh.createElement('div', { class: 'selectbox-container', }, selectboxWrapperDownloaded); for (const downloadedFile of data.downloadedFiles) { selectboxContainerDownloaded.append(await createSelectbox(data.sender, downloadedFile, dialog)); } dialog.dialogContent.append(selectboxWrapperDownloaded); } } function showAllFiles(sender) { chrome.tabs.sendMessage(sender.tab.id, { type: nameKey, action: 'picker', tabId: sender.tab.id, frameId: sender.frameId, }, { frameId: sender.frameId, }); } async function chooseFile(sender, file) { for (const chunk of file.fileData) { await chrome.tabs.sendMessage(sender.tab.id, { type: nameKey, action: 'file', tabId: sender.tab.id, frameId: sender.frameId, file: { fileData: chunk, fileDataLength: file.fileDataLength, fileName: file.fileName, mimeType: file.mimeType, }, }, { frameId: sender.frameId, }); } } async function requestPermissions(sender) { const result = await navigator.permissions.query({ name: 'clipboard-read' }); switch (result.state) { case 'prompt': try { await readClipboardOnFocus(); return true; } catch (error) { const tab = await chrome.tabs.create({ url: 'chrome-extension://mpognobbkildjkofajifpdfhcoklimli/window.html', active: true }); return new Promise((resolve) => { chrome.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) { if (tabId === tab.id && changeInfo.status === 'complete') { chrome.tabs.onUpdated.removeListener(onUpdated); chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async () => { try { await navigator.clipboard.read(); return true; } catch (error) { console.error(error); return false; } }, }, (results) => { chrome.tabs.remove(tab.id); chrome.tabs.update(sender.tab.id, { active: true }); if (results.length) { resolve(results[0].result); } else { resolve(false); } }); } }); }); } case 'granted': return true; default: return false; } } chrome.runtime.onMessage.addListener(async (info, sender, sendResponse) => { if (info.type === nameKey) { switch (info.action) { case 'click': let images = []; if (await requestPermissions(sender)) { images = await readClipboard(info.attributes.accept); } const downloadedFiles = await getDownloadedFiles(info.attributes.accept); if (images.length || downloadedFiles.length) { showDialogChooseFile({ info, sender, images, downloadedFiles, }) } else { showAllFiles(sender); } break; } } }); gnoh.timeOut(() => { chrome.tabs.query({ windowId: window.vivaldiWindowId, windowType: 'normal' }, (tabs) => { tabs.forEach((tab) => { chrome.webNavigation.getAllFrames({ tabId: tab.id }, (details) => { details.forEach((detail) => { chrome.scripting.executeScript({ target: { tabId: tab.id, frameIds: [detail.frameId] }, func: inject, args: [nameKey], }); }); }); }); }); chrome.webNavigation.onCommitted.addListener((details) => { chrome.scripting.executeScript({ target: { tabId: details.tabId, frameIds: [details.frameId] }, func: inject, args: [nameKey], }); }); }, () => { return window.vivaldiWindowId != null; }); })();