Skip to content

Instantly share code, notes, and snippets.

@tam710562
Last active March 8, 2026 04:31
Show Gist options
  • Select an option

  • Save tam710562/67ce3c4387d72e94a83cf0ae9f890cec to your computer and use it in GitHub Desktop.

Select an option

Save tam710562/67ce3c4387d72e94a83cf0ae9f890cec to your computer and use it in GitHub Desktop.

Revisions

  1. tam710562 revised this gist Dec 24, 2024. 1 changed file with 25 additions and 4 deletions.
    29 changes: 25 additions & 4 deletions easy-files.js
    Original 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 ? 80 : -80);
    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 = Object.assign({}, gnoh.constant.dialogButtons.submit, {
    const buttonShowAllFilesElement = gnoh.object.merge(gnoh.constant.dialogButtons.submit, {
    label: langs.showMore,
    click() {
    showAllFiles(sender);
    disconnectResizeObserver && disconnectResizeObserver();
    },
    });

    const buttonCancelElement = Object.assign({}, gnoh.constant.dialogButtons.cancel, {
    const buttonCancelElement = gnoh.object.merge(gnoh.constant.dialogButtons.cancel, {
    click() {
    disconnectResizeObserver && disconnectResizeObserver();
    },
    @@ -1056,4 +1077,4 @@
    }
    });
    }, () => window.vivaldiWindowId != null);
    })();
    })();
  2. tam710562 revised this gist Dec 24, 2024. 1 changed file with 16 additions and 8 deletions.
    24 changes: 16 additions & 8 deletions easy-files.js
    Original 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) {
    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 };
    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;
    }
    buttonElements.push(this.createElement('input', button));
    button.element = this.createElement('input', button);
    buttonElements.push(button.element);
    }

    const focusModal = this.createElement('span', {
    @@ -484,8 +492,8 @@
    pointerPosition.y = event.clientY;
    }

    window.addEventListener('click', handleClick);
    window.addEventListener('mousedown', handleMouseDown);
    document.addEventListener('click', handleClick);
    document.addEventListener('mousedown', handleMouseDown);

    function changeFile(dataTransfer) {
    fileInput.files = dataTransfer.files;
    @@ -1048,4 +1056,4 @@
    }
    });
    }, () => window.vivaldiWindowId != null);
    })();
    })();
  3. tam710562 revised this gist Nov 9, 2024. 1 changed file with 115 additions and 60 deletions.
    175 changes: 115 additions & 60 deletions easy-files.js
    Original 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 window.getComputedStyle(element);
    return getComputedStyle(element);
    },
    },
    createElement(tagName, attribute, parent, inner, options) {
    @@ -207,10 +218,12 @@
    }
    }

    function onClickCloseDialog(event) {
    function onClickCloseDialog(windowId, mousedown, button, clientX, clientY) {
    if (
    config.autoClose
    && !event.target.closest('.dialog-custom[data-dialog-id="' + id + '"]')
    && 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);
    document.removeEventListener('mousedown', onClickCloseDialog);
    vivaldi.tabsPrivate.onWebviewClickCheck.addListener(onClickCloseDialog);
    }

    vivaldi.tabsPrivate.onKeyboardShortcut.addListener(onKeyCloseDialog);
    document.addEventListener('mousedown', onClickCloseDialog);
    vivaldi.tabsPrivate.onWebviewClickCheck.addListener(onClickCloseDialog);

    const buttonElements = [];
    for (let button of buttons) {
    @@ -300,15 +313,15 @@
    close: closeDialog,
    };
    },
    timeOut(callback, conditon, timeOut = 300) {
    timeOut(callback, condition, timeOut = 300) {
    let timeOutId = setTimeout(function wait() {
    let result;
    if (!conditon) {
    if (!condition) {
    result = document.getElementById('browser');
    } else if (typeof conditon === 'string') {
    result = document.querySelector(conditon);
    } else if (typeof conditon === 'function') {
    result = conditon();
    } 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 = {
    showAllFiles: gnoh.i18n.getMessage('Show all files...'),
    downloaded: gnoh.i18n.getMessage('Downloaded'),
    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 = fileInput.getBoundingClientRect();
    elementClickedRect = getRect(fileInput);
    }

    const attributes = {};
    @@ -435,7 +478,7 @@
    }

    function handleMouseDown(event) {
    elementClickedRect = event.target.getBoundingClientRect();
    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((info, sender, sendResponse) => {
    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(
    [new Uint8Array(fileData.flat())],
    [decompressedArrayBuffer],
    info.file.fileName,
    { type: info.file.mimeType },
    ));
    @@ -545,19 +591,23 @@
    }
    }

    if (checkType && file.size <= maxAllowedSize) {
    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 uint8Array = new Uint8Array(await blob.arrayBuffer());
    const fileData = gnoh.array.chunks(uint8Array, chunkSize).map(a => Array.from(a));
    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: uint8Array,
    data: arrayBuffer,
    mimeType: blob.type,
    });
    break;
    @@ -630,13 +680,14 @@
    downloadedFile
    && downloadedFile.exists === true
    && downloadedFile.state === 'complete'
    && downloadedFile.fileSize <= maxAllowedSize
    && (!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 || '',
    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 uint8Array = new Uint8Array(await vivaldi.mailPrivate.readFileToBuffer(file.path));
    file.fileData = gnoh.array.chunks(uint8Array, chunkSize).map(a => Array.from(a));
    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.showAllFiles,
    label: langs.showMore,
    click() {
    showAllFiles(sender);
    disconnectResizeObserver && disconnectResizeObserver();
    @@ -806,39 +860,41 @@
    dialog.modalBg.style.right = 'unset';
    dialog.modalBg.style.bottom = 'unset';

    function setPosition() {
    const dialogRect = dialog.dialog.getBoundingClientRect();
    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.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.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';
    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);
    }
    disconnectResizeObserver = () => resizeObserver.unobserve(dialog.dialog);

    if (clipboardFiles.length) {
    const selectboxWrapperClipboard = gnoh.createElement('div', {
    class: 'selectbox-wrapper',
    });

    const h3Clipboard = gnoh.createElement('h3', {
    text: 'Clipboard',
    text: langs.clipboard,
    }, selectboxWrapperClipboard);

    const selectboxContainerClipboard = gnoh.createElement('div', {
    @@ -858,7 +914,7 @@
    });

    const h3Downloaded = gnoh.createElement('h3', {
    text: 'Downloaded',
    text: langs.downloads,
    }, selectboxWrapperDownloaded);

    const selectboxContainerDownloaded = gnoh.createElement('div', {
    @@ -884,13 +940,13 @@
    });
    }

    async function chooseFile(sender, file) {
    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, {
    chrome.tabs.sendMessage(sender.tab.id, {
    type: nameKey,
    action: 'file',
    tabId: sender.tab.id,
    @@ -908,15 +964,16 @@
    }
    }

    const pointerPosition = {
    x: 0,
    y: 0,
    };

    window.addEventListener('mousedown', (event) => {
    pointerPosition.x = event.clientX;
    pointerPosition.y = event.clientY;
    })
    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 @@
    });
    }
    });
    }, () => {
    return window.vivaldiWindowId != null;
    });
    }, () => window.vivaldiWindowId != null);
    })();
  4. tam710562 revised this gist Sep 22, 2024. 1 changed file with 183 additions and 68 deletions.
    251 changes: 183 additions & 68 deletions easy-files.js
    Original file line number Diff line number Diff line change
    @@ -3,11 +3,14 @@
    * Written by Tam710562
    */

    (function () {
    (() => {
    '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, function (i) {
    return '_' + i.codePointAt(0) + '_';
    }) + '0';
    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: {
    toColor(str) {
    toHashCode(str) {
    let hash = 0;
    str.split('').forEach(char => {
    hash = char.charCodeAt(0) + ((hash << 5) - hash);
    });
    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 gnoh.color.getLuminance(r, g, b) < 156;
    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
    },
    primary: {
    class: 'primary'
    },
    danger: {
    class: 'danger'
    },
    default: {},
    }
    },
    };
    },
    dialog(title, content, buttons = [], config) {
    @@ -288,9 +292,10 @@
    class: 'slide',
    }, inner, [focusModal.cloneNode(true), div, focusModal.cloneNode(true)]);
    return {
    dialog: dialog,
    dialogHeader: dialogHeader,
    dialogContent: dialogContent,
    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, function (c) {
    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.uuid.generate(ids);
    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;
    default:
    return false;
    }
    }
    });
    @@ -473,7 +509,6 @@

    document.execCommand('paste');
    });

    }

    async function readClipboard(accept) {
    @@ -627,7 +662,10 @@
    }

    function createFileIcon(extension) {
    const colorBg = gnoh.string.toColor(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: {
    click: async (event) => {
    async click(event) {
    event.preventDefault();
    dialog.close();

    switch (file.category) {
    case 'downloaded-file':
    if (!file.fileData) {
    const fileData = await vivaldi.utilities.readImage(file.path);
    file.fileData = gnoh.array.chunks(fileData.data, chunkSize);
    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 || file.fileName.split('.').pop();
    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 || file.fileName.split('.').pop();
    const name = file.fileName.substring(0, file.fileName.length - extension.length - 1);
    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);

    const filenameExtension = gnoh.createElement('div', {
    class: 'filename-extension',
    text: '.' + extension,
    }, filenameText);
    if (extension) {
    const filenameExtension = gnoh.createElement('div', {
    class: 'filename-extension',
    text: '.' + extension,
    }, filenameText);
    }
    }

    return selectbox;
    }

    async function showDialogChooseFile(data) {
    async function showDialogChooseFile({ info, sender, clipboardFiles, downloadedFiles }) {
    let disconnectResizeObserver;

    const buttonShowAllFilesElement = Object.assign({}, gnoh.constant.dialogButtons.submit, {
    label: langs.showAllFiles,
    click: () => showAllFiles(data.sender),
    click() {
    showAllFiles(sender);
    disconnectResizeObserver && disconnectResizeObserver();
    },
    });

    const buttonCancelElement = Object.assign({}, gnoh.constant.dialogButtons.cancel);
    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';

    if (data.clipboardFiles.length) {
    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 data.clipboardFiles) {
    selectboxContainerClipboard.append(await createSelectbox(data.sender, clipboardFile, dialog));
    for (const clipboardFile of clipboardFiles) {
    selectboxContainerClipboard.append(await createSelectbox(sender, clipboardFile, dialog));
    }

    dialog.dialogContent.append(selectboxWrapperClipboard);
    }

    if (data.downloadedFiles.length) {
    if (downloadedFiles.length) {
    const selectboxWrapperDownloaded = gnoh.createElement('div', {
    class: 'selectbox-wrapper',
    });
    @@ -781,8 +865,8 @@
    class: 'selectbox-container',
    }, selectboxWrapperDownloaded);

    for (const downloadedFile of data.downloadedFiles) {
    selectboxContainerDownloaded.append(await createSelectbox(data.sender, downloadedFile, dialog));
    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) {
    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);
    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.webNavigation.getAllFrames({ tabId: tab.id }, (details) => {
    details.forEach((detail) => {
    chrome.scripting.executeScript({
    target: {
    tabId: tab.id,
    frameIds: [detail.frameId]
    },
    func: inject,
    args: [nameKey],
    });
    });
    chrome.scripting.executeScript({
    target: {
    tabId: tab.id,
    allFrames: true,
    },
    func: inject,
    args: [nameKey],
    });
    });
    });

    chrome.webNavigation.onCommitted.addListener((details) => {
    chrome.scripting.executeScript({
    target: {
    tabId: details.tabId,
    frameIds: [details.frameId]
    },
    func: inject,
    args: [nameKey],
    });
    if (details.tabId !== -1) {
    chrome.scripting.executeScript({
    target: {
    tabId: details.tabId,
    frameIds: [details.frameId],
    },
    func: inject,
    args: [nameKey],
    });
    }
    });
    }, () => {
    return window.vivaldiWindowId != null;
  5. tam710562 revised this gist Mar 20, 2024. 1 changed file with 248 additions and 200 deletions.
    448 changes: 248 additions & 200 deletions easy-files.js
    Original file line number Diff line number Diff line change
    @@ -8,7 +8,7 @@

    const gnoh = {
    file: {
    verifyAccept: function ({ fileName, mimeType }, accept) {
    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)
    mt.startsWith('.')
    ? new RegExp(mt.replace('.', '.+\\.') + '$').test(fileName)
    : new RegExp(mt.replace('*', '.+')).test(mimeType)
    ) {
    return true;
    }
    @@ -30,17 +31,17 @@
    },
    },
    i18n: {
    getMessageName: function (message, type) {
    getMessageName(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) {
    getMessage(message, type) {
    return chrome.i18n.getMessage(this.getMessageName(message, type)) || message;
    },
    },
    addStyle: function (css, id, isNotMin) {
    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];
    },
    createElement: function (tagName, attribute, parent, inner, options) {
    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: function (html) {
    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: function (title, content, buttons = [], config) {
    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(key) {
    if (key === 'Esc') {
    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 + '"]')) {
    if (
    config.autoClose
    && !event.target.closest('.dialog-custom[data-dialog-id="' + id + '"]')
    ) {
    closeDialog(true);
    }
    }
    @@ -186,7 +234,7 @@
    cancelEvent = clickEvent;
    }
    button.events = {
    click: function (event) {
    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',
    }, undefined, [focusModal.cloneNode(true), div, focusModal.cloneNode(true)]);
    const inner = document.querySelector('#main .inner');
    if (inner) {
    inner.prepend(modalBg);
    }
    }, inner, [focusModal.cloneNode(true), div, focusModal.cloneNode(true)]);
    return {
    dialog: dialog,
    dialogHeader: dialogHeader,
    @@ -248,7 +295,7 @@
    close: closeDialog,
    };
    },
    timeOut: function (callback, conditon, timeOut = 300) {
    timeOut(callback, conditon, timeOut = 300) {
    let timeOutId = setTimeout(function wait() {
    let result;
    if (!conditon) {
    @@ -278,7 +325,7 @@
    };
    },
    uuid: {
    generate: function (ids) {
    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-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}.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;
    }

    let fileData = [];
    const fileData = [];

    let fileInput = null;

    @@ -338,7 +390,7 @@

    const attributes = {};

    for (attr of fileInput.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 = [...fileData, ...info.file.fileData];
    fileData[info.file.fileDataIndex] = info.file.fileData;

    if (fileData.length === info.file.fileDataLength) {
    if (Object.entries(fileData).length === info.file.fileDataLength) {
    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(new File(
    [new Uint8Array(fileData)],
    [new Uint8Array(fileData.flat())],
    info.file.fileName,
    { type: info.file.mimeType },
    ));
    @@ -387,92 +439,121 @@
    });
    }

    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);
    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,
    });
    }
    }
    activeElement.focus();
    }

    resolve({
    items,
    isRealFile,
    });
    }, { once: true });

    document.execCommand('paste');
    });

    }, { 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 clipboardFiles = [];

    const supportedType = supportedTypes.find(s => gnoh.file.verifyAccept({ fileName: 'image.' + s.extension, mimeType: s.mimeType }, accept));
    try {
    const supportedTypes = [
    {
    extension: 'png',
    mimeType: 'image/png',
    },
    {
    extension: 'jpeg',
    mimeType: 'image/jpeg',
    },
    {
    extension: 'jpg',
    mimeType: 'image/jpeg',
    },
    ];

    if (!supportedType) {
    return images;
    }
    const supportedType = supportedTypes.find(s => gnoh.file.verifyAccept({ fileName: 'image.' + s.extension, mimeType: s.mimeType }, accept));

    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 pasteData = await simulatePaste();

    const uint8Array = new Uint8Array(await blob.arrayBuffer());
    images.push({
    previewDataUrl: await vivaldi.utilities.storeImage({
    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: supportedType.mimeType,
    }),
    fileData: chunks(uint8Array, chunkSize).map(a => Array.from(a)),
    fileDataLength: uint8Array.length,
    extension: supportedType.extension,
    mimeType: supportedType.mimeType,
    category: 'clipboard',
    });
    mimeType: blob.type,
    });
    break;
    }

    clipboardFiles.push(clipboardFile);
    }
    }

    return images;
    } catch (error) {
    console.error(error);

    return images;
    }

    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)
    ) {
    const fileIcon = await chrome.downloads.getFileIcon(downloadedFile.id);
    downloadedFile = (await chrome.downloads.search({ id: downloadedFile.id }))[0];

    if (
    @@ -519,7 +599,6 @@
    && !result[downloadedFile.filename]
    ) {
    const file = {
    previewDataUrl: fileIcon,
    mimeType: downloadedFile.mime,
    path: downloadedFile.filename,
    fileName: downloadedFile.filename.replace(/^.*[\\/]/, ''),
    @@ -533,7 +612,7 @@
    case 'image/webp':
    case 'image/gif':
    case 'image/bmp':
    file.previewDataUrl = await vivaldi.utilities.storeImage({
    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 = chunks(fileData.data, chunkSize);
    file.fileDataLength = fileData.data.length;
    file.fileData = gnoh.array.chunks(fileData.data, chunkSize);
    file.fileDataLength = file.fileData.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}`;
    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);

    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;
    if (file.previewUrl) {
    selectboxImage.classList.add('preview');
    } else {
    selectboxImage.classList.add('icon');
    }

    image.src = file.previewDataUrl;
    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.fileName.split('.').pop();
    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.images.length) {
    if (data.clipboardFiles.length) {
    const selectboxWrapperClipboard = gnoh.createElement('div', {
    class: 'selectbox-wrapper',
    style: {
    'flex': '0 0 auto',
    },
    });

    const h3Clipboard = gnoh.createElement('h3', {
    @@ -665,8 +761,8 @@
    class: 'selectbox-container',
    }, selectboxWrapperClipboard);

    for (const image of data.images) {
    selectboxContainerClipboard.append(await createSelectbox(data.sender, image, dialog));
    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) {
    for (const chunk of file.fileData) {
    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,
    fileDataLength: file.fileDataLength,
    fileDataIndex: index,
    fileDataLength: file.fileData.length,
    fileName: file.fileName,
    mimeType: file.mimeType,
    },
    @@ -723,72 +824,19 @@
    }
    }

    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) {
    if (sender.tab.windowId === vivaldiWindowId && info.type === nameKey) {
    switch (info.action) {
    case 'click':
    let images = [];
    if (await requestPermissions(sender)) {
    images = await readClipboard(info.attributes.accept);
    }
    const clipboardFiles = await readClipboard(info.attributes.accept);

    const downloadedFiles = await getDownloadedFiles(info.attributes.accept);

    if (images.length || downloadedFiles.length) {
    if (clipboardFiles.length || downloadedFiles.length) {
    showDialogChooseFile({
    info,
    sender,
    images,
    clipboardFiles,
    downloadedFiles,
    })
    } else {
  6. tam710562 created this gist Mar 6, 2024.
    833 changes: 833 additions & 0 deletions easy-files.js
    Original 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;
    });
    })();