├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.MD ├── _locales ├── az │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fr │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── nl_NL │ └── messages.json ├── pl │ └── messages.json ├── pt_PT │ └── messages.json ├── pt_br │ └── messages.json ├── ru │ └── messages.json ├── sv │ └── messages.json ├── tr │ └── messages.json ├── uk │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── build-extension.sh ├── contributors.md ├── eslint.config.mjs ├── faq.md ├── known-issues.md ├── lib ├── chrome-browser-polyfill.js ├── single-file-background.js ├── single-file-bootstrap.js ├── single-file-extension-background.js ├── single-file-extension-bootstrap.js ├── single-file-extension-core.js ├── single-file-extension-editor-helper.js ├── single-file-extension-editor-init.js ├── single-file-extension-editor.js ├── single-file-extension-frames.js ├── single-file-extension.js ├── single-file-frames.js ├── single-file-hooks-frames.js ├── single-file-infobar.js ├── single-file-z-worker.js ├── single-file-zip.js ├── single-file-zip.min.js ├── single-file.js └── web-stream.js ├── manifest.json ├── package-lock.json ├── package.json ├── privacy.md ├── rollup.config.dev.js ├── rollup.config.js └── src ├── core ├── bg │ ├── autosave-util.js │ ├── autosave.js │ ├── background.html │ ├── bookmarks.js │ ├── bootstrap.js │ ├── business.js │ ├── companion.js │ ├── config.js │ ├── devtools.js │ ├── download-util.js │ ├── downloads.js │ ├── editor.js │ ├── external-messages.js │ ├── index.js │ ├── requests.js │ ├── tabs-data.js │ ├── tabs-util.js │ └── tabs.js ├── common │ └── download.js ├── content │ ├── content-bootstrap.js │ ├── content-frames.js │ └── content.js └── devtools │ ├── devtools.html │ └── devtools.js ├── index.js ├── lib ├── dropbox │ └── dropbox.js ├── gdrive │ └── gdrive.js ├── github │ └── github.js ├── readability │ ├── Readability-readerable.js │ └── Readability.js ├── rest-form-api │ └── index.js ├── s3 │ └── s3.js ├── single-file │ ├── background.js │ ├── browser-polyfill │ │ └── chrome-browser-polyfill.js │ ├── core │ │ ├── bg │ │ │ └── scripts.js │ │ └── content │ │ │ ├── content-hooks-frames-extension-injection.js │ │ │ └── content-hooks-frames-inline-injection.js │ ├── fetch │ │ ├── bg │ │ │ └── fetch.js │ │ └── content │ │ │ └── content-fetch.js │ ├── frame-tree │ │ └── bg │ │ │ └── frame-tree.js │ └── lazy │ │ └── bg │ │ └── lazy-timeout.js ├── web-stream │ └── index.js ├── webdav │ └── webdav.js ├── woleet │ └── woleet.js └── yabson │ └── yabson.js └── ui ├── bg ├── index.js ├── ui-batch-save-urls.js ├── ui-button.js ├── ui-commands.js ├── ui-editor.js ├── ui-help.js ├── ui-menus.js ├── ui-options-editor.js ├── ui-options.js ├── ui-panel.js ├── ui-pendings.js └── ui-viewer.js ├── common └── common-content-ui.js ├── content ├── content-ui-editor-init-web.js ├── content-ui-editor-web.js └── content-ui.js ├── pages ├── batch-save-urls.css ├── batch-save-urls.html ├── editor-frame-web.css ├── editor-mask-web.css ├── editor-note-web.css ├── editor.css ├── editor.html ├── help.css ├── help.html ├── help_zh_CN.html ├── options-editor.html ├── options.css ├── options.html ├── panel.css ├── panel.html ├── pendings.css ├── pendings.html └── viewer.html └── resources ├── button_cancel.png ├── button_cut_inner.png ├── button_cut_outer.png ├── button_delete.png ├── button_delete_all.png ├── button_download.png ├── button_edit.png ├── button_highlighter_blue.png ├── button_highlighter_delete.png ├── button_highlighter_green.png ├── button_highlighter_hidden.png ├── button_highlighter_pink.png ├── button_highlighter_visible.png ├── button_highlighter_yellow.png ├── button_new.png ├── button_note_blue.png ├── button_note_edit.png ├── button_note_format.png ├── button_note_green.png ├── button_note_hidden.png ├── button_note_pink.png ├── button_note_visible.png ├── button_note_yellow.png ├── button_ok.png ├── button_print.png ├── button_redo_cut.png ├── button_undo_all_cut.png ├── button_undo_cut.png ├── icon_128.png ├── icon_128_wait0.png ├── icon_128_wait1.png ├── icon_128_wait2.png ├── icon_128_wait3.png ├── icon_128_wait4.png ├── icon_128_wait5.png ├── icon_128_wait6.png ├── icon_128_wait7.png ├── icon_128_wait8.png ├── icon_16.png ├── icon_32.png ├── icon_48.png └── icon_64.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.me/glormeau 2 | liberapay: gildas 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 15 | 16 | **Describe the bug** 17 | 18 | 19 | **To Reproduce** 20 | Steps to reproduce the behavior: 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | **Expected behavior** 27 | 28 | 29 | **Screenshots** 30 | 31 | 32 | **Environment** 33 | - OS: [e.g. Win10 Pro, Ubuntu 18.04.1, Android 7.1.2] 34 | - Browser: [e.g. Chrome, Firefox] 35 | - Version: [e.g. 64] 36 | 37 | **Additional context** 38 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: 'feature request' 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | 9 | 10 | 11 | **Describe the solution you'd like** 12 | 13 | 14 | 15 | **Describe alternatives you've considered (optional)** 16 | 17 | 18 | 19 | **Additional context (optional)** 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | **/node_modules/ 3 | .idea/ -------------------------------------------------------------------------------- /build-extension.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dpkg -s zip &> /dev/null 4 | if [ $? -ne 0 ] 5 | then 6 | if ! command -v zip &> /dev/null; then 7 | echo "Installing zip" 8 | sudo apt install zip 9 | fi 10 | fi 11 | 12 | dpkg -s jq &> /dev/null 13 | if [ $? -ne 0 ] 14 | then 15 | if ! command -v jq &> /dev/null; then 16 | echo "Installing jq" 17 | sudo apt install jq 18 | fi 19 | fi 20 | 21 | npm install 22 | npm update 23 | 24 | npx rollup -c rollup.config.js 25 | 26 | cp package.json package.copy.json 27 | jq 'del(.dependencies."single-file-cli")' package.copy.json > package.json 28 | zip -r singlefile-extension-source.zip manifest.json package.json _locales src rollup*.js .eslintrc.js build-extension.sh 29 | mv package.copy.json package.json 30 | 31 | rm singlefile-extension-firefox.zip 32 | 33 | cp src/core/bg/config.js config.copy.js 34 | cp src/core/bg/companion.js companion.copy.js 35 | sed -i "" 's/forceWebAuthFlow: false/forceWebAuthFlow: true/g' src/core/bg/config.js 36 | sed -i "" 's/enabled: true/enabled: false/g' src/core/bg/companion.js 37 | zip -r singlefile-extension-firefox.zip manifest.json lib _locales src 38 | mv config.copy.js src/core/bg/config.js 39 | mv companion.copy.js src/core/bg/companion.js 40 | -------------------------------------------------------------------------------- /contributors.md: -------------------------------------------------------------------------------- 1 | # SingleFile 2 | 3 | ## Contributors 4 | 5 | - Azerbaijani translation done by Haciagha_Sadikhov (https://github.com/Hajiagha-Sadikhov) 6 | - Chinese translation done by yfdyh000 (https://github.com/yfdyh000), Liu8Can 7 | (https://github.com/Liu8Can), KrasnayaPloshchad 8 | (https://github.com/KrasnayaPloshchad), frostblazergit 9 | (https://github.com/frostblazergit), dnknn (https://github.com/dnknn), 10 | lqzhgood (https://github.com/lqzhgood) 11 | - Traditional Chinese translation done by frostblazergit 12 | (https://github.com/frostblazergit), lqzhgood (https://github.com/lqzhgood) 13 | - Dutch translation done by jooleer (https://github.com/jooleer) 14 | - German translation done by womotroll (https://github.com/womotroll), bannmann 15 | (https://github.com/bannmann) 16 | - Italian translation done by Fastbyte01 (https://github.com/Fastbyte01) 17 | - Japanese translation done by Shitennouji(四天王寺) 18 | (https://github.com/Shitennouji) 19 | - Polish translation done by xesarni (https://github.com/xesarni) 20 | - Portuguese translation done by Blackspirits (https://github.com/Blackspirits) 21 | - Portuguese-Brazilian translation done by @mezysinc, Blackspirits 22 | (https://github.com/Blackspirits) 23 | - Russian translation done by rstp14, kramola-RU 24 | (https://github.com/kramola-RU), solokot (https://github.com/solokot), 25 | TotalCaesar659 (https://github.com/TotalCaesar659) 26 | - Spanish translation done by strel (https://github.com/strel) 27 | - Swedish translation done by NickWick13 (https://github.com/NickWick13) 28 | - Turkish translation done by hbaklan943 (https://github.com/hbaklan943) 29 | - Ukrainian translation done by perdolka (https://github.com/perdolka) 30 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | 3 | export default [ 4 | js.configs.recommended, 5 | { 6 | languageOptions: { 7 | ecmaVersion: 2025, 8 | sourceType: "module", 9 | globals: { 10 | console: "readonly", 11 | } 12 | }, 13 | rules: { 14 | "linebreak-style": [ 15 | "error", 16 | "unix" 17 | ], 18 | "quotes": [ 19 | "error", 20 | "double" 21 | ], 22 | "semi": [ 23 | "error", 24 | "always" 25 | ], 26 | "no-console": [ 27 | "warn" 28 | ] 29 | } 30 | } 31 | ]; 32 | -------------------------------------------------------------------------------- /faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## What does SingleFile do? 4 | SingleFile is a browser extension designed to help users save web pages as complete, self-contained files. The extension's primary function is to capture an entire web page, including its HTML, CSS, JavaScript, images, and other resources, and package them into a single HTML file. 5 | 6 | ## I am a web archivist, is it ok to use SingleFile to archive content? 7 | No, SingleFile is not a tool used by professionals to archive content on the Web, especially in the academic field. Professionals prefer to rely on tools based on the [WARC specification](https://iipc.github.io/warc-specifications/) instead. 8 | 9 | ## Does SingleFile upload any data to third-party servers? 10 | As stated in the [privacy policy](https://github.com/gildas-lormeau/SingleFile/blob/master/privacy.md), SingleFile does not upload any data to third-party servers. All the work is done in your browser. However, when you save a page with SingleFile, it can download resources (images, CSS, frame contents, fonts etc.) that are not displayed or not already cached but present in the page. 11 | 12 | ## Why can't I save some pages like https://addons.mozilla.org/addon/single-file? 13 | For security purposes, browsers block web extensions on certain domains. This prevents a malicious extension to remove or change bad reviews, for example. 14 | 15 | ## Why aren't images saved on sites like sspai.com or weibo.com? 16 | These sites require the HTTP header "referer" to be present in order to download images. For privacy reasons, this feature is not enabled by default in SingleFile. To enable it, you need to check the option "Network > pass "Referer" header after a cross-origin request error". 17 | 18 | ## Why don't interactive elements like folding titles, dynamic maps or carousels work properly in saved pages? 19 | These elements need JavaScript to work properly. By default, SingleFile removes scripts because they can alter the rendering and there is no guarantee they will work offline. However, you can save them by unchecking the option "Network > blocked resources > scripts", and optionally unchecking "HTML Content > remove hidden elements", unchecking "Stylesheets > remove unused styles", and checking the option "HTML content > save raw page". 20 | 21 | ## Why isn't the infobar displayed / Why cannot I save a page from the filesystem in Chrome? 22 | By default, Chrome extensions are not allowed to access to pages stored on the filesystem. Therefore, you must enable the option "Allow access to file URLs" in the extension page to display the infobar when viewing a saved page, or to save a page stored on the filesystem. 23 | 24 | ## How does the self-extracting ZIP format work? 25 | The self-extracting ZIP files created by SingleFile are essentially regular ZIP files. They take advantage of the flexibility in the ZIP specification, which allows for additional data to be included before and after the ZIP payload. See this [presentation](https://github.com/gildas-lormeau/Polyglot-HTML-ZIP-PNG) for more info. 26 | 27 | ## What are the permissions requested by SingleFile for? 28 | The permissions requested by SingleFile are defined in the [manifest.json](https://github.com/gildas-lormeau/SingleFile/blob/master/manifest.json) file. Below are the reasons why they are necessary. 29 | - `identity`: allows SingleFile to connect to your Google Drive account. 30 | - `storage`: allows SingleFile to store your settings. 31 | - `menus/contextMenus`: allows SingleFile to display an entry in the context menu of web pages. 32 | - `tabs` (all_urls): allows SingleFile to inject the code needed to process a page in any tab. This permission is needed for saving several tabs in one click, for example. 33 | - `downloads`: allows SingleFile to save pages as if they were downloaded from the web. 34 | - `clipboardWrite`: allows SingleFile to copy the content of a page into the clipboard instead of saving it. 35 | - `nativeMessaging`: allows you to use [SingleFile Companion](https://github.com/gildas-lormeau/single-file-companion) to save pages. 36 | 37 | ## SingleFile is slow on my computer/tablet/phone, can it run faster? 38 | The default configuration of SingleFile is optimized to produce small pages. This can sometimes slow down the save process considerably. Below are the options you can disable to save time and CPU. 39 | - HTML content > remove hidden elements 40 | - Stylesheets > remove unused styles 41 | 42 | You can also disable the options below. Some resources (e.g. images, frames) on the page may be missing though. 43 | - HTML content > remove frames 44 | - Images > save deferred images 45 | 46 | ## How to convert MHTML files to single HTML files? 47 | See https://github.com/gildas-lormeau/mhtml-to-html. 48 | -------------------------------------------------------------------------------- /known-issues.md: -------------------------------------------------------------------------------- 1 | # SingleFile 2 | 3 | ## Known Issues 4 | 5 | - All browsers: 6 | - For security reasons, you cannot save pages hosted on 7 | https://chrome.google.com, https://addons.mozilla.org and some other Mozilla 8 | domains. When this happens, 🛇 is displayed on top of the SingleFile icon. 9 | - For 10 | [security reasons](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image), 11 | SingleFile is sometimes unable to save the image representation of 12 | [canvas](https://developer.mozilla.org/docs/Web/HTML/Element/canvas) and 13 | snapshots of 14 | [video](https://developer.mozilla.org/docs/Web/HTML/Element/video) elements. 15 | - The last saved path cannot be remembered by default. To circumvent this 16 | limitation, disable the option "Misc > save pages in background". 17 | - The following characters are replaced by their full-width equivalent 18 | symbols in file names: ~, +, ?, %, *, :, |, ", <, >, \. The 19 | replacement characters are respectively: ~, +, ?, %, *, :, |, ", <, >, \. 20 | Other invalid charcaters are replaced by _. This is done to maintain 21 | compatibility with various OSs and file systems. If you don't need that 22 | level of compatibility and know what you are doing, you can change the 23 | list of forbidden characters and the replacement characters in the Hidden 24 | options(https://github.com/gildas-lormeau/SingleFile/wiki/Hidden-options). 25 | - Chromium-based browsers: 26 | - You must enable the option "Allow access to file URLs" in the extension page 27 | to display the infobar when viewing a saved page, and to save or to annotate 28 | a page stored on the filesystem. 29 | - If the file name of a saved page looks like 30 | "56833935-156b-4d8c-a00f-19599c6513d3.html", disable the option "Misc > save 31 | pages in background". Reinstalling the browser may also fix this issue. This 32 | issue might also be due to a conflict with another "downloader" extension. 33 | You can find more info about this bug 34 | [here](https://bugs.chromium.org/p/chromium/issues/detail?id=892133). 35 | - Disabling the option "File name > open the "Save as" dialog to confirm the 36 | file name" will work if and only if the option "Ask where to save each file 37 | before downloading" is disabled in chrome://settings/downloads. 38 | - Firefox: 39 | - The "File name > file name conflict resolution" option does not work if set 40 | to "prompt for a name" 41 | - Sometimes, SingleFile is unable to save the contents of sandboxed iframes 42 | because of [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1411641). 43 | - When processing a page from the filesystem, external resources (e.g. images, 44 | stylesheets, fonts etc.) will not be embedded into the saved page. You can 45 | find more info about this bug 46 | [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1644488). This bug has 47 | been closed by Mozilla as "WontFix". But there is a simple workaround 48 | proposed 49 | [here](https://github.com/gildas-lormeau/SingleFile/issues/7#issuecomment-618980153). 50 | - Waterfox Classic 51 | - User interface elements displayed in the page (progress bar, logs panel) 52 | won't be displayed unless `dom.webcomponents.enabled` is enabled in 53 | `about:config`. 54 | - When opening pages saved with the option "Images > group duplicate images 55 | together" enabled, some duplicate images might not displayed. It is 56 | recommended to disable this option. 57 | 58 | ## Troubleshooting unknown issues 59 | 60 | Please follow these steps if you find an unknown issue: 61 | 62 | - Save the page in incognito. 63 | - If saving page in incognito did not fix the issue, reset SingleFile options. 64 | - If resetting options did not fix the issue, restart the browser. 65 | - If restarting the browser did not fix the issue, try to disable all other 66 | extensions to see if there is a conflict. 67 | - If there is a conflict then try to determine against which extension(s). 68 | - Please report the issue with a short description on how to reproduce it here: 69 | https://github.com/gildas-lormeau/SingleFile/issues. 70 | -------------------------------------------------------------------------------- /lib/single-file-background.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";const e=new Map,t=8388608;async function r(e,r,s){for(let a=0;a*t<=s.array.length;a++){const n={method:"singlefile.fetchResponse",requestId:r,headers:s.headers,status:s.status,error:s.error};n.truncated=s.array.length>t,n.truncated?(n.finished=(a+1)*t>s.array.length,n.array=s.array.slice(a*t,(a+1)*t)):n.array=s.array,await browser.tabs.sendMessage(e,n)}return{}}function s(t,r={},a){return new Promise(((n,i)=>{const o=new XMLHttpRequest;if(o.withCredentials=!0,o.responseType="arraybuffer",o.onerror=e=>i(new Error(e.detail)),o.onreadystatechange=()=>{o.readyState==XMLHttpRequest.DONE&&(o.status||o.response.byteLength?401!=o.status&&403!=o.status&&404!=o.status||a?n({arrayBuffer:o.response,array:Array.from(new Uint8Array(o.response)),headers:{"content-type":o.getResponseHeader("Content-Type")},status:o.status}):s(t,r,!0).then(n).catch(i):i(new Error("Empty response")))},o.open("GET",t,!0),r.headers)for(const e of Object.entries(r.headers))o.setRequestHeader(e[0],e[1]);if(a){const t=String(Math.random()).substring(2);d=t,f=r.referrer,e.set(d,f),o.setRequestHeader("x-single-file-request-id",t)}var d,f;o.send()}))}browser.runtime.onMessage.addListener(((e,t)=>{if(e.method&&e.method.startsWith("singlefile.fetch"))return new Promise((a=>{(async function(e,t){if("singlefile.fetch"==e.method)try{const a=await s(e.url,{referrer:e.referrer,headers:e.headers});return r(t.tab.id,e.requestId,a)}catch(s){return r(t.tab.id,e.requestId,{error:s.message,array:[]})}else if("singlefile.fetchFrame"==e.method)return browser.tabs.sendMessage(t.tab.id,e)})(e,t).then(a).catch((e=>a({error:e&&(e.message||e.toString())})))}))})),browser.runtime.onMessage.addListener(((e,t)=>{if("singlefile.frameTree.initResponse"==e.method||"singlefile.frameTree.ackInitRequest"==e.method)return browser.tabs.sendMessage(t.tab.id,e,{frameId:0}),Promise.resolve({})}));const a=new Map;function n(e,t){e.delete(t)}browser.runtime.onMessage.addListener(((e,t)=>{if("singlefile.lazyTimeout.setTimeout"==e.method){let r,s=a.get(t.tab.id);if(s)if(r=s.get(t.frameId),r){const t=r.get(e.type);t&&clearTimeout(t)}else r=new Map;const i=setTimeout((async()=>{try{const r=a.get(t.tab.id),s=r.get(t.frameId);r&&s&&n(s,e.type),await browser.tabs.sendMessage(t.tab.id,{method:"singlefile.lazyTimeout.onTimeout",type:e.type})}catch(e){}}),e.delay);return s||(s=new Map,r=new Map,s.set(t.frameId,r),a.set(t.tab.id,s)),r.set(e.type,i),Promise.resolve({})}if("singlefile.lazyTimeout.clearTimeout"==e.method){let r=a.get(t.tab.id);if(r){const s=r.get(t.frameId);if(s){const t=s.get(e.type);t&&clearTimeout(t),n(s,e.type)}}return Promise.resolve({})}})),browser.tabs.onRemoved.addListener((e=>a.delete(e)))}(); 2 | -------------------------------------------------------------------------------- /lib/single-file-extension-bootstrap.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";const e=33554432,t=globalThis.singlefileBootstrap,o=new Map;let n,a,r,s,d,i,c,l,u,m,h,f,v,p;async function E(){if(document.documentElement.dataset&&void 0!==document.documentElement.dataset.sfz){const e=await g();document.querySelectorAll("#sfz-error-message").forEach((e=>e.remove())),function(e){document.dispatchEvent(new CustomEvent("single-file-bootstrap",{detail:{data:e}}))}(e)}else if(document.body&&1==document.body.childNodes.length&&"PRE"==document.body.childNodes[0].tagName&&/]* data-sfz[^>]*>/i.test(document.body.childNodes[0].textContent)){const e=(new DOMParser).parseFromString(document.body.childNodes[0].textContent,"text/html");document.replaceChild(e.documentElement,document.documentElement),document.querySelectorAll("script").forEach((e=>{const t=document.createElement("script");t.textContent=e.textContent,e.parentElement.replaceChild(t,e)})),await E()}}function g(){return new Promise(((e,t)=>{const n=new XMLHttpRequest;n.open("GET",location.href),n.send(),n.responseType="arraybuffer",n.onload=()=>e(new Uint8Array(n.response)),n.onerror=()=>{const n=document.getElementById("sfz-error-message");n&&n.remove();const a=o.size;o.set(a,{resolve:e,reject:t}),browser.runtime.sendMessage({method:"singlefile.fetch",requestId:a,url:location.href})}}))}async function y(e){return d&&"content.autosave"==e.method?(async function(e){a=e.options,"complete"!=document.readyState&&await new Promise((e=>globalThis.addEventListener("load",e)));await b(),a.autoSaveRepeat&&setTimeout((()=>{d&&!c&&(l=!1,a.autoSaveDelay=0,y(e))}),1e3*a.autoSaveRepeatDelay)}(e),{}):"content.maybeInit"==e.method?(S(),{}):"content.init"==e.method?(a=e.options,d=e.autoSaveEnabled,w(),{}):"content.openEditor"==e.method?(M(document)?D(document):w(),{}):"devtools.resourceCommitted"==e.method?(t.pageInfo.updatedResources[e.url]={content:e.content,type:e.type,encoding:e.encoding},{}):"singlefile.fetchResponse"==e.method?await async function(e){const t=o.get(e.requestId);if(t)return e.error?(t.reject(new Error(e.error)),o.delete(e.requestId)):(e.truncated&&(t.array?t.array=t.array.concat(e.array):(t.array=e.array,o.set(e.requestId,t)),e.finished&&(e.array=t.array)),e.truncated&&!e.finished||(t.resolve(e.array),o.delete(e.requestId))),{}}(e):void 0}function S(){const e=document.querySelector("singlefile-infobar");e&&e.remove(),u==location.href||t.pageInfo.processing||(l=!1,u=location.href,browser.runtime.sendMessage({method:"tabs.init",savedPageDetected:M(document)}).catch((()=>{})),browser.runtime.sendMessage({method:"ui.processInit"}).catch((()=>{})))}async function b(){const e=t.helper;if((!c||i)&&!l)if(c=!0,a.autoSaveDelay&&!i)await new Promise((e=>i=setTimeout(e,1e3*a.autoSaveDelay))),await b();else{const o=globalThis[e.WAIT_FOR_USERSCRIPT_PROPERTY_NAME];let n,r=[];i=null,!a.removeFrames&&globalThis.frames&&globalThis.frames.length&&(r=await t.processors.frameTree.getAsync(a)),n=r&&r.sessionId,a.userScriptEnabled&&o&&await o(e.ON_BEFORE_CAPTURE_EVENT_NAME);const s=e.preProcessDoc(document,globalThis,a);A(s,r),n&&t.processors.frameTree.cleanup(n),e.postProcessDoc(document,s.markedElements,s.invalidElements),a.userScriptEnabled&&o&&await o(e.ON_AFTER_CAPTURE_EVENT_NAME),l=!0,c=!1}}function w(){d&&a&&(a.autoSaveUnload||a.autoSaveLoadOrUnload||a.autoSaveDiscard||a.autoSaveRemove)?n||(globalThis.addEventListener("unload",T),document.addEventListener("visibilitychange",R),n=!0):(globalThis.removeEventListener("unload",T),document.removeEventListener("visibilitychange",R),n=!1)}function R(){"hidden"==document.visibilityState&&a.autoSaveDiscard&&C({autoSaveDiscard:a.autoSaveDiscard})}function T(){!l&&(a.autoSaveUnload||a.autoSaveLoadOrUnload||a.autoSaveRemove)&&C({autoSaveUnload:a.autoSaveUnload,autoSaveRemove:a.autoSaveRemove})}function C({autoSaveUnload:e,autoSaveDiscard:o,autoSaveRemove:n}){const r=t.helper,s=globalThis[r.WAIT_FOR_USERSCRIPT_PROPERTY_NAME];let d=[];!a.removeFrames&&globalThis.frames&&globalThis.frames.length&&(d=t.processors.frameTree.getSync(a)),a.userScriptEnabled&&s&&s(r.ON_BEFORE_CAPTURE_EVENT_NAME);A(r.preProcessDoc(document,globalThis,a),d,{autoSaveUnload:e,autoSaveDiscard:o,autoSaveRemove:n})}function A(e,o,{autoSaveUnload:n,autoSaveDiscard:d,autoSaveRemove:i}={}){const c=t.helper,l=t.pageInfo.updatedResources,u=t.pageInfo.visitDate.getTime();Object.keys(l).forEach((e=>l[e].retrieved=!1)),browser.runtime.sendMessage({method:"autosave.save",tabId:r,tabIndex:s,taskId:a.taskId,content:c.serialize(document),canvases:e.canvases,fonts:e.fonts,stylesheets:e.stylesheets,images:e.images,posters:e.posters,usedFonts:e.usedFonts,shadowRoots:e.shadowRoots,videos:e.videos,referrer:e.referrer,adoptedStyleSheets:e.adoptedStyleSheets,worklets:e.worklets,frames:o,url:location.href,updatedResources:l,visitDate:u,autoSaveUnload:n,autoSaveDiscard:d,autoSaveRemove:i})}async function D(o){let n;h?n=await g():(O(o),n=t.helper.serialize(o));for(let t=0;t*ee,o.truncated?(o.finished=(t+1)*e>n.length,n instanceof Uint8Array?o.content=Array.from(n.subarray(t*e,(t+1)*e)):o.content=n.substring(t*e,(t+1)*e)):(o.embeddedImage=await I(n),o.content=n instanceof Uint8Array?Array.from(n):n),await browser.runtime.sendMessage(o)}}async function I(e){if(137==e[0]&&80==e[1]&&78==e[2]&&71==e[3]){let t=new Blob([new Uint8Array(e)],{type:"image/png"});const o=URL.createObjectURL(t),n=new Image;n.src=o,await new Promise(((e,t)=>{n.onload=e,n.onerror=t}));const a=new OffscreenCanvas(n.width,n.height);a.getContext("2d").drawImage(n,0,0),t=await a.convertToBlob({type:"image/png"});const r=await t.arrayBuffer();return Array.from(new Uint8Array(r))}}function M(e){if(void 0===m){const o=t.helper,n=e.documentElement.firstChild;h=e.documentElement.dataset&&""==e.documentElement.dataset.sfz,f=Boolean(e.querySelector("sfz-extra-data")),v=Boolean(e.querySelector("body > main[hidden]")),p=Boolean(e.querySelector("meta[http-equiv=content-security-policy]")),m=h||n.nodeType==Node.COMMENT_NODE&&(n.textContent.includes(o.COMMENT_HEADER)||n.textContent.includes(o.COMMENT_HEADER_LEGACY))}return m}function O(e){e.querySelectorAll("*").forEach((e=>{const o=t.helper.getShadowRoot(e);if(o){O(o);const t=document.createElement("template");t.setAttribute("shadowrootmode","open"),Array.from(o.childNodes).forEach((e=>t.appendChild(e))),e.appendChild(t)}}))}t.pageInfo={updatedResources:{},visitDate:new Date},browser.runtime.sendMessage({method:"bootstrap.init"}).then((e=>{a=e.optionsAutoSave;const t=e.options;r=e.tabId,s=e.tabIndex,d=e.autoSaveEnabled,t&&t.autoOpenEditor&&M(document)?"loading"==document.readyState?document.addEventListener("DOMContentLoaded",(()=>D(document))):D(document):"loading"==document.readyState?document.addEventListener("DOMContentLoaded",w):w()})),browser.runtime.onMessage.addListener((e=>{if(d&&"content.autosave"==e.method||"content.maybeInit"==e.method||"content.init"==e.method||"content.openEditor"==e.method||"devtools.resourceCommitted"==e.method||"singlefile.fetchResponse"==e.method)return y(e)})),document.addEventListener("DOMContentLoaded",S,!1),globalThis.window==globalThis.top&&location&&location.href&&(location.href.startsWith("file://")||location.href.startsWith("content://"))&&("loading"==document.readyState?document.addEventListener("DOMContentLoaded",E,!1):E())}(); 2 | -------------------------------------------------------------------------------- /lib/single-file-extension-core.js: -------------------------------------------------------------------------------- 1 | !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).extension={})}(this,(function(e){"use strict";let r,t;const a=["lib/web-stream.js","lib/chrome-browser-polyfill.js","lib/single-file.js"],n=["lib/chrome-browser-polyfill.js","lib/single-file-frames.js"];async function s(e,s){let o;if(await async function(e){const s=e.extensionScriptFiles||[];r||t||([r,t]=await Promise.all([i(a.concat(s)),i(n)]))}(s),!s.removeFrames)try{await browser.tabs.executeScript(e,{code:t,allFrames:!0,matchAboutBlank:!0,runAt:"document_start"})}catch(e){}try{await browser.tabs.executeScript(e,{code:r,allFrames:!1,runAt:"document_idle"}),o=!0}catch(e){}return o&&s.frameId&&await browser.tabs.executeScript(e,{code:"document.documentElement.dataset.requestedFrameId = true",frameId:s.frameId,matchAboutBlank:!0,runAt:"document_start"}),o}async function i(e){const r=e.map((async e=>{if("function"==typeof e)return"("+e.toString()+")();";{const r=await fetch(browser.runtime.getURL("../../../"+e));return(new TextDecoder).decode(await r.arrayBuffer())}}));let t="";for(const e of r)t+=await e;return t}const o="single-file-response-fetch",c="Host fetch error (SingleFile)",f=Boolean(window.wrappedJSObject),d=window.fetch.bind(window);let u,l=0,h=new Map;async function w(e,r={},t=!0){try{const a={cache:r.cache||"force-cache",headers:r.headers,referrerPolicy:r.referrerPolicy||"strict-origin-when-cross-origin"};let n;try{n=r.referrer&&!f||!t?await d(e,a):await async function(e,r){if(void 0===u&&(u=!1,document.addEventListener("single-file-response-fetch-supported",(()=>u=!0),!1),document.dispatchEvent(new CustomEvent("single-file-request-fetch-supported"))),u)return new Promise(((t,a)=>{document.dispatchEvent(new CustomEvent("single-file-request-fetch",{detail:JSON.stringify({url:e,options:r})})),document.addEventListener(o,(function r(n){n.detail?n.detail.url==e&&(document.removeEventListener(o,r,!1),n.detail.response?t({status:n.detail.status,headers:new Map(n.detail.headers),arrayBuffer:async()=>n.detail.response}):a(n.detail.error)):a()}),!1)}));throw new Error(c)}(e,a),401!=n.status&&403!=n.status&&404!=n.status||"no-referrer"==a.referrerPolicy||r.referrer||(n=await w(e,{...a,referrerPolicy:"no-referrer"},t))}catch(s){if(s&&s.message==c)n=await w(e,{...a},!1);else{if("no-referrer"==a.referrerPolicy||r.referrer)throw s;n=await w(e,{...a,referrerPolicy:"no-referrer"},t)}}return n}catch(t){l++;const a=new Promise(((e,r)=>h.set(l,{resolve:e,reject:r})));return await m({method:"singlefile.fetch",url:e,requestId:l,referrer:r.referrer,headers:r.headers}),a}}async function y(e,r){const t=await m({method:"singlefile.fetchFrame",url:e,frameId:r.frameId,referrer:r.referrer,headers:r.headers});return{status:t.status,headers:new Map(t.headers),arrayBuffer:async()=>new Uint8Array(t.array).buffer}}async function m(e){const r=await browser.runtime.sendMessage(e);if(!r||r.error)throw new Error(r&&r.error&&r.error.toString());return r}browser.runtime.onMessage.addListener((e=>"singlefile.fetchFrame"==e.method&&window.frameId&&window.frameId==e.frameId?async function(e){try{const r=await d(e.url,{cache:"force-cache",headers:e.headers,referrerPolicy:"strict-origin-when-cross-origin"});return{status:r.status,headers:[...r.headers],array:Array.from(new Uint8Array(await r.arrayBuffer()))}}catch(e){return{error:e&&(e.message||e.toString())}}}(e):"singlefile.fetchResponse"==e.method?async function(e){const r=h.get(e.requestId);r&&(e.error?(r.reject(new Error(e.error)),h.delete(e.requestId)):(e.truncated&&(r.array?r.array=r.array.concat(e.array):(r.array=e.array,h.set(e.requestId,r)),e.finished&&(e.array=r.array)),e.truncated&&!e.finished||(r.resolve({status:e.status,headers:{get:r=>e.headers&&e.headers[r]},arrayBuffer:async()=>new Uint8Array(e.array).buffer}),h.delete(e.requestId))));return{}}(e):void 0)),e.getPageData=function(e,r={fetch:w,frameFetch:y},t,a){return globalThis.singlefile.getPageData(e,r,t,a)},e.injectScript=function(e,r){return s(e,r)}})); 2 | -------------------------------------------------------------------------------- /lib/single-file-extension-editor-init.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";document.currentScript.remove(),function e(t){t.querySelectorAll("template[shadowrootmode]").forEach((t=>{let o=t.parentElement.shadowRoot;if(!o){try{o=t.parentElement.attachShadow({mode:t.getAttribute("shadowrootmode")}),o.innerHTML=t.innerHTML,t.remove()}catch(e){}o&&e(o)}}))}(document)}(); 2 | -------------------------------------------------------------------------------- /lib/single-file-extension-frames.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";const e=globalThis.document;if(e instanceof globalThis.Document){let n=e.createElement("script");n.src="data:,("+t.toString()+")()",(e.documentElement||e).appendChild(n),n.remove(),n=e.createElement("script"),n.textContent="("+t.toString()+")()",(e.documentElement||e).appendChild(n),n.remove()}function t(){"undefined"==typeof globalThis&&(window.globalThis=window);const e=globalThis.document,t=globalThis.CustomEvent,n=globalThis.FileReader,r=globalThis.Blob,a={family:"font-family",style:"font-style",weight:"font-weight",stretch:"font-stretch",unicodeRange:"unicode-range",variant:"font-variant",featureSettings:"font-feature-settings"};if(globalThis.FontFace){const n=globalThis.FontFace;globalThis.FontFace=function(){return o(...arguments).then((n=>e.dispatchEvent(new t("single-file-new-font-face",{detail:n})))),new n(...arguments)},globalThis.FontFace.prototype=n.prototype,globalThis.FontFace.toString=function(){return"function FontFace() { [native code] }"};const r=e.fonts.delete;e.fonts.delete=function(n){return o(n.family).then((n=>e.dispatchEvent(new t("single-file-delete-font",{detail:n})))),r.call(e.fonts,n)},e.fonts.delete.toString=function(){return"function delete() { [native code] }"};const a=e.fonts.clear;e.fonts.clear=function(){return e.dispatchEvent(new t("single-file-clear-fonts")),a.call(e.fonts)},e.fonts.clear.toString=function(){return"function clear() { [native code] }"}}async function o(e,t,o){const s={};return s["font-family"]=e,s.src=t,o&&Object.keys(o).forEach((e=>{a[e]&&(s[a[e]]=o[e])})),new Promise((e=>{if(s.src instanceof ArrayBuffer){const t=new n;t.readAsDataURL(new r([s.src])),t.addEventListener("load",(()=>{s.src="url("+t.result+")",e(s)}))}else e(s)}))}}const n=globalThis.browser,r=globalThis.document;if(r instanceof globalThis.Document&&n&&n.runtime&&n.runtime.getURL){const e=r.createElement("script");e.src=n.runtime.getURL("/lib/single-file-hooks-frames.js"),e.async=!1,(r.documentElement||r).appendChild(e),e.remove()}const a=window.fetch.bind(window);let o=new Map;browser.runtime.onMessage.addListener((e=>"singlefile.fetchFrame"==e.method&&window.frameId&&window.frameId==e.frameId?async function(e){try{const t=await a(e.url,{cache:"force-cache",headers:e.headers,referrerPolicy:"strict-origin-when-cross-origin"});return{status:t.status,headers:[...t.headers],array:Array.from(new Uint8Array(await t.arrayBuffer()))}}catch(e){return{error:e&&(e.message||e.toString())}}}(e):"singlefile.fetchResponse"==e.method?async function(e){const t=o.get(e.requestId);t&&(e.error?(t.reject(new Error(e.error)),o.delete(e.requestId)):(e.truncated&&(t.array?t.array=t.array.concat(e.array):(t.array=e.array,o.set(e.requestId,t)),e.finished&&(e.array=t.array)),e.truncated&&!e.finished||(t.resolve({status:e.status,headers:{get:t=>e.headers&&e.headers[t]},arrayBuffer:async()=>new Uint8Array(e.array).buffer}),o.delete(e.requestId))));return{}}(e):void 0))}(); 2 | -------------------------------------------------------------------------------- /lib/web-stream.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";void 0===globalThis.TransformStream&&(globalThis.TransformStream=class{}),void 0===globalThis.WritableStream&&(globalThis.WritableStream=class{})}(); 2 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SingleFile", 3 | "author": "Gildas Lormeau", 4 | "homepage_url": "https://www.getsinglefile.com", 5 | "icons": { 6 | "16": "src/ui/resources/icon_16.png", 7 | "32": "src/ui/resources/icon_32.png", 8 | "48": "src/ui/resources/icon_48.png", 9 | "64": "src/ui/resources/icon_64.png", 10 | "128": "src/ui/resources/icon_128.png" 11 | }, 12 | "version": "1.22.81", 13 | "description": "__MSG_extensionDescription__", 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "" 18 | ], 19 | "run_at": "document_start", 20 | "js": [ 21 | "lib/chrome-browser-polyfill.js", 22 | "lib/single-file-frames.js", 23 | "lib/single-file-extension-frames.js" 24 | ], 25 | "all_frames": true, 26 | "match_about_blank": true 27 | }, 28 | { 29 | "matches": [ 30 | "" 31 | ], 32 | "run_at": "document_start", 33 | "js": [ 34 | "lib/web-stream.js", 35 | "lib/chrome-browser-polyfill.js", 36 | "lib/single-file-bootstrap.js", 37 | "lib/single-file-extension-bootstrap.js", 38 | "lib/single-file-infobar.js" 39 | ] 40 | } 41 | ], 42 | "background": { 43 | "page": "src/core/bg/background.html" 44 | }, 45 | "sidebar_action": { 46 | "browser_style": true, 47 | "default_title": "SingleFile", 48 | "default_panel": "src/ui/pages/panel.html", 49 | "default_icon": "src/ui/resources/icon_128.png", 50 | "open_at_install": false 51 | }, 52 | "options_ui": { 53 | "browser_style": true, 54 | "page": "src/ui/pages/options.html", 55 | "open_in_tab": false 56 | }, 57 | "browser_action": { 58 | "default_icon": { 59 | "16": "src/ui/resources/icon_16.png", 60 | "32": "src/ui/resources/icon_32.png", 61 | "48": "src/ui/resources/icon_48.png", 62 | "64": "src/ui/resources/icon_64.png", 63 | "128": "src/ui/resources/icon_128.png" 64 | }, 65 | "default_title": "__MSG_buttonDefaultTooltip__" 66 | }, 67 | "commands": { 68 | "save-selected-tabs": { 69 | "suggested_key": { 70 | "default": "Ctrl+Shift+Y" 71 | }, 72 | "description": "__MSG_commandSaveSelectedTabs__" 73 | }, 74 | "save-all-tabs": { 75 | "suggested_key": { 76 | "default": "Ctrl+Shift+U" 77 | }, 78 | "description": "__MSG_commandSaveAllTabs__" 79 | } 80 | }, 81 | "web_accessible_resources": [ 82 | "lib/single-file-hooks-frames.js", 83 | "lib/single-file-infobar.js", 84 | "lib/single-file-extension-editor-init.js", 85 | "lib/single-file-extension-editor.js", 86 | "lib/single-file-extension-editor-helper.js", 87 | "lib/single-file-zip.min.js", 88 | "lib/single-file-z-worker.js", 89 | "lib/web-stream.js", 90 | "src/lib/readability/Readability.js", 91 | "src/lib/readability/Readability-readerable.js", 92 | "src/ui/pages/editor-note-web.css", 93 | "src/ui/pages/editor-mask-web.css", 94 | "src/ui/pages/editor-frame-web.css" 95 | ], 96 | "permissions": [ 97 | "identity", 98 | "menus", 99 | "downloads", 100 | "storage", 101 | "tabs", 102 | "" 103 | ], 104 | "optional_permissions": [ 105 | "clipboardWrite", 106 | "nativeMessaging", 107 | "bookmarks", 108 | "webRequest", 109 | "webRequestBlocking" 110 | ], 111 | "browser_specific_settings": { 112 | "gecko": { 113 | "id": "{531906d3-e22f-4a6c-a102-8057b88a1a63}" 114 | }, 115 | "gecko_android": {} 116 | }, 117 | "devtools_page": "src/core/devtools/devtools.html", 118 | "incognito": "spanning", 119 | "manifest_version": 2, 120 | "default_locale": "en" 121 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "single-file", 3 | "version": "1.2.4", 4 | "description": "SingleFile", 5 | "author": "Gildas Lormeau", 6 | "license": "AGPL-3.0-or-later", 7 | "scripts": { 8 | "dev": "npx rollup -c rollup.config.dev.js", 9 | "build": "./build-extension.sh" 10 | }, 11 | "type": "module", 12 | "dependencies": { 13 | "single-file-core": "1.5.48" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^9.20.1", 17 | "@rollup/plugin-node-resolve": "15.2.3", 18 | "@rollup/plugin-terser": "0.4.4", 19 | "rollup": "4.22.4" 20 | }, 21 | "overrides": { 22 | "terser": "^5.15.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | Your data always belongs to you, and only you. SingleFile does not collect any data. 3 | 4 | ## How your data is protected and used 5 | - By default, SingleFile does not send any data to our servers or any third parties. All your data is processed and stored locally on your device. 6 | - If you connect SingleFile to your Google Drive account, data is only transmitted from your browser to servers belonging to Google. 7 | - If you connect SingleFile to your Dropbox account, data is only transmitted from your browser to servers belonging to Dropbox. 8 | - If you save pages on GitHub, data is only transmitted from your browser to servers belonging to GitHub. 9 | - If you enable the "synchronize options" setting, your options will be sent to servers belonging to the vendor of the browser you are using. 10 | - If you enable the "add proof of existence" setting, a SHA256 hash of each saved page will be sent to servers belonging to Woleet. 11 | - When processing a page, SingleFile needs to inject temporary data into the page to save it accurately. SingleFile can download resources that are not displayed or not already cached but present in the page. When using SingleFile in Chromium-based browsers, websites can fetch resources declared in the "web_accessible_resources" section of the manifest.json file. Because of this, a website may be able to detect you are using SingleFile or have saved a page on the website with SingleFile. 12 | 13 | ## Customer support 14 | When you request for customer support, we may ask for your email address to get back to you. 15 | 16 | ## Changes to our privacy policy 17 | We keep our privacy notice under regular review and we will place any updates on this document. This privacy notice was last updated on December 2019. 18 | 19 | ## How to contact us 20 | Please contact us if you have any questions about our privacy policy by email to gildas.lormeau <at> gmail.com. 21 | 22 | ## History 23 | cf. https://github.com/gildas-lormeau/SingleFile/commits/master/privacy.md 24 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import terser from "@rollup/plugin-terser"; 3 | 4 | const PLUGINS = [resolve({ moduleDirectories: [".."] })]; 5 | const EXTERNAL = ["single-file-core"]; 6 | 7 | export default [{ 8 | input: ["single-file-core/single-file.js"], 9 | output: [{ 10 | file: "lib/single-file.js", 11 | format: "umd", 12 | name: "singlefile", 13 | plugins: [] 14 | }], 15 | plugins: PLUGINS, 16 | external: EXTERNAL 17 | }, { 18 | input: ["single-file-core/single-file-frames.js"], 19 | output: [{ 20 | file: "lib/single-file-frames.js", 21 | format: "umd", 22 | name: "singlefile", 23 | plugins: [] 24 | }], 25 | plugins: PLUGINS, 26 | external: EXTERNAL 27 | }, { 28 | input: ["single-file-core/single-file-bootstrap.js"], 29 | output: [{ 30 | file: "lib/single-file-bootstrap.js", 31 | format: "umd", 32 | name: "singlefileBootstrap", 33 | plugins: [] 34 | }], 35 | plugins: PLUGINS, 36 | external: EXTERNAL 37 | }, { 38 | input: ["single-file-core/single-file-hooks-frames.js"], 39 | output: [{ 40 | file: "lib/single-file-hooks-frames.js", 41 | format: "iife", 42 | plugins: [] 43 | }], 44 | plugins: PLUGINS, 45 | external: EXTERNAL 46 | }, { 47 | input: ["single-file-core/single-file-infobar.js"], 48 | output: [{ 49 | file: "lib/single-file-infobar.js", 50 | format: "iife", 51 | plugins: [terser()] 52 | }], 53 | plugins: PLUGINS, 54 | external: EXTERNAL 55 | }, { 56 | input: ["single-file-core/vendor/zip/z-worker.js"], 57 | output: [{ 58 | file: "lib/single-file-z-worker.js", 59 | format: "es", 60 | plugins: [] 61 | }], 62 | plugins: PLUGINS, 63 | external: EXTERNAL 64 | }, { 65 | input: ["single-file-core/vendor/zip/zip.js"], 66 | output: [{ 67 | file: "lib/single-file-zip.js", 68 | format: "es", 69 | plugins: [] 70 | }], 71 | context: "this", 72 | plugins: PLUGINS, 73 | external: EXTERNAL 74 | }, { 75 | input: ["single-file-core/vendor/zip/zip.min.js"], 76 | output: [{ 77 | file: "lib/single-file-zip.min.js", 78 | format: "es", 79 | plugins: [] 80 | }], 81 | context: "this", 82 | plugins: PLUGINS, 83 | external: EXTERNAL 84 | }, { 85 | input: ["src/core/content/content-bootstrap.js"], 86 | output: [{ 87 | file: "lib/single-file-extension-bootstrap.js", 88 | format: "iife", 89 | plugins: [] 90 | }] 91 | }, { 92 | input: ["src/core/content/content-frames.js"], 93 | output: [{ 94 | file: "lib/single-file-extension-frames.js", 95 | format: "iife", 96 | plugins: [] 97 | }] 98 | }, { 99 | input: ["src/index.js"], 100 | output: [{ 101 | file: "lib/single-file-extension-core.js", 102 | format: "umd", 103 | name: "extension", 104 | plugins: [] 105 | }] 106 | }, { 107 | input: ["src/core/content/content.js"], 108 | output: [{ 109 | file: "lib/single-file-extension.js", 110 | format: "iife", 111 | plugins: [] 112 | }] 113 | }, { 114 | input: ["src/ui/content/content-ui-editor-init-web.js"], 115 | output: [{ 116 | file: "lib/single-file-extension-editor-init.js", 117 | format: "iife", 118 | plugins: [] 119 | }], 120 | plugins: PLUGINS, 121 | external: EXTERNAL 122 | }, { 123 | input: ["src/ui/content/content-ui-editor-web.js"], 124 | output: [{ 125 | file: "lib/single-file-extension-editor.js", 126 | format: "iife", 127 | plugins: [] 128 | }], 129 | plugins: PLUGINS, 130 | external: EXTERNAL 131 | }, { 132 | input: ["single-file-core/single-file-editor-helper.js"], 133 | output: [{ 134 | file: "lib/single-file-extension-editor-helper.js", 135 | format: "umd", 136 | name: "singlefile", 137 | plugins: [] 138 | }], 139 | plugins: PLUGINS, 140 | external: EXTERNAL 141 | }, { 142 | input: ["src/lib/single-file/browser-polyfill/chrome-browser-polyfill.js"], 143 | output: [{ 144 | file: "lib/chrome-browser-polyfill.js", 145 | format: "iife", 146 | plugins: [] 147 | }] 148 | }, { 149 | input: ["src/core/bg/index.js"], 150 | output: [{ 151 | file: "lib/single-file-extension-background.js", 152 | format: "iife", 153 | plugins: [] 154 | }] 155 | }, { 156 | input: ["src/lib/single-file/background.js"], 157 | output: [{ 158 | file: "lib/single-file-background.js", 159 | format: "iife", 160 | plugins: [] 161 | }] 162 | }, { 163 | input: ["src/lib/web-stream/index.js"], 164 | output: [{ 165 | file: "lib/web-stream.js", 166 | format: "iife", 167 | plugins: [] 168 | }] 169 | }]; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import terser from "@rollup/plugin-terser"; 3 | 4 | const PLUGINS = [resolve({ moduleDirectories: ["node_modules"] })]; 5 | const EXTERNAL = ["single-file-core"]; 6 | 7 | export default [{ 8 | input: ["single-file-core/single-file.js"], 9 | output: [{ 10 | file: "lib/single-file.js", 11 | format: "umd", 12 | name: "singlefile", 13 | plugins: [terser()] 14 | }], 15 | plugins: PLUGINS, 16 | external: EXTERNAL 17 | }, { 18 | input: ["single-file-core/single-file-frames.js"], 19 | output: [{ 20 | file: "lib/single-file-frames.js", 21 | format: "umd", 22 | name: "singlefile", 23 | plugins: [terser()] 24 | }], 25 | plugins: PLUGINS, 26 | external: EXTERNAL 27 | }, { 28 | input: ["single-file-core/single-file-bootstrap.js"], 29 | output: [{ 30 | file: "lib/single-file-bootstrap.js", 31 | format: "umd", 32 | name: "singlefileBootstrap", 33 | plugins: [terser()] 34 | }], 35 | plugins: PLUGINS, 36 | external: EXTERNAL 37 | }, { 38 | input: ["single-file-core/single-file-hooks-frames.js"], 39 | output: [{ 40 | file: "lib/single-file-hooks-frames.js", 41 | format: "iife", 42 | plugins: [terser()] 43 | }], 44 | plugins: PLUGINS, 45 | external: EXTERNAL 46 | }, { 47 | input: ["single-file-core/single-file-infobar.js"], 48 | output: [{ 49 | file: "lib/single-file-infobar.js", 50 | format: "iife", 51 | plugins: [terser()] 52 | }], 53 | plugins: PLUGINS, 54 | external: EXTERNAL 55 | }, { 56 | input: ["single-file-core/vendor/zip/z-worker.js"], 57 | output: [{ 58 | file: "lib/single-file-z-worker.js", 59 | format: "es", 60 | plugins: [terser()] 61 | }], 62 | plugins: PLUGINS, 63 | external: EXTERNAL 64 | }, { 65 | input: ["single-file-core/vendor/zip/zip.js"], 66 | output: [{ 67 | file: "lib/single-file-zip.js", 68 | format: "es", 69 | plugins: [terser()] 70 | }], 71 | context: "this", 72 | plugins: PLUGINS, 73 | external: EXTERNAL 74 | }, { 75 | input: ["single-file-core/vendor/zip/zip.min.js"], 76 | output: [{ 77 | file: "lib/single-file-zip.min.js", 78 | format: "es", 79 | plugins: [terser()] 80 | }], 81 | context: "this", 82 | plugins: PLUGINS, 83 | external: EXTERNAL 84 | }, { 85 | input: ["src/core/content/content-bootstrap.js"], 86 | output: [{ 87 | file: "lib/single-file-extension-bootstrap.js", 88 | format: "iife", 89 | plugins: [terser()] 90 | }] 91 | }, { 92 | input: ["src/core/content/content-frames.js"], 93 | output: [{ 94 | file: "lib/single-file-extension-frames.js", 95 | format: "iife", 96 | plugins: [terser()] 97 | }] 98 | }, { 99 | input: ["src/index.js"], 100 | output: [{ 101 | file: "lib/single-file-extension-core.js", 102 | format: "umd", 103 | name: "extension", 104 | plugins: [terser()] 105 | }] 106 | }, { 107 | input: ["src/core/content/content.js"], 108 | output: [{ 109 | file: "lib/single-file-extension.js", 110 | format: "iife", 111 | plugins: [terser()] 112 | }] 113 | }, { 114 | input: ["src/ui/content/content-ui-editor-init-web.js"], 115 | output: [{ 116 | file: "lib/single-file-extension-editor-init.js", 117 | format: "iife", 118 | plugins: [terser()] 119 | }], 120 | plugins: PLUGINS, 121 | external: EXTERNAL 122 | }, { 123 | input: ["src/ui/content/content-ui-editor-web.js"], 124 | output: [{ 125 | file: "lib/single-file-extension-editor.js", 126 | format: "iife", 127 | plugins: [] 128 | }], 129 | plugins: PLUGINS, 130 | external: EXTERNAL 131 | }, { 132 | input: ["single-file-core/single-file-editor-helper.js"], 133 | output: [{ 134 | file: "lib/single-file-extension-editor-helper.js", 135 | format: "umd", 136 | name: "singlefile", 137 | plugins: [terser()] 138 | }], 139 | plugins: PLUGINS, 140 | external: EXTERNAL 141 | }, { 142 | input: ["src/lib/single-file/browser-polyfill/chrome-browser-polyfill.js"], 143 | output: [{ 144 | file: "lib/chrome-browser-polyfill.js", 145 | format: "iife", 146 | plugins: [terser()] 147 | }] 148 | }, { 149 | input: ["src/core/bg/index.js"], 150 | output: [{ 151 | file: "lib/single-file-extension-background.js", 152 | format: "iife", 153 | plugins: [terser()] 154 | }] 155 | }, { 156 | input: ["src/lib/single-file/background.js"], 157 | output: [{ 158 | file: "lib/single-file-background.js", 159 | format: "iife", 160 | plugins: [terser()] 161 | }] 162 | }, { 163 | input: ["src/lib/web-stream/index.js"], 164 | output: [{ 165 | file: "lib/web-stream.js", 166 | format: "iife", 167 | plugins: [terser()] 168 | }] 169 | }]; -------------------------------------------------------------------------------- /src/core/bg/autosave-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | import * as config from "./config.js"; 27 | import * as tabsData from "./tabs-data.js"; 28 | 29 | export { 30 | autoSaveIsEnabled, 31 | refreshAutoSaveTabs 32 | }; 33 | 34 | async function autoSaveIsEnabled(tab) { 35 | if (tab) { 36 | const [allTabsData, rule] = await Promise.all([tabsData.get(), config.getRule(tab.url)]); 37 | return Boolean(allTabsData.autoSaveAll || 38 | (allTabsData.autoSaveUnpinned && !tab.pinned) || 39 | (allTabsData[tab.id] && allTabsData[tab.id].autoSave)) && 40 | (!rule || rule.autoSaveProfile != config.DISABLED_PROFILE_NAME); 41 | } 42 | } 43 | 44 | async function refreshAutoSaveTabs() { 45 | const tabs = (await browser.tabs.query({})); 46 | return Promise.all(tabs.map(async tab => { 47 | const [options, autoSaveEnabled] = await Promise.all([config.getOptions(tab.url, true), autoSaveIsEnabled(tab)]); 48 | try { 49 | await browser.tabs.sendMessage(tab.id, { method: "content.init", autoSaveEnabled, options }); 50 | // eslint-disable-next-line no-unused-vars 51 | } catch (error) { 52 | // ignored 53 | } 54 | })); 55 | } -------------------------------------------------------------------------------- /src/core/bg/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Background page 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/core/bg/bookmarks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | import * as config from "./config.js"; 27 | import * as business from "./business.js"; 28 | 29 | const pendingSaves = new Set(); 30 | 31 | Promise.resolve().then(enable); 32 | 33 | export { 34 | onMessage, 35 | enable as saveCreatedBookmarks, 36 | disable, 37 | update 38 | }; 39 | 40 | async function onMessage(message) { 41 | if (message.method.endsWith(".saveCreatedBookmarks")) { 42 | enable(); 43 | return {}; 44 | } 45 | if (message.method.endsWith(".disable")) { 46 | disable(); 47 | return {}; 48 | } 49 | } 50 | 51 | async function enable() { 52 | try { 53 | browser.bookmarks.onCreated.removeListener(onCreated); 54 | browser.bookmarks.onMoved.removeListener(onMoved); 55 | // eslint-disable-next-line no-unused-vars 56 | } catch (error) { 57 | // ignored 58 | } 59 | let enabled; 60 | const profiles = await config.getProfiles(); 61 | Object.keys(profiles).forEach(profileName => { 62 | if (profiles[profileName].saveCreatedBookmarks) { 63 | enabled = true; 64 | } 65 | }); 66 | if (enabled) { 67 | browser.bookmarks.onCreated.addListener(onCreated); 68 | browser.bookmarks.onMoved.addListener(onMoved); 69 | } 70 | } 71 | 72 | async function disable() { 73 | let disabled; 74 | const profiles = await config.getProfiles(); 75 | Object.keys(profiles).forEach(profileName => disabled = disabled || !profiles[profileName].saveCreatedBookmarks); 76 | if (disabled) { 77 | browser.bookmarks.onCreated.removeListener(onCreated); 78 | browser.bookmarks.onMoved.removeListener(onMoved); 79 | } 80 | } 81 | 82 | async function update(id, changes) { 83 | try { 84 | await browser.bookmarks.update(id, changes); 85 | // eslint-disable-next-line no-unused-vars 86 | } catch (error) { 87 | // ignored 88 | } 89 | } 90 | 91 | async function onCreated(bookmarkId, bookmarkInfo) { 92 | pendingSaves.add(bookmarkId); 93 | await saveBookmark(bookmarkId, bookmarkInfo.url, bookmarkInfo); 94 | } 95 | 96 | async function onMoved(bookmarkId, bookmarkInfo) { 97 | if (pendingSaves.has(bookmarkId)) { 98 | const bookmarks = await browser.bookmarks.get(bookmarkId); 99 | if (bookmarks[0]) { 100 | await saveBookmark(bookmarkId, bookmarks[0].url, bookmarkInfo); 101 | } 102 | } 103 | } 104 | 105 | async function saveBookmark(bookmarkId, url, bookmarkInfo) { 106 | const activeTabs = await browser.tabs.query({ lastFocusedWindow: true, active: true }); 107 | const options = await config.getOptions(url); 108 | if (options.saveCreatedBookmarks) { 109 | const bookmarkFolders = await getParentFolders(bookmarkInfo.parentId); 110 | const allowedBookmarkSet = options.allowedBookmarkFolders.toString(); 111 | const allowedBookmark = bookmarkFolders.find(folder => options.allowedBookmarkFolders.includes(folder)); 112 | const ignoredBookmarkSet = options.ignoredBookmarkFolders.toString(); 113 | const ignoredBookmark = bookmarkFolders.find(folder => options.ignoredBookmarkFolders.includes(folder)); 114 | if ( 115 | ((allowedBookmarkSet && allowedBookmark) || !allowedBookmarkSet) && 116 | ((ignoredBookmarkSet && !ignoredBookmark) || !ignoredBookmarkSet) 117 | ) { 118 | if (activeTabs.length && activeTabs[0].url == url) { 119 | pendingSaves.delete(bookmarkId); 120 | business.saveTabs(activeTabs, { bookmarkId, bookmarkFolders }); 121 | } else { 122 | const tabs = await browser.tabs.query({}); 123 | if (tabs.length) { 124 | const tab = tabs.find(tab => tab.url == url); 125 | if (tab) { 126 | pendingSaves.delete(bookmarkId); 127 | business.saveTabs([tab], { bookmarkId, bookmarkFolders }); 128 | } else { 129 | if (url) { 130 | if (url == "about:blank") { 131 | browser.bookmarks.onChanged.addListener(onChanged); 132 | } else { 133 | saveUrl(url); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | async function getParentFolders(id, folderNames = []) { 143 | if (id) { 144 | const bookmarkNode = (await browser.bookmarks.get(id))[0]; 145 | if (bookmarkNode && bookmarkNode.title) { 146 | folderNames.unshift(bookmarkNode.title); 147 | await getParentFolders(bookmarkNode.parentId, folderNames); 148 | } 149 | } 150 | return folderNames; 151 | } 152 | 153 | function onChanged(id, changeInfo) { 154 | if (id == bookmarkId && changeInfo.url) { 155 | browser.bookmarks.onChanged.removeListener(onChanged); 156 | saveUrl(changeInfo.url); 157 | } 158 | } 159 | 160 | function saveUrl(url) { 161 | pendingSaves.delete(bookmarkId); 162 | business.saveUrls([url], { bookmarkId }); 163 | } 164 | } -------------------------------------------------------------------------------- /src/core/bg/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | import * as config from "./config.js"; 25 | import { autoSaveIsEnabled } from "./autosave-util.js"; 26 | 27 | export { 28 | onMessage 29 | }; 30 | 31 | async function onMessage(message, sender) { 32 | if (message.method.endsWith(".init")) { 33 | const [optionsAutoSave, options, autoSaveEnabled] = await Promise.all([config.getOptions(sender.tab.url, true), config.getOptions(sender.tab.url), autoSaveIsEnabled(sender.tab)]); 34 | return { optionsAutoSave, options, autoSaveEnabled, tabId: sender.tab.id, tabIndex: sender.tab.index }; 35 | } 36 | } -------------------------------------------------------------------------------- /src/core/bg/companion.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | let enabled = true; 27 | 28 | export { 29 | enabled, 30 | onMessage, 31 | externalSave, 32 | save 33 | }; 34 | 35 | async function onMessage(message) { 36 | if (message.method.endsWith(".state")) { 37 | return { enabled }; 38 | } 39 | } 40 | 41 | async function externalSave(pageData) { 42 | pageData.autoSaveExternalSave = false; 43 | let response; 44 | try { 45 | response = await browser.runtime.sendNativeMessage("singlefile_companion", { 46 | method: "externalSave", 47 | pageData 48 | }); 49 | } catch (error) { 50 | if (!error.message || !error.message.includes("Native host has exited")) { 51 | throw error; 52 | } 53 | } 54 | if (response && response.error) { 55 | throw new Error(response.error + " (Companion)"); 56 | } 57 | } 58 | 59 | async function save(pageData) { 60 | let response; 61 | try { 62 | response = await browser.runtime.sendNativeMessage("singlefile_companion", { 63 | method: "save", 64 | pageData 65 | }); 66 | } catch (error) { 67 | if (!error.message || !error.message.includes("Native host has exited")) { 68 | throw error; 69 | } 70 | } 71 | if (response && response.error) { 72 | throw new Error(response.error + " (Companion)"); 73 | } 74 | } -------------------------------------------------------------------------------- /src/core/bg/devtools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | export { 27 | onMessage 28 | }; 29 | 30 | async function onMessage(message) { 31 | if (message.method.endsWith(".resourceCommitted")) { 32 | if (message.tabId && message.url && (message.type == "stylesheet" || message.type == "script")) { 33 | await browser.tabs.sendMessage(message.tabId, message); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/core/bg/download-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | const STATE_DOWNLOAD_COMPLETE = "complete"; 27 | const STATE_DOWNLOAD_INTERRUPTED = "interrupted"; 28 | const STATE_ERROR_CANCELED_CHROMIUM = "USER_CANCELED"; 29 | const ERROR_DOWNLOAD_CANCELED_GECKO = "canceled"; 30 | const ERROR_CONFLICT_ACTION_GECKO = "conflictaction prompt not yet implemented"; 31 | const ERROR_INCOGNITO_GECKO = "'incognito'"; 32 | const ERROR_INCOGNITO_GECKO_ALT = "\"incognito\""; 33 | const ERROR_INVALID_FILENAME_GECKO = "illegal characters"; 34 | const ERROR_INVALID_FILENAME_CHROMIUM = "invalid filename"; 35 | 36 | export { 37 | download 38 | }; 39 | 40 | async function download(downloadInfo, replacementCharacter) { 41 | let downloadId; 42 | const result = new Promise((resolve, reject) => { 43 | browser.downloads.onChanged.addListener(onChanged); 44 | 45 | function onChanged(event) { 46 | if (event.id == downloadId && event.state) { 47 | if (event.state.current == STATE_DOWNLOAD_COMPLETE) { 48 | browser.downloads.search({ id: downloadId }) 49 | .then(downloadItems => resolve({ filename: downloadItems[0] && downloadItems[0].filename })) 50 | .catch(() => resolve({})); 51 | browser.downloads.onChanged.removeListener(onChanged); 52 | } 53 | if (event.state.current == STATE_DOWNLOAD_INTERRUPTED) { 54 | if (event.error && event.error.current == STATE_ERROR_CANCELED_CHROMIUM) { 55 | resolve({ cancelled: true }); 56 | } else { 57 | reject(new Error(event.state.current)); 58 | } 59 | browser.downloads.onChanged.removeListener(onChanged); 60 | } 61 | } 62 | } 63 | }); 64 | try { 65 | downloadId = await browser.downloads.download(downloadInfo); 66 | } catch (error) { 67 | if (error.message) { 68 | const errorMessage = error.message.toLowerCase(); 69 | const invalidFilename = errorMessage.includes(ERROR_INVALID_FILENAME_GECKO) || errorMessage.includes(ERROR_INVALID_FILENAME_CHROMIUM); 70 | if (invalidFilename && downloadInfo.filename.startsWith(".")) { 71 | downloadInfo.filename = replacementCharacter + downloadInfo.filename; 72 | return download(downloadInfo, replacementCharacter); 73 | } else if (invalidFilename && downloadInfo.filename.includes(",")) { 74 | downloadInfo.filename = downloadInfo.filename.replace(/,/g, replacementCharacter); 75 | return download(downloadInfo, replacementCharacter); 76 | } else if (invalidFilename && downloadInfo.filename.match(/\u200C|\u200D|\u200E|\u200F/)) { 77 | downloadInfo.filename = downloadInfo.filename.replace(/\u200C|\u200D|\u200E|\u200F/g, replacementCharacter); 78 | return download(downloadInfo, replacementCharacter); 79 | } else if (invalidFilename && !downloadInfo.filename.match(/^[\x00-\x7F]+$/)) { // eslint-disable-line no-control-regex 80 | downloadInfo.filename = downloadInfo.filename.replace(/[^\x00-\x7F]+/g, replacementCharacter); // eslint-disable-line no-control-regex 81 | return download(downloadInfo, replacementCharacter); 82 | } else if ((errorMessage.includes(ERROR_INCOGNITO_GECKO) || errorMessage.includes(ERROR_INCOGNITO_GECKO_ALT)) && downloadInfo.incognito) { 83 | delete downloadInfo.incognito; 84 | return download(downloadInfo, replacementCharacter); 85 | } else if (errorMessage == ERROR_CONFLICT_ACTION_GECKO && downloadInfo.conflictAction) { 86 | delete downloadInfo.conflictAction; 87 | return download(downloadInfo, replacementCharacter); 88 | } else if (errorMessage.includes(ERROR_DOWNLOAD_CANCELED_GECKO)) { 89 | return { cancelled: true }; 90 | } else { 91 | throw error; 92 | } 93 | } else { 94 | throw error; 95 | } 96 | } 97 | return result; 98 | } -------------------------------------------------------------------------------- /src/core/bg/editor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | import * as config from "./config.js"; 27 | 28 | const MAX_CONTENT_SIZE = 32 * (1024 * 1024); 29 | const EDITOR_PAGE_URL = "/src/ui/pages/editor.html"; 30 | const tabsData = new Map(); 31 | const partialContents = new Map(); 32 | const EDITOR_URL = browser.runtime.getURL(EDITOR_PAGE_URL); 33 | 34 | export { 35 | onMessage, 36 | onTabRemoved, 37 | isEditor, 38 | open, 39 | EDITOR_URL 40 | }; 41 | 42 | async function open({ tabIndex, content, filename, compressContent, selfExtractingArchive, extractDataFromPage, insertTextBody, insertMetaCSP, embeddedImage }) { 43 | const createTabProperties = { active: true, url: EDITOR_PAGE_URL }; 44 | if (tabIndex != null) { 45 | createTabProperties.index = tabIndex; 46 | } 47 | const tab = await browser.tabs.create(createTabProperties); 48 | tabsData.set(tab.id, { 49 | content, 50 | filename, 51 | compressContent, 52 | selfExtractingArchive, 53 | extractDataFromPage, 54 | insertTextBody, 55 | insertMetaCSP, 56 | embeddedImage 57 | }); 58 | } 59 | 60 | function onTabRemoved(tabId) { 61 | tabsData.delete(tabId); 62 | } 63 | 64 | function isEditor(tab) { 65 | return tab.url == EDITOR_URL; 66 | } 67 | 68 | async function onMessage(message, sender) { 69 | if (message.method.endsWith(".getTabData")) { 70 | const tab = sender.tab; 71 | const tabData = tabsData.get(tab.id); 72 | if (tabData) { 73 | const options = await config.getOptions(tabData.url); 74 | const content = JSON.stringify(tabData); 75 | for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) { 76 | const message = { 77 | method: "editor.setTabData", 78 | compressContent: tabData.compressContent 79 | }; 80 | message.truncated = content.length > MAX_CONTENT_SIZE; 81 | if (message.truncated) { 82 | message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > content.length; 83 | message.content = content.substring(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE); 84 | if (message.finished) { 85 | message.options = options; 86 | } 87 | } else { 88 | message.content = content; 89 | options.embeddedImage = tabData.embeddedImage; 90 | message.options = options; 91 | } 92 | await browser.tabs.sendMessage(tab.id, message); 93 | } 94 | } 95 | return {}; 96 | } 97 | if (message.method.endsWith(".open")) { 98 | let contents; 99 | const tab = sender.tab; 100 | if (message.truncated) { 101 | contents = partialContents.get(tab.id); 102 | if (!contents) { 103 | contents = []; 104 | partialContents.set(tab.id, contents); 105 | } 106 | contents.push(message.content); 107 | if (message.finished) { 108 | partialContents.delete(tab.id); 109 | } 110 | } else if (message.content) { 111 | contents = [message.content]; 112 | } 113 | if (!message.truncated || message.finished) { 114 | const updateTabProperties = { url: EDITOR_PAGE_URL }; 115 | await browser.tabs.update(tab.id, updateTabProperties); 116 | const content = message.compressContent ? contents.flat() : contents.join(""); 117 | tabsData.set(tab.id, { 118 | url: tab.url, 119 | content, 120 | filename: message.filename, 121 | compressContent: message.compressContent, 122 | selfExtractingArchive: message.selfExtractingArchive, 123 | extractDataFromPageTags: message.extractDataFromPageTags, 124 | insertTextBody: message.insertTextBody, 125 | insertMetaCSP: message.insertMetaCSP, 126 | embeddedImage: message.embeddedImage 127 | }); 128 | } 129 | return {}; 130 | } 131 | } -------------------------------------------------------------------------------- /src/core/bg/external-messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | import * as autosave from "./autosave.js"; 27 | import * as business from "./business.js"; 28 | import "./../../lib/single-file/background.js"; 29 | 30 | const ACTION_SAVE_PAGE = "save-page"; 31 | const ACTION_EDIT_AND_SAVE_PAGE = "edit-and-save-page"; 32 | const ACTION_SAVE_SELECTED_LINKS = "save-selected-links"; 33 | const ACTION_SAVE_SELECTED = "save-selected-content"; 34 | const ACTION_SAVE_SELECTED_TABS = "save-selected-tabs"; 35 | const ACTION_SAVE_UNPINNED_TABS = "save-unpinned-tabs"; 36 | const ACTION_SAVE_ALL_TABS = "save-all-tabs"; 37 | 38 | export { onMessage }; 39 | 40 | async function onMessage(message, sender) { 41 | if (message == ACTION_SAVE_PAGE) { 42 | const tabs = await browser.tabs.query({ currentWindow: true, active: true }); 43 | tabs.length = 1; 44 | await business.saveTabs(tabs); 45 | } else if (message == ACTION_EDIT_AND_SAVE_PAGE) { 46 | const tabs = await browser.tabs.query({ currentWindow: true, active: true }); 47 | tabs.length = 1; 48 | await business.saveTabs(tabs, { openEditor: true }); 49 | } else if (message == ACTION_SAVE_SELECTED_LINKS) { 50 | const tabs = await browser.tabs.query({ currentWindow: true, active: true }); 51 | await business.saveSelectedLinks(tabs[0]); 52 | } else if (message == ACTION_SAVE_SELECTED) { 53 | const tabs = await browser.tabs.query({ currentWindow: true, active: true }); 54 | await business.saveTabs(tabs, { selected: true }); 55 | } else if (message == ACTION_SAVE_SELECTED_TABS) { 56 | const tabs = await queryTabs({ currentWindow: true, highlighted: true }); 57 | await business.saveTabs(tabs); 58 | } else if (message == ACTION_SAVE_UNPINNED_TABS) { 59 | const tabs = await queryTabs({ currentWindow: true, pinned: false }); 60 | await business.saveTabs(tabs); 61 | } else if (message == ACTION_SAVE_ALL_TABS) { 62 | const tabs = await queryTabs({ currentWindow: true }); 63 | await business.saveTabs(tabs); 64 | } else if (message.method) { 65 | const tabs = await browser.tabs.query({ currentWindow: true, active: true }); 66 | const currentTab = tabs[0]; 67 | if (currentTab) { 68 | return autosave.onMessageExternal(message, currentTab, sender); 69 | } else { 70 | return false; 71 | } 72 | } 73 | } 74 | 75 | async function queryTabs(options) { 76 | const tabs = await browser.tabs.query(options); 77 | return tabs.sort((tab1, tab2) => tab1.index - tab2.index); 78 | } 79 | -------------------------------------------------------------------------------- /src/core/bg/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | import * as config from "./config.js"; 27 | import * as bootstrap from "./bootstrap.js"; 28 | import * as autosave from "./autosave.js"; 29 | import * as bookmarks from "./bookmarks.js"; 30 | import * as companion from "./companion.js"; 31 | import * as devtools from "./devtools.js"; 32 | import * as downloads from "./downloads.js"; 33 | import * as editor from "./editor.js"; 34 | import * as requests from "./requests.js"; 35 | import * as tabsData from "./tabs-data.js"; 36 | import * as tabs from "./tabs.js"; 37 | import * as externalMesssages from "./external-messages.js"; 38 | import * as ui from "./../../ui/bg/index.js"; 39 | import "./../../lib/single-file/background.js"; 40 | 41 | browser.runtime.onMessage.addListener((message, sender) => { 42 | if (message.method.startsWith("tabs.")) { 43 | return tabs.onMessage(message, sender); 44 | } 45 | if (message.method.startsWith("downloads.")) { 46 | return downloads.onMessage(message, sender); 47 | } 48 | if (message.method.startsWith("autosave.")) { 49 | return autosave.onMessage(message, sender); 50 | } 51 | if (message.method.startsWith("ui.")) { 52 | return ui.onMessage(message, sender); 53 | } 54 | if (message.method.startsWith("config.")) { 55 | return config.onMessage(message, sender); 56 | } 57 | if (message.method.startsWith("tabsData.")) { 58 | return tabsData.onMessage(message, sender); 59 | } 60 | if (message.method.startsWith("devtools.")) { 61 | return devtools.onMessage(message, sender); 62 | } 63 | if (message.method.startsWith("editor.")) { 64 | return editor.onMessage(message, sender); 65 | } 66 | if (message.method.startsWith("bookmarks.")) { 67 | return bookmarks.onMessage(message, sender); 68 | } 69 | if (message.method.startsWith("companion.")) { 70 | return companion.onMessage(message, sender); 71 | } 72 | if (message.method.startsWith("requests.")) { 73 | return requests.onMessage(message, sender); 74 | } 75 | if (message.method.startsWith("bootstrap.")) { 76 | return bootstrap.onMessage(message, sender); 77 | } 78 | if (message.method == "ping") { 79 | return Promise.resolve({}); 80 | } 81 | }); 82 | 83 | if (browser.runtime.onMessageExternal) { 84 | browser.runtime.onMessageExternal.addListener(externalMesssages.onMessage); 85 | } -------------------------------------------------------------------------------- /src/core/bg/requests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | import { 27 | REQUEST_ID_HEADER_NAME, 28 | referrers 29 | } from "../../lib/single-file/fetch/bg/fetch.js"; 30 | 31 | let referrerOnErrorEnabled = false; 32 | 33 | export { 34 | onMessage, 35 | enableReferrerOnError 36 | }; 37 | 38 | function onMessage(message) { 39 | if (message.method.endsWith(".enableReferrerOnError")) { 40 | enableReferrerOnError(); 41 | return {}; 42 | } 43 | if (message.method.endsWith(".disableReferrerOnError")) { 44 | disableReferrerOnError(); 45 | return {}; 46 | } 47 | } 48 | 49 | function injectRefererHeader(details) { 50 | if (referrerOnErrorEnabled) { 51 | let requestIdHeader = details.requestHeaders.find(header => header.name === REQUEST_ID_HEADER_NAME); 52 | if (requestIdHeader) { 53 | details.requestHeaders = details.requestHeaders.filter(header => header.name !== REQUEST_ID_HEADER_NAME); 54 | const referrer = referrers.get(requestIdHeader.value); 55 | if (referrer) { 56 | referrers.delete(requestIdHeader.value); 57 | const header = details.requestHeaders.find(header => header.name.toLowerCase() === "referer"); 58 | if (!header) { 59 | details.requestHeaders.push({ name: "Referer", value: referrer }); 60 | return { requestHeaders: details.requestHeaders }; 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | function enableReferrerOnError() { 68 | if (!referrerOnErrorEnabled) { 69 | try { 70 | browser.webRequest.onBeforeSendHeaders.addListener(injectRefererHeader, { urls: [""] }, ["blocking", "requestHeaders", "extraHeaders"]); 71 | // eslint-disable-next-line no-unused-vars 72 | } catch (error) { 73 | browser.webRequest.onBeforeSendHeaders.addListener(injectRefererHeader, { urls: [""] }, ["blocking", "requestHeaders"]); 74 | } 75 | referrerOnErrorEnabled = true; 76 | } 77 | } 78 | 79 | function disableReferrerOnError() { 80 | try { 81 | browser.webRequest.onBeforeSendHeaders.removeListener(injectRefererHeader); 82 | // eslint-disable-next-line no-unused-vars 83 | } catch (error) { 84 | // ignored 85 | } 86 | referrerOnErrorEnabled = false; 87 | } -------------------------------------------------------------------------------- /src/core/bg/tabs-data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, setTimeout */ 25 | 26 | let persistentData, temporaryData, cleanedUp; 27 | setTimeout(() => getPersistent().then(tabsData => persistentData = tabsData), 0); 28 | export { 29 | onMessage, 30 | getTemporary, 31 | getPersistent as get, 32 | setPersistent as set, 33 | onTabReplaced, 34 | remove 35 | }; 36 | 37 | function onMessage(message) { 38 | if (message.method.endsWith(".get")) { 39 | return getPersistent(); 40 | } 41 | if (message.method.endsWith(".set")) { 42 | return setPersistent(message.tabsData); 43 | } 44 | } 45 | 46 | async function onTabReplaced(addedTabId, removedTabId) { 47 | let tabsData = await getPersistent(); 48 | await updateTabsData(tabsData, addedTabId, removedTabId); 49 | setPersistent(tabsData); 50 | await updateTabsData(temporaryData, addedTabId, removedTabId); 51 | } 52 | 53 | async function updateTabsData(tabsData, addedTabId, removedTabId) { 54 | if (tabsData[removedTabId] && !tabsData[addedTabId]) { 55 | tabsData[addedTabId] = tabsData[removedTabId]; 56 | delete tabsData[removedTabId]; 57 | } 58 | } 59 | 60 | async function remove(tabId) { 61 | if (temporaryData) { 62 | delete temporaryData[tabId]; 63 | } 64 | const tabsData = await getPersistent(); 65 | if (tabsData[tabId]) { 66 | const autoSave = tabsData[tabId].autoSave; 67 | tabsData[tabId] = { autoSave }; 68 | await setPersistent(tabsData); 69 | } 70 | } 71 | 72 | function getTemporary(desiredTabId) { 73 | if (!temporaryData) { 74 | temporaryData = {}; 75 | } 76 | if (desiredTabId !== undefined && !temporaryData[desiredTabId]) { 77 | temporaryData[desiredTabId] = {}; 78 | } 79 | return temporaryData; 80 | } 81 | 82 | async function getPersistent(desiredTabId) { 83 | if (!persistentData) { 84 | const config = await browser.storage.local.get(); 85 | persistentData = config.tabsData || {}; 86 | } 87 | cleanup(); 88 | if (desiredTabId !== undefined && !persistentData[desiredTabId]) { 89 | persistentData[desiredTabId] = {}; 90 | } 91 | return persistentData; 92 | } 93 | 94 | async function setPersistent(tabsData) { 95 | persistentData = tabsData; 96 | await browser.storage.local.set({ tabsData }); 97 | } 98 | 99 | async function cleanup() { 100 | if (!cleanedUp) { 101 | cleanedUp = true; 102 | const tabs = await browser.tabs.query({ currentWindow: true, highlighted: true }); 103 | Object.keys(persistentData).filter(key => { 104 | if (key != "autoSaveAll" && key != "autoSaveUnpinned" && key != "profileName") { 105 | return !tabs.find(tab => tab.id == key); 106 | } 107 | }).forEach(tabId => delete persistentData[tabId]); 108 | await browser.storage.local.set({ tabsData: persistentData }); 109 | } 110 | } -------------------------------------------------------------------------------- /src/core/bg/tabs-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, URLSearchParams, URL */ 25 | 26 | export { 27 | queryTabs, 28 | extractAuthCode, 29 | launchWebAuthFlow 30 | }; 31 | 32 | async function queryTabs(options) { 33 | const tabs = await browser.tabs.query(options); 34 | return tabs.sort((tab1, tab2) => tab1.index - tab2.index); 35 | } 36 | 37 | function extractAuthCode(authURL) { 38 | return new Promise((resolve, reject) => { 39 | browser.tabs.onUpdated.addListener(onTabUpdated); 40 | 41 | function onTabUpdated(tabId, changeInfo) { 42 | if (changeInfo && changeInfo.url && changeInfo.url.startsWith(authURL)) { 43 | browser.tabs.onUpdated.removeListener(onTabUpdated); 44 | const code = new URLSearchParams(new URL(changeInfo.url).search).get("code"); 45 | if (code) { 46 | browser.tabs.remove(tabId); 47 | resolve(code); 48 | } else { 49 | reject(); 50 | } 51 | } 52 | } 53 | }); 54 | } 55 | 56 | async function launchWebAuthFlow(options) { 57 | const tab = await browser.tabs.create({ url: options.url, active: true }); 58 | return new Promise((resolve, reject) => { 59 | browser.tabs.onRemoved.addListener(onTabRemoved); 60 | function onTabRemoved(tabId) { 61 | if (tabId == tab.id) { 62 | browser.tabs.onRemoved.removeListener(onTabRemoved); 63 | reject(new Error("code_required")); 64 | } 65 | } 66 | }); 67 | } -------------------------------------------------------------------------------- /src/core/bg/tabs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, setTimeout, OffscreenCanvas, Image, URL */ 25 | 26 | import * as config from "./config.js"; 27 | import * as autosave from "./autosave.js"; 28 | import * as business from "./business.js"; 29 | import * as editor from "./editor.js"; 30 | import * as tabsData from "./tabs-data.js"; 31 | import * as ui from "./../../ui/bg/index.js"; 32 | 33 | const DELAY_MAYBE_INIT = 1500; 34 | 35 | browser.tabs.onCreated.addListener(tab => onTabCreated(tab)); 36 | browser.tabs.onActivated.addListener(activeInfo => onTabActivated(activeInfo)); 37 | browser.tabs.onRemoved.addListener(tabId => onTabRemoved(tabId)); 38 | browser.tabs.onUpdated.addListener((tabId, changeInfo) => onTabUpdated(tabId, changeInfo)); 39 | browser.tabs.onReplaced.addListener((addedTabId, removedTabId) => onTabReplaced(addedTabId, removedTabId)); 40 | export { 41 | onMessage 42 | }; 43 | 44 | async function onMessage(message, sender) { 45 | if (message.method.endsWith(".init")) { 46 | await onInit(sender.tab, message); 47 | ui.onInit(sender.tab); 48 | business.onInit(sender.tab); 49 | autosave.onInit(sender.tab); 50 | } 51 | if (message.method.endsWith(".getOptions")) { 52 | return config.getOptions(message.url); 53 | } 54 | if (message.method.endsWith(".activate")) { 55 | await browser.tabs.update(message.tabId, { active: true }); 56 | } 57 | if (message.method.endsWith(".getScreenshot")) { 58 | return captureTab(sender.tab.id, message); 59 | } 60 | } 61 | 62 | async function onInit(tab, options) { 63 | await tabsData.remove(tab.id); 64 | const allTabsData = await tabsData.get(tab.id); 65 | allTabsData[tab.id].savedPageDetected = options.savedPageDetected; 66 | await tabsData.set(allTabsData); 67 | } 68 | 69 | async function onTabUpdated(tabId, changeInfo) { 70 | if (changeInfo.status == "complete") { 71 | setTimeout(async () => { 72 | try { 73 | await browser.tabs.sendMessage(tabId, { method: "content.maybeInit" }); 74 | } 75 | // eslint-disable-next-line no-unused-vars 76 | catch (error) { 77 | // ignored 78 | } 79 | }, DELAY_MAYBE_INIT); 80 | autosave.onTabUpdated(tabId); 81 | const tab = await browser.tabs.get(tabId); 82 | if (editor.isEditor(tab)) { 83 | const allTabsData = await tabsData.get(tab.id); 84 | allTabsData[tab.id].editorDetected = true; 85 | await tabsData.set(allTabsData); 86 | ui.onTabActivated(tab); 87 | } 88 | } 89 | if (changeInfo.discarded) { 90 | autosave.onTabDiscarded(tabId); 91 | } 92 | } 93 | 94 | function onTabReplaced(addedTabId, removedTabId) { 95 | tabsData.onTabReplaced(addedTabId, removedTabId); 96 | autosave.onTabReplaced(addedTabId, removedTabId); 97 | business.onTabReplaced(addedTabId, removedTabId); 98 | } 99 | 100 | function onTabCreated(tab) { 101 | ui.onTabCreated(tab); 102 | } 103 | 104 | async function onTabActivated(activeInfo) { 105 | const tab = await browser.tabs.get(activeInfo.tabId); 106 | ui.onTabActivated(tab); 107 | } 108 | 109 | function onTabRemoved(tabId) { 110 | business.cancel(tabId); 111 | tabsData.remove(tabId); 112 | editor.onTabRemoved(tabId); 113 | autosave.onTabRemoved(tabId); 114 | } 115 | 116 | async function captureTab(tabId, options) { 117 | const { width, height, scale = 1 } = options; 118 | const canvasWidth = Math.floor(width * scale); 119 | const canvasHeight = Math.floor(height * scale); 120 | let y = 0, canvas, canvasY = 0, scrollYStep, activeTabId; 121 | if (browser.tabs.captureTab) { 122 | scrollYStep = 4 * 1024; 123 | } else { 124 | scrollYStep = options.innerHeight; 125 | activeTabId = (await browser.tabs.query({ active: true, currentWindow: true }))[0].id; 126 | } 127 | const canvasScrollStep = Math.floor(scrollYStep * scale); 128 | await browser.tabs.sendMessage(tabId, { method: "content.beginScrollTo" }); 129 | try { 130 | canvas = new OffscreenCanvas(canvasWidth, canvasHeight); 131 | const context = canvas.getContext("2d"); 132 | while (y < height) { 133 | let imageSrc; 134 | if (browser.tabs.captureTab) { 135 | imageSrc = await browser.tabs.captureTab(tabId, { 136 | format: "png", 137 | rect: { x: 0, y, width, height: Math.min(height - y, scrollYStep) } 138 | }); 139 | } else { 140 | await browser.tabs.sendMessage(tabId, { method: "content.scrollTo", y }); 141 | await browser.tabs.update(tabId, { active: true }); 142 | imageSrc = await browser.tabs.captureVisibleTab(null, { 143 | format: "png" 144 | }); 145 | } 146 | const image = new Image(); 147 | await new Promise((resolve, reject) => { 148 | image.onload = resolve; 149 | image.onerror = event => reject(new Error(event.detail)); 150 | image.src = imageSrc; 151 | }); 152 | const imageHeight = Math.min(canvasHeight - canvasY, canvasScrollStep); 153 | context.drawImage(image, 0, canvasY, canvasWidth, imageHeight); 154 | y += scrollYStep; 155 | canvasY += canvasScrollStep; 156 | } 157 | if (!browser.tabs.captureTab) { 158 | await browser.tabs.update(activeTabId, { active: true }); 159 | } 160 | } catch (error) { 161 | if (scale > .1) { 162 | options.scale = scale * .75; 163 | return captureTab(tabId, options); 164 | } else { 165 | throw error; 166 | } 167 | } finally { 168 | await browser.tabs.sendMessage(tabId, { method: "content.endScrollTo" }); 169 | } 170 | if (canvas) { 171 | return URL.createObjectURL(await canvas.convertToBlob({ type: "image/png" })); 172 | } 173 | } -------------------------------------------------------------------------------- /src/core/content/content-frames.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | import "./../../lib/single-file/core/content/content-hooks-frames-inline-injection.js"; 25 | import "./../../lib/single-file/core/content/content-hooks-frames-extension-injection.js"; 26 | import "./../../lib/single-file/fetch/content/content-fetch.js"; -------------------------------------------------------------------------------- /src/core/devtools/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DevTools page 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/core/devtools/devtools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | if (browser.devtools.inspectedWindow && browser.devtools.inspectedWindow.onResourceContentCommitted) { 27 | browser.devtools.inspectedWindow.onResourceContentCommitted.addListener(resource => { 28 | resource.getContent((content, encoding) => { 29 | browser.runtime.sendMessage({ 30 | method: "devtools.resourceCommitted", 31 | tabId: browser.devtools.inspectedWindow.tabId, 32 | url: resource.url, 33 | content, 34 | encoding, 35 | type: resource.type 36 | }); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | import * as scripts from "./lib/single-file/core/bg/scripts.js"; 25 | import { fetch, frameFetch } from "./lib/single-file/fetch/content/content-fetch.js"; 26 | 27 | export { 28 | injectScript, 29 | getPageData 30 | }; 31 | 32 | function injectScript(tabId, options) { 33 | return scripts.inject(tabId, options); 34 | } 35 | 36 | function getPageData(options, initOptions = { fetch, frameFetch }, doc, win) { 37 | return globalThis.singlefile.getPageData(options, initOptions, doc, win); 38 | } -------------------------------------------------------------------------------- /src/lib/github/github.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global fetch, btoa, Blob, FileReader, AbortController */ 25 | 26 | const EMPTY_STRING = ""; 27 | const CONFLICT_ACTION_SKIP = "skip"; 28 | const CONFLICT_ACTION_UNIQUIFY = "uniquify"; 29 | const CONFLICT_ACTION_OVERWRITE = "overwrite"; 30 | const CONFLICT_ACTION_PROMPT = "prompt"; 31 | const AUTHORIZATION_HEADER = "Authorization"; 32 | const BEARER_PREFIX_AUTHORIZATION = "Bearer "; 33 | const ACCEPT_HEADER = "Accept"; 34 | const GITHUB_API_CONTENT_TYPE = "application/vnd.github+json"; 35 | const GITHUB_API_VERSION_HEADER = "X-GitHub-Api-Version"; 36 | const GITHUB_API_VERSION = "2022-11-28"; 37 | const EXTENSION_SEPARATOR = "."; 38 | const INDEX_FILENAME_PREFIX = " ("; 39 | const INDEX_FILENAME_SUFFIX = ")"; 40 | const INDEX_FILENAME_REGEXP = /\s\((\d+)\)$/; 41 | const ABORT_ERROR_NAME = "AbortError"; 42 | const GET_METHOD = "GET"; 43 | const PUT_METHOD = "PUT"; 44 | const GITHUB_URL = "https://github.com"; 45 | const GITHUB_API_URL = "https://api.github.com"; 46 | const BLOB_PATH = "blob"; 47 | const REPOS_PATH = "repos"; 48 | const CONTENTS_PATH = "contents"; 49 | 50 | export { GitHub }; 51 | 52 | let pendingPush; 53 | 54 | class GitHub { 55 | constructor(token, userName, repositoryName, branch) { 56 | this.headers = new Map([ 57 | [AUTHORIZATION_HEADER, BEARER_PREFIX_AUTHORIZATION + token], 58 | [ACCEPT_HEADER, GITHUB_API_CONTENT_TYPE], 59 | [GITHUB_API_VERSION_HEADER, GITHUB_API_VERSION] 60 | ]); 61 | this.userName = userName; 62 | this.repositoryName = repositoryName; 63 | this.branch = branch; 64 | } 65 | 66 | async upload(path, content, options) { 67 | this.controller = new AbortController(); 68 | options.signal = this.controller.signal; 69 | options.headers = this.headers; 70 | const base64Content = content instanceof Blob ? await blobToBase64(content) : btoa(unescape(encodeURIComponent(content))); 71 | return upload(this.userName, this.repositoryName, this.branch, path, base64Content, options); 72 | } 73 | 74 | abort() { 75 | if (this.controller) { 76 | this.controller.abort(); 77 | } 78 | } 79 | } 80 | 81 | async function upload(userName, repositoryName, branch, path, content, options) { 82 | const { filenameConflictAction, prompt, signal, headers } = options; 83 | while (pendingPush) { 84 | await pendingPush; 85 | } 86 | try { 87 | pendingPush = await createContent({ path, content }); 88 | } finally { 89 | pendingPush = null; 90 | } 91 | return { 92 | url: `${GITHUB_URL}/${userName}/${repositoryName}/${BLOB_PATH}/${branch}/${path}` 93 | }; 94 | 95 | async function createContent({ path, content, message = EMPTY_STRING, sha }) { 96 | try { 97 | const response = await fetchContentData(PUT_METHOD, JSON.stringify({ 98 | content, 99 | message, 100 | branch, 101 | sha 102 | })); 103 | const responseData = await response.json(); 104 | if (response.status == 422) { 105 | if (filenameConflictAction == CONFLICT_ACTION_OVERWRITE) { 106 | const response = await fetchContentData(GET_METHOD); 107 | const responseData = await response.json(); 108 | const sha = responseData.sha; 109 | return await createContent({ path, content, message, sha }); 110 | } else if (filenameConflictAction == CONFLICT_ACTION_UNIQUIFY) { 111 | const { filenameWithoutExtension, extension, indexFilename } = splitFilename(path); 112 | options.indexFilename = indexFilename + 1; 113 | path = getFilename(filenameWithoutExtension, extension); 114 | return await createContent({ path, content, message }); 115 | } else if (filenameConflictAction == CONFLICT_ACTION_SKIP) { 116 | return responseData; 117 | } else if (filenameConflictAction == CONFLICT_ACTION_PROMPT) { 118 | if (prompt) { 119 | path = await prompt(path); 120 | if (path) { 121 | return await createContent({ path, content, message }); 122 | } else { 123 | return responseData; 124 | } 125 | } else { 126 | options.filenameConflictAction = CONFLICT_ACTION_UNIQUIFY; 127 | return await createContent({ path, content, message }); 128 | } 129 | } 130 | } 131 | if (response.status < 400) { 132 | return responseData; 133 | } else { 134 | throw new Error(responseData.message); 135 | } 136 | } catch (error) { 137 | if (error.name != ABORT_ERROR_NAME) { 138 | throw error; 139 | } 140 | } 141 | 142 | function fetchContentData(method, body) { 143 | return fetch(`${GITHUB_API_URL}/${REPOS_PATH}/${userName}/${repositoryName}/${CONTENTS_PATH}/${path}`, { 144 | method, 145 | headers, 146 | body, 147 | signal 148 | }); 149 | } 150 | } 151 | 152 | function splitFilename(filename) { 153 | let filenameWithoutExtension = filename; 154 | let extension = EMPTY_STRING; 155 | const indexExtensionSeparator = filename.lastIndexOf(EXTENSION_SEPARATOR); 156 | if (indexExtensionSeparator > -1) { 157 | filenameWithoutExtension = filename.substring(0, indexExtensionSeparator); 158 | extension = filename.substring(indexExtensionSeparator + 1); 159 | } 160 | let indexFilename; 161 | ({ filenameWithoutExtension, indexFilename } = extractIndexFilename(filenameWithoutExtension)); 162 | return { filenameWithoutExtension, extension, indexFilename }; 163 | } 164 | 165 | function extractIndexFilename(filenameWithoutExtension) { 166 | const indexFilenameMatch = filenameWithoutExtension.match(INDEX_FILENAME_REGEXP); 167 | let indexFilename = 0; 168 | if (indexFilenameMatch && indexFilenameMatch.length > 1) { 169 | const parsedIndexFilename = Number(indexFilenameMatch[indexFilenameMatch.length - 1]); 170 | if (!Number.isNaN(parsedIndexFilename)) { 171 | indexFilename = parsedIndexFilename; 172 | filenameWithoutExtension = filenameWithoutExtension.replace(INDEX_FILENAME_REGEXP, EMPTY_STRING); 173 | } 174 | } 175 | return { filenameWithoutExtension, indexFilename }; 176 | } 177 | 178 | function getFilename(filenameWithoutExtension, extension) { 179 | return filenameWithoutExtension + 180 | INDEX_FILENAME_PREFIX + options.indexFilename + INDEX_FILENAME_SUFFIX + 181 | (extension ? EXTENSION_SEPARATOR + extension : EMPTY_STRING); 182 | } 183 | } 184 | 185 | function blobToBase64(blob) { 186 | return new Promise((resolve, reject) => { 187 | const reader = new FileReader(); 188 | reader.onloadend = () => resolve(reader.result.match(/^data:[^,]+,(.*)$/)[1]); 189 | reader.onerror = event => reject(event.detail); 190 | reader.readAsDataURL(blob); 191 | }); 192 | } -------------------------------------------------------------------------------- /src/lib/readability/Readability-readerable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010 Arc90 Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * This code is heavily based on Arc90's readability.js (1.7.1) script 19 | * available at: http://code.google.com/p/arc90labs-readability 20 | */ 21 | 22 | var REGEXPS = { 23 | // NOTE: These two regular expressions are duplicated in 24 | // Readability.js. Please keep both copies in sync. 25 | unlikelyCandidates: 26 | /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i, 27 | okMaybeItsACandidate: /and|article|body|column|content|main|mathjax|shadow/i, 28 | }; 29 | 30 | function isNodeVisible(node) { 31 | // Have to null-check node.style and node.className.includes to deal with SVG and MathML nodes. 32 | return ( 33 | (!node.style || node.style.display != "none") && 34 | !node.hasAttribute("hidden") && 35 | //check for "fallback-image" so that wikimedia math images are displayed 36 | (!node.hasAttribute("aria-hidden") || 37 | node.getAttribute("aria-hidden") != "true" || 38 | (node.className && 39 | node.className.includes && 40 | node.className.includes("fallback-image"))) 41 | ); 42 | } 43 | 44 | /** 45 | * Decides whether or not the document is reader-able without parsing the whole thing. 46 | * @param {Object} options Configuration object. 47 | * @param {number} [options.minContentLength=140] The minimum node content length used to decide if the document is readerable. 48 | * @param {number} [options.minScore=20] The minumum cumulated 'score' used to determine if the document is readerable. 49 | * @param {Function} [options.visibilityChecker=isNodeVisible] The function used to determine if a node is visible. 50 | * @return {boolean} Whether or not we suspect Readability.parse() will suceeed at returning an article object. 51 | */ 52 | function isProbablyReaderable(doc, options = {}) { 53 | // For backward compatibility reasons 'options' can either be a configuration object or the function used 54 | // to determine if a node is visible. 55 | if (typeof options == "function") { 56 | options = { visibilityChecker: options }; 57 | } 58 | 59 | var defaultOptions = { 60 | minScore: 20, 61 | minContentLength: 140, 62 | visibilityChecker: isNodeVisible, 63 | }; 64 | options = Object.assign(defaultOptions, options); 65 | 66 | var nodes = doc.querySelectorAll("p, pre, article"); 67 | 68 | // Get
nodes which have
node(s) and append them into the `nodes` variable. 69 | // Some articles' DOM structures might look like 70 | //
71 | // Sentences
72 | //
73 | // Sentences
74 | //
75 | var brNodes = doc.querySelectorAll("div > br"); 76 | if (brNodes.length) { 77 | var set = new Set(nodes); 78 | [].forEach.call(brNodes, function (node) { 79 | set.add(node.parentNode); 80 | }); 81 | nodes = Array.from(set); 82 | } 83 | 84 | var score = 0; 85 | // This is a little cheeky, we use the accumulator 'score' to decide what to return from 86 | // this callback: 87 | return [].some.call(nodes, function (node) { 88 | if (!options.visibilityChecker(node)) { 89 | return false; 90 | } 91 | 92 | var matchString = node.className + " " + node.id; 93 | if ( 94 | REGEXPS.unlikelyCandidates.test(matchString) && 95 | !REGEXPS.okMaybeItsACandidate.test(matchString) 96 | ) { 97 | return false; 98 | } 99 | 100 | if (node.matches("li p")) { 101 | return false; 102 | } 103 | 104 | var textContentLength = node.textContent.trim().length; 105 | if (textContentLength < options.minContentLength) { 106 | return false; 107 | } 108 | 109 | score += Math.sqrt(textContentLength - options.minContentLength); 110 | 111 | if (score > options.minScore) { 112 | return true; 113 | } 114 | return false; 115 | }); 116 | } 117 | 118 | if (typeof module === "object") { 119 | /* eslint-disable-next-line no-redeclare */ 120 | /* global module */ 121 | module.exports = isProbablyReaderable; 122 | } 123 | -------------------------------------------------------------------------------- /src/lib/rest-form-api/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2024 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * author: gildas.lormeau gmail.com 5 | * author: dcardin2007 gmail.com 6 | * 7 | * This file is part of SingleFile. 8 | * 9 | * The code in this file is free software: you can redistribute it and/or 10 | * modify it under the terms of the GNU Affero General Public License 11 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 12 | * of the License, or (at your option) any later version. 13 | * 14 | * The code in this file is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 17 | * General Public License for more details. 18 | * 19 | * As additional permission under GNU AGPL version 3 section 7, you may 20 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 21 | * AGPL normally required by section 4, provided you include this license 22 | * notice and a URL through which recipients can access the Corresponding 23 | * Source. 24 | */ 25 | 26 | /* global fetch, Blob, AbortController, FormData */ 27 | 28 | const AUTHORIZATION_HEADER = "Authorization"; 29 | const BEARER_PREFIX_AUTHORIZATION = "Bearer "; 30 | const ACCEPT_HEADER = "Accept"; 31 | const CONTENT_TYPE = "application/json"; 32 | 33 | export { RestFormApi }; 34 | 35 | class RestFormApi { 36 | constructor(token, restApiUrl, fileFieldName, urlFieldName) { 37 | this.headers = new Map([ 38 | [AUTHORIZATION_HEADER, BEARER_PREFIX_AUTHORIZATION + token], 39 | [ACCEPT_HEADER, CONTENT_TYPE] 40 | ]); 41 | this.restApiUrl = restApiUrl; 42 | this.fileFieldName = fileFieldName; 43 | this.urlFieldName = urlFieldName; 44 | } 45 | 46 | async upload(filename, content, url) { 47 | this.controller = new AbortController(); 48 | const blob = content instanceof Blob ? content : new Blob([content], { type: "text/html" }); 49 | let formData = new FormData(); 50 | if (this.fileFieldName) { 51 | formData.append(this.fileFieldName, blob, filename); 52 | } 53 | if (this.urlFieldName) { 54 | formData.append(this.urlFieldName, url); 55 | } 56 | const response = await fetch(this.restApiUrl, { 57 | method: "POST", 58 | body: formData, 59 | headers: this.headers, 60 | signal: this.controller.signal 61 | }); 62 | if ([200, 201].includes(response.status)) { 63 | return response.json(); 64 | } else { 65 | throw new Error(await response.text()); 66 | } 67 | } 68 | 69 | abort() { 70 | if (this.controller) { 71 | this.controller.abort(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/single-file/background.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | import "./fetch/bg/fetch.js"; 25 | import "./frame-tree/bg/frame-tree.js"; 26 | import "./lazy/bg/lazy-timeout.js"; -------------------------------------------------------------------------------- /src/lib/single-file/core/bg/scripts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, fetch, TextDecoder */ 25 | 26 | let contentScript, frameScript; 27 | 28 | const contentScriptFiles = [ 29 | "lib/web-stream.js", 30 | "lib/chrome-browser-polyfill.js", 31 | "lib/single-file.js" 32 | ]; 33 | 34 | const frameScriptFiles = [ 35 | "lib/chrome-browser-polyfill.js", 36 | "lib/single-file-frames.js" 37 | ]; 38 | 39 | const basePath = "../../../"; 40 | 41 | export { 42 | inject 43 | }; 44 | 45 | async function inject(tabId, options) { 46 | await initScripts(options); 47 | let scriptsInjected; 48 | if (!options.removeFrames) { 49 | try { 50 | await browser.tabs.executeScript(tabId, { code: frameScript, allFrames: true, matchAboutBlank: true, runAt: "document_start" }); 51 | // eslint-disable-next-line no-unused-vars 52 | } catch (error) { 53 | // ignored 54 | } 55 | } 56 | try { 57 | await browser.tabs.executeScript(tabId, { code: contentScript, allFrames: false, runAt: "document_idle" }); 58 | scriptsInjected = true; 59 | // eslint-disable-next-line no-unused-vars 60 | } catch (error) { 61 | // ignored 62 | } 63 | if (scriptsInjected) { 64 | if (options.frameId) { 65 | await browser.tabs.executeScript(tabId, { code: "document.documentElement.dataset.requestedFrameId = true", frameId: options.frameId, matchAboutBlank: true, runAt: "document_start" }); 66 | } 67 | } 68 | return scriptsInjected; 69 | } 70 | 71 | async function initScripts(options) { 72 | const extensionScriptFiles = options.extensionScriptFiles || []; 73 | if (!contentScript && !frameScript) { 74 | [contentScript, frameScript] = await Promise.all([ 75 | getScript(contentScriptFiles.concat(extensionScriptFiles)), 76 | getScript(frameScriptFiles) 77 | ]); 78 | } 79 | } 80 | 81 | async function getScript(scriptFiles) { 82 | const scriptsPromises = scriptFiles.map(async scriptFile => { 83 | if (typeof scriptFile == "function") { 84 | return "(" + scriptFile.toString() + ")();"; 85 | } else { 86 | const scriptResource = await fetch(browser.runtime.getURL(basePath + scriptFile)); 87 | return new TextDecoder().decode(await scriptResource.arrayBuffer()); 88 | } 89 | }); 90 | let content = ""; 91 | for (const scriptPromise of scriptsPromises) { 92 | content += await scriptPromise; 93 | } 94 | return content; 95 | } -------------------------------------------------------------------------------- /src/lib/single-file/core/content/content-hooks-frames-extension-injection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | const browser = globalThis.browser; 25 | const document = globalThis.document; 26 | const Document = globalThis.Document; 27 | 28 | if (document instanceof Document && browser && browser.runtime && browser.runtime.getURL) { 29 | const scriptElement = document.createElement("script"); 30 | scriptElement.src = browser.runtime.getURL("/lib/single-file-hooks-frames.js"); 31 | scriptElement.async = false; 32 | (document.documentElement || document).appendChild(scriptElement); 33 | scriptElement.remove(); 34 | } -------------------------------------------------------------------------------- /src/lib/single-file/core/content/content-hooks-frames-inline-injection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global window */ 25 | 26 | const document = globalThis.document; 27 | const Document = globalThis.Document; 28 | 29 | if (document instanceof Document) { 30 | let scriptElement = document.createElement("script"); 31 | scriptElement.src = "data:," + "(" + injectedScript.toString() + ")()"; 32 | (document.documentElement || document).appendChild(scriptElement); 33 | scriptElement.remove(); 34 | scriptElement = document.createElement("script"); 35 | scriptElement.textContent = "(" + injectedScript.toString() + ")()"; 36 | (document.documentElement || document).appendChild(scriptElement); 37 | scriptElement.remove(); 38 | } 39 | 40 | function injectedScript() { 41 | if (typeof globalThis == "undefined") { 42 | window.globalThis = window; 43 | } 44 | const document = globalThis.document; 45 | const CustomEvent = globalThis.CustomEvent; 46 | const FileReader = globalThis.FileReader; 47 | const Blob = globalThis.Blob; 48 | const NEW_FONT_FACE_EVENT = "single-file-new-font-face"; 49 | const DELETE_FONT_EVENT = "single-file-delete-font"; 50 | const CLEAR_FONTS_EVENT = "single-file-clear-fonts"; 51 | const FONT_STYLE_PROPERTIES = { 52 | family: "font-family", 53 | style: "font-style", 54 | weight: "font-weight", 55 | stretch: "font-stretch", 56 | unicodeRange: "unicode-range", 57 | variant: "font-variant", 58 | featureSettings: "font-feature-settings" 59 | }; 60 | 61 | if (globalThis.FontFace) { 62 | const FontFace = globalThis.FontFace; 63 | globalThis.FontFace = function () { 64 | getDetailObject(...arguments).then(detail => document.dispatchEvent(new CustomEvent(NEW_FONT_FACE_EVENT, { detail }))); 65 | return new FontFace(...arguments); 66 | }; 67 | globalThis.FontFace.prototype = FontFace.prototype; 68 | globalThis.FontFace.toString = function () { return "function FontFace() { [native code] }"; }; 69 | const deleteFont = document.fonts.delete; 70 | document.fonts.delete = function (fontFace) { 71 | getDetailObject(fontFace.family).then(detail => document.dispatchEvent(new CustomEvent(DELETE_FONT_EVENT, { detail }))); 72 | return deleteFont.call(document.fonts, fontFace); 73 | }; 74 | document.fonts.delete.toString = function () { return "function delete() { [native code] }"; }; 75 | const clearFonts = document.fonts.clear; 76 | document.fonts.clear = function () { 77 | document.dispatchEvent(new CustomEvent(CLEAR_FONTS_EVENT)); 78 | return clearFonts.call(document.fonts); 79 | }; 80 | document.fonts.clear.toString = function () { return "function clear() { [native code] }"; }; 81 | } 82 | 83 | async function getDetailObject(fontFamily, src, descriptors) { 84 | const detail = {}; 85 | detail["font-family"] = fontFamily; 86 | detail.src = src; 87 | if (descriptors) { 88 | Object.keys(descriptors).forEach(descriptor => { 89 | if (FONT_STYLE_PROPERTIES[descriptor]) { 90 | detail[FONT_STYLE_PROPERTIES[descriptor]] = descriptors[descriptor]; 91 | } 92 | }); 93 | } 94 | return new Promise(resolve => { 95 | if (detail.src instanceof ArrayBuffer) { 96 | const reader = new FileReader(); 97 | reader.readAsDataURL(new Blob([detail.src])); 98 | reader.addEventListener("load", () => { 99 | detail.src = "url(" + reader.result + ")"; 100 | resolve(detail); 101 | }); 102 | } else { 103 | resolve(detail); 104 | } 105 | }); 106 | } 107 | } -------------------------------------------------------------------------------- /src/lib/single-file/fetch/bg/fetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, XMLHttpRequest */ 25 | 26 | const referrers = new Map(); 27 | const REQUEST_ID_HEADER_NAME = "x-single-file-request-id"; 28 | const MAX_CONTENT_SIZE = 8 * (1024 * 1024); 29 | 30 | export { 31 | REQUEST_ID_HEADER_NAME, 32 | referrers, 33 | fetchResource 34 | }; 35 | 36 | browser.runtime.onMessage.addListener((message, sender) => { 37 | if (message.method && message.method.startsWith("singlefile.fetch")) { 38 | return new Promise(resolve => { 39 | onRequest(message, sender) 40 | .then(resolve) 41 | .catch(error => resolve({ error: error && (error.message || error.toString()) })); 42 | }); 43 | } 44 | }); 45 | 46 | async function onRequest(message, sender) { 47 | if (message.method == "singlefile.fetch") { 48 | try { 49 | const response = await fetchResource(message.url, { referrer: message.referrer, headers: message.headers }); 50 | return sendResponse(sender.tab.id, message.requestId, response); 51 | } catch (error) { 52 | return sendResponse(sender.tab.id, message.requestId, { error: error.message, array: [] }); 53 | } 54 | } else if (message.method == "singlefile.fetchFrame") { 55 | return browser.tabs.sendMessage(sender.tab.id, message); 56 | } 57 | } 58 | 59 | async function sendResponse(tabId, requestId, response) { 60 | for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE <= response.array.length; blockIndex++) { 61 | const message = { 62 | method: "singlefile.fetchResponse", 63 | requestId, 64 | headers: response.headers, 65 | status: response.status, 66 | error: response.error 67 | }; 68 | message.truncated = response.array.length > MAX_CONTENT_SIZE; 69 | if (message.truncated) { 70 | message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > response.array.length; 71 | message.array = response.array.slice(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE); 72 | } else { 73 | message.array = response.array; 74 | } 75 | await browser.tabs.sendMessage(tabId, message); 76 | } 77 | return {}; 78 | } 79 | 80 | function fetchResource(url, options = {}, includeRequestId) { 81 | return new Promise((resolve, reject) => { 82 | const xhrRequest = new XMLHttpRequest(); 83 | xhrRequest.withCredentials = true; 84 | xhrRequest.responseType = "arraybuffer"; 85 | xhrRequest.onerror = event => reject(new Error(event.detail)); 86 | xhrRequest.onreadystatechange = () => { 87 | if (xhrRequest.readyState == XMLHttpRequest.DONE) { 88 | if (xhrRequest.status || xhrRequest.response.byteLength) { 89 | if ((xhrRequest.status == 401 || xhrRequest.status == 403 || xhrRequest.status == 404) && !includeRequestId) { 90 | fetchResource(url, options, true) 91 | .then(resolve) 92 | .catch(reject); 93 | } else { 94 | resolve({ 95 | arrayBuffer: xhrRequest.response, 96 | array: Array.from(new Uint8Array(xhrRequest.response)), 97 | headers: { "content-type": xhrRequest.getResponseHeader("Content-Type") }, 98 | status: xhrRequest.status 99 | }); 100 | } 101 | } else { 102 | reject(new Error("Empty response")); 103 | } 104 | } 105 | }; 106 | xhrRequest.open("GET", url, true); 107 | if (options.headers) { 108 | for (const entry of Object.entries(options.headers)) { 109 | xhrRequest.setRequestHeader(entry[0], entry[1]); 110 | } 111 | } 112 | if (includeRequestId) { 113 | const randomId = String(Math.random()).substring(2); 114 | setReferrer(randomId, options.referrer); 115 | xhrRequest.setRequestHeader(REQUEST_ID_HEADER_NAME, randomId); 116 | } 117 | xhrRequest.send(); 118 | }); 119 | } 120 | 121 | function setReferrer(requestId, referrer) { 122 | referrers.set(requestId, referrer); 123 | } -------------------------------------------------------------------------------- /src/lib/single-file/fetch/content/content-fetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, window, document, CustomEvent */ 25 | 26 | const FETCH_SUPPORTED_REQUEST_EVENT = "single-file-request-fetch-supported"; 27 | const FETCH_SUPPORTED_RESPONSE_EVENT = "single-file-response-fetch-supported"; 28 | const FETCH_REQUEST_EVENT = "single-file-request-fetch"; 29 | const FETCH_RESPONSE_EVENT = "single-file-response-fetch"; 30 | const ERR_HOST_FETCH = "Host fetch error (SingleFile)"; 31 | const USE_HOST_FETCH = Boolean(window.wrappedJSObject); 32 | 33 | const fetch = window.fetch.bind(window); 34 | 35 | let requestId = 0, pendingResponses = new Map(), hostFetchSupported; 36 | 37 | browser.runtime.onMessage.addListener(message => { 38 | if (message.method == "singlefile.fetchFrame" && window.frameId && window.frameId == message.frameId) { 39 | return onFetchFrame(message); 40 | } 41 | if (message.method == "singlefile.fetchResponse") { 42 | return onFetchResponse(message); 43 | } 44 | }); 45 | 46 | async function onFetchFrame(message) { 47 | try { 48 | const response = await fetch(message.url, { cache: "force-cache", headers: message.headers, referrerPolicy: "strict-origin-when-cross-origin" }); 49 | return { 50 | status: response.status, 51 | headers: [...response.headers], 52 | array: Array.from(new Uint8Array(await response.arrayBuffer())) 53 | }; 54 | } catch (error) { 55 | return { 56 | error: error && (error.message || error.toString()) 57 | }; 58 | } 59 | } 60 | 61 | async function onFetchResponse(message) { 62 | const pendingResponse = pendingResponses.get(message.requestId); 63 | if (pendingResponse) { 64 | if (message.error) { 65 | pendingResponse.reject(new Error(message.error)); 66 | pendingResponses.delete(message.requestId); 67 | } else { 68 | if (message.truncated) { 69 | if (pendingResponse.array) { 70 | pendingResponse.array = pendingResponse.array.concat(message.array); 71 | } else { 72 | pendingResponse.array = message.array; 73 | pendingResponses.set(message.requestId, pendingResponse); 74 | } 75 | if (message.finished) { 76 | message.array = pendingResponse.array; 77 | } 78 | } 79 | if (!message.truncated || message.finished) { 80 | pendingResponse.resolve({ 81 | status: message.status, 82 | headers: { get: headerName => message.headers && message.headers[headerName] }, 83 | arrayBuffer: async () => new Uint8Array(message.array).buffer 84 | }); 85 | pendingResponses.delete(message.requestId); 86 | } 87 | } 88 | } 89 | return {}; 90 | } 91 | 92 | async function hostFetch(url, options) { 93 | if (hostFetchSupported === undefined) { 94 | hostFetchSupported = false; 95 | document.addEventListener(FETCH_SUPPORTED_RESPONSE_EVENT, () => hostFetchSupported = true, false); 96 | document.dispatchEvent(new CustomEvent(FETCH_SUPPORTED_REQUEST_EVENT)); 97 | } 98 | if (hostFetchSupported) { 99 | const result = new Promise((resolve, reject) => { 100 | document.dispatchEvent(new CustomEvent(FETCH_REQUEST_EVENT, { detail: JSON.stringify({ url, options }) })); 101 | document.addEventListener(FETCH_RESPONSE_EVENT, onResponseFetch, false); 102 | 103 | function onResponseFetch(event) { 104 | if (event.detail) { 105 | if (event.detail.url == url) { 106 | document.removeEventListener(FETCH_RESPONSE_EVENT, onResponseFetch, false); 107 | if (event.detail.response) { 108 | resolve({ 109 | status: event.detail.status, 110 | headers: new Map(event.detail.headers), 111 | arrayBuffer: async () => event.detail.response 112 | }); 113 | } else { 114 | reject(event.detail.error); 115 | } 116 | } 117 | } else { 118 | reject(); 119 | } 120 | } 121 | }); 122 | return result; 123 | } else { 124 | throw new Error(ERR_HOST_FETCH); 125 | } 126 | } 127 | 128 | export { 129 | fetchResource as fetch, 130 | frameFetch 131 | }; 132 | 133 | async function fetchResource(url, options = {}, useHostFetch = true) { 134 | try { 135 | const fetchOptions = { 136 | cache: options.cache || "force-cache", 137 | headers: options.headers, 138 | referrerPolicy: options.referrerPolicy || "strict-origin-when-cross-origin" 139 | }; 140 | let response; 141 | try { 142 | if ((options.referrer && !USE_HOST_FETCH) || !useHostFetch) { 143 | response = await fetch(url, fetchOptions); 144 | } else { 145 | response = await hostFetch(url, fetchOptions); 146 | } 147 | if (response.status == 401 || response.status == 403 || response.status == 404) { 148 | if (fetchOptions.referrerPolicy != "no-referrer" && !options.referrer) { 149 | response = await fetchResource(url, { ...fetchOptions, referrerPolicy: "no-referrer" }, useHostFetch); 150 | } 151 | } 152 | } catch (error) { 153 | if (error && error.message == ERR_HOST_FETCH) { 154 | response = await fetchResource(url, { ...fetchOptions }, false); 155 | } else if (fetchOptions.referrerPolicy != "no-referrer" && !options.referrer) { 156 | response = await fetchResource(url, { ...fetchOptions, referrerPolicy: "no-referrer" }, useHostFetch); 157 | } else { 158 | throw error; 159 | } 160 | } 161 | return response; 162 | // eslint-disable-next-line no-unused-vars 163 | } catch (error) { 164 | requestId++; 165 | const promise = new Promise((resolve, reject) => pendingResponses.set(requestId, { resolve, reject })); 166 | await sendMessage({ method: "singlefile.fetch", url, requestId, referrer: options.referrer, headers: options.headers }); 167 | return promise; 168 | } 169 | } 170 | 171 | async function frameFetch(url, options) { 172 | const response = await sendMessage({ method: "singlefile.fetchFrame", url, frameId: options.frameId, referrer: options.referrer, headers: options.headers }); 173 | return { 174 | status: response.status, 175 | headers: new Map(response.headers), 176 | arrayBuffer: async () => new Uint8Array(response.array).buffer 177 | }; 178 | } 179 | 180 | async function sendMessage(message) { 181 | const response = await browser.runtime.sendMessage(message); 182 | if (!response || response.error) { 183 | throw new Error(response && response.error && response.error.toString()); 184 | } else { 185 | return response; 186 | } 187 | } -------------------------------------------------------------------------------- /src/lib/single-file/frame-tree/bg/frame-tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | browser.runtime.onMessage.addListener((message, sender) => { 27 | if (message.method == "singlefile.frameTree.initResponse" || message.method == "singlefile.frameTree.ackInitRequest") { 28 | browser.tabs.sendMessage(sender.tab.id, message, { frameId: 0 }); 29 | return Promise.resolve({}); 30 | } 31 | }); -------------------------------------------------------------------------------- /src/lib/single-file/lazy/bg/lazy-timeout.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, setTimeout, clearTimeout */ 25 | 26 | const timeouts = new Map(); 27 | 28 | browser.runtime.onMessage.addListener((message, sender) => { 29 | if (message.method == "singlefile.lazyTimeout.setTimeout") { 30 | let tabTimeouts = timeouts.get(sender.tab.id); 31 | let frameTimeouts; 32 | if (tabTimeouts) { 33 | frameTimeouts = tabTimeouts.get(sender.frameId); 34 | if (frameTimeouts) { 35 | const previousTimeoutId = frameTimeouts.get(message.type); 36 | if (previousTimeoutId) { 37 | clearTimeout(previousTimeoutId); 38 | } 39 | } else { 40 | frameTimeouts = new Map(); 41 | } 42 | } 43 | const timeoutId = setTimeout(async () => { 44 | try { 45 | const tabTimeouts = timeouts.get(sender.tab.id); 46 | const frameTimeouts = tabTimeouts.get(sender.frameId); 47 | if (tabTimeouts && frameTimeouts) { 48 | deleteTimeout(frameTimeouts, message.type); 49 | } 50 | await browser.tabs.sendMessage(sender.tab.id, { method: "singlefile.lazyTimeout.onTimeout", type: message.type }); 51 | // eslint-disable-next-line no-unused-vars 52 | } catch (error) { 53 | // ignored 54 | } 55 | }, message.delay); 56 | if (!tabTimeouts) { 57 | tabTimeouts = new Map(); 58 | frameTimeouts = new Map(); 59 | tabTimeouts.set(sender.frameId, frameTimeouts); 60 | timeouts.set(sender.tab.id, tabTimeouts); 61 | } 62 | frameTimeouts.set(message.type, timeoutId); 63 | return Promise.resolve({}); 64 | } 65 | if (message.method == "singlefile.lazyTimeout.clearTimeout") { 66 | let tabTimeouts = timeouts.get(sender.tab.id); 67 | if (tabTimeouts) { 68 | const frameTimeouts = tabTimeouts.get(sender.frameId); 69 | if (frameTimeouts) { 70 | const timeoutId = frameTimeouts.get(message.type); 71 | if (timeoutId) { 72 | clearTimeout(timeoutId); 73 | } 74 | deleteTimeout(frameTimeouts, message.type); 75 | } 76 | } 77 | return Promise.resolve({}); 78 | } 79 | }); 80 | 81 | browser.tabs.onRemoved.addListener(tabId => timeouts.delete(tabId)); 82 | 83 | function deleteTimeout(framesTimeouts, type) { 84 | framesTimeouts.delete(type); 85 | } -------------------------------------------------------------------------------- /src/lib/web-stream/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | if (typeof globalThis.TransformStream === "undefined") { 25 | globalThis.TransformStream = class TransformStream { }; 26 | } 27 | if (typeof globalThis.WritableStream === "undefined") { 28 | globalThis.WritableStream = class WritableStream { }; 29 | } -------------------------------------------------------------------------------- /src/lib/webdav/webdav.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global fetch, btoa, AbortController */ 25 | 26 | const EMPTY_STRING = ""; 27 | const CONFLICT_ACTION_SKIP = "skip"; 28 | const CONFLICT_ACTION_UNIQUIFY = "uniquify"; 29 | const CONFLICT_ACTION_OVERWRITE = "overwrite"; 30 | const CONFLICT_ACTION_PROMPT = "prompt"; 31 | const BASIC_PREFIX_AUTHORIZATION = "Basic "; 32 | const AUTHORIZATION_HEADER = "Authorization"; 33 | const AUTHORIZATION_SEPARATOR = ":"; 34 | const DIRECTORY_SEPARATOR = "/"; 35 | const EXTENSION_SEPARATOR = "."; 36 | const ERROR_PREFIX_MESSAGE = "Error "; 37 | const INDEX_FILENAME_PREFIX = " ("; 38 | const INDEX_FILENAME_SUFFIX = ")"; 39 | const INDEX_FILENAME_REGEXP = /\s\((\d+)\)$/; 40 | const ABORT_ERROR_NAME = "AbortError"; 41 | const HEAD_METHOD = "HEAD"; 42 | const PUT_METHOD = "PUT"; 43 | const DELETE_METHOD = "DELETE"; 44 | const PROPFIND_METHOD = "PROPFIND"; 45 | const MKCOL_METHOD = "MKCOL"; 46 | const CONTENT_TYPE_HEADER = "Content-Type"; 47 | const HTML_CONTENT_TYPE = "text/html"; 48 | const CREDENTIALS_PARAMETER = "omit"; 49 | const FOUND_STATUS = 200; 50 | const CREATED_STATUS = 201; 51 | const NOT_FOUND_STATUS = 404; 52 | const MIN_ERROR_STATUS = 400; 53 | 54 | export { 55 | WebDAV 56 | }; 57 | 58 | class WebDAV { 59 | constructor(url, username, password) { 60 | if (!url.endsWith(DIRECTORY_SEPARATOR)) { 61 | url += DIRECTORY_SEPARATOR; 62 | } 63 | this.url = url; 64 | this.authorization = BASIC_PREFIX_AUTHORIZATION + btoa(username + AUTHORIZATION_SEPARATOR + password); 65 | } 66 | 67 | upload(filename, content, options) { 68 | this.controller = new AbortController(); 69 | options.signal = this.controller.signal; 70 | options.authorization = this.authorization; 71 | options.url = this.url; 72 | return upload(filename, content, options); 73 | } 74 | 75 | abort() { 76 | if (this.controller) { 77 | this.controller.abort(); 78 | } 79 | } 80 | } 81 | 82 | async function upload(filename, content, options) { 83 | const { authorization, filenameConflictAction, prompt, signal, preventRetry } = options; 84 | let { url } = options; 85 | try { 86 | if (filenameConflictAction == CONFLICT_ACTION_OVERWRITE) { 87 | let response = await sendRequest(filename, PUT_METHOD, content); 88 | if (response.status == CREATED_STATUS) { 89 | return response; 90 | } else if (response.status >= MIN_ERROR_STATUS) { 91 | response = await sendRequest(filename, DELETE_METHOD); 92 | if (response.status >= MIN_ERROR_STATUS) { 93 | throw new Error(ERROR_PREFIX_MESSAGE + response.status); 94 | } 95 | return await upload(filename, content, options); 96 | } 97 | } else { 98 | let response = await sendRequest(filename, HEAD_METHOD); 99 | if (response.status == FOUND_STATUS) { 100 | if (filenameConflictAction == CONFLICT_ACTION_UNIQUIFY || (filenameConflictAction == CONFLICT_ACTION_PROMPT && !prompt)) { 101 | const { filenameWithoutExtension, extension, indexFilename } = splitFilename(filename); 102 | options.indexFilename = indexFilename + 1; 103 | return await upload(getFilename(filenameWithoutExtension, extension), content, options); 104 | } else if (filenameConflictAction == CONFLICT_ACTION_PROMPT) { 105 | filename = await prompt(filename); 106 | return filename ? upload(filename, content, options) : response; 107 | } else if (filenameConflictAction == CONFLICT_ACTION_SKIP) { 108 | return response; 109 | } 110 | } else if (response.status == NOT_FOUND_STATUS) { 111 | response = await sendRequest(filename, PUT_METHOD, content); 112 | if (response.status >= MIN_ERROR_STATUS && !preventRetry) { 113 | if (filename.includes(DIRECTORY_SEPARATOR)) { 114 | await createDirectories(); 115 | options.preventRetry = true; 116 | return await upload(filename, content, options); 117 | } else { 118 | throw new Error(ERROR_PREFIX_MESSAGE + response.status); 119 | } 120 | } else { 121 | return response; 122 | } 123 | } else if (response.status >= MIN_ERROR_STATUS) { 124 | throw new Error(ERROR_PREFIX_MESSAGE + response.status); 125 | } 126 | } 127 | } catch (error) { 128 | if (error.name != ABORT_ERROR_NAME) { 129 | throw error; 130 | } 131 | } 132 | 133 | function sendRequest(path, method, body) { 134 | const headers = { 135 | [AUTHORIZATION_HEADER]: authorization 136 | }; 137 | if (body) { 138 | headers[CONTENT_TYPE_HEADER] = HTML_CONTENT_TYPE; 139 | } 140 | return fetch(url + path, { method, headers, signal, body, credentials: CREDENTIALS_PARAMETER }); 141 | } 142 | 143 | function splitFilename(filename) { 144 | let filenameWithoutExtension = filename; 145 | let extension = EMPTY_STRING; 146 | const indexExtensionSeparator = filename.lastIndexOf(EXTENSION_SEPARATOR); 147 | if (indexExtensionSeparator > -1) { 148 | filenameWithoutExtension = filename.substring(0, indexExtensionSeparator); 149 | extension = filename.substring(indexExtensionSeparator + 1); 150 | } 151 | let indexFilename; 152 | ({ filenameWithoutExtension, indexFilename } = extractIndexFilename(filenameWithoutExtension)); 153 | return { filenameWithoutExtension, extension, indexFilename }; 154 | } 155 | 156 | function extractIndexFilename(filenameWithoutExtension) { 157 | const indexFilenameMatch = filenameWithoutExtension.match(INDEX_FILENAME_REGEXP); 158 | let indexFilename = 0; 159 | if (indexFilenameMatch && indexFilenameMatch.length > 1) { 160 | const parsedIndexFilename = Number(indexFilenameMatch[indexFilenameMatch.length - 1]); 161 | if (!Number.isNaN(parsedIndexFilename)) { 162 | indexFilename = parsedIndexFilename; 163 | filenameWithoutExtension = filenameWithoutExtension.replace(INDEX_FILENAME_REGEXP, EMPTY_STRING); 164 | } 165 | } 166 | return { filenameWithoutExtension, indexFilename }; 167 | } 168 | 169 | function getFilename(filenameWithoutExtension, extension) { 170 | return filenameWithoutExtension + 171 | INDEX_FILENAME_PREFIX + options.indexFilename + INDEX_FILENAME_SUFFIX + 172 | (extension ? EXTENSION_SEPARATOR + extension : EMPTY_STRING); 173 | } 174 | 175 | async function createDirectories() { 176 | const filenameParts = filename.split(DIRECTORY_SEPARATOR); 177 | filenameParts.pop(); 178 | let path = EMPTY_STRING; 179 | for (const filenamePart of filenameParts) { 180 | if (filenamePart) { 181 | path += filenamePart; 182 | const response = await sendRequest(path, PROPFIND_METHOD); 183 | if (response.status == NOT_FOUND_STATUS) { 184 | const response = await sendRequest(path, MKCOL_METHOD); 185 | if (response.status >= MIN_ERROR_STATUS) { 186 | throw new Error(ERROR_PREFIX_MESSAGE + response.status); 187 | } 188 | } 189 | path += DIRECTORY_SEPARATOR; 190 | } 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /src/lib/woleet/woleet.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | /* global fetch */ 24 | const urlService = "https://api.woleet.io/v1/anchor"; 25 | const apiKey = ""; 26 | export { 27 | anchor 28 | }; 29 | async function anchor(hash, userKey) { 30 | let bearer = userKey || apiKey; 31 | const response = await fetch(urlService, { 32 | method: "POST", 33 | headers: { 34 | "Accept": "application/json", 35 | "Content-Type": "application/json", 36 | "Authorization": "Bearer " + bearer 37 | }, 38 | body: JSON.stringify({ 39 | "name": hash, 40 | "hash": hash, 41 | "public": true 42 | }) 43 | }); 44 | if (response.status == 401) { 45 | const error = new Error("Your access token on Woleet is invalid. Go to __DOC_LINK__ to create your account."); 46 | error.link = "https://app.woleet.io/"; 47 | throw error; 48 | } else if (response.status == 402) { 49 | const error = new Error("You have no more credits on Woleet. Go to __DOC_LINK__ to recharge them."); 50 | error.link = "https://app.woleet.io/"; 51 | throw error; 52 | } else if (response.status >= 400) { 53 | throw new Error((response.statusText || ("Error " + response.status)) + " (Woleet)"); 54 | } 55 | return response.json(); 56 | } -------------------------------------------------------------------------------- /src/ui/bg/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | import * as button from "./ui-button.js"; 27 | import * as menus from "./ui-menus.js"; 28 | import * as command from "./ui-commands.js"; 29 | 30 | export { 31 | onMessage, 32 | refreshTab, 33 | onForbiddenDomain, 34 | onStart, 35 | onError, 36 | onEdit, 37 | onEnd, 38 | onCancelled, 39 | onUploadProgress, 40 | onTabCreated, 41 | onTabActivated, 42 | onInit, 43 | init 44 | }; 45 | 46 | function init(businessApi) { 47 | menus.init(businessApi); 48 | button.init(businessApi); 49 | command.init(businessApi); 50 | } 51 | 52 | function onMessage(message, sender) { 53 | if (message.method.endsWith(".refreshMenu")) { 54 | return menus.onMessage(message, sender); 55 | } else { 56 | return button.onMessage(message, sender); 57 | } 58 | } 59 | 60 | async function refreshTab(tab) { 61 | return Promise.all([menus.refreshTab(tab), button.refreshTab(tab)]); 62 | } 63 | 64 | function onForbiddenDomain(tab) { 65 | button.onForbiddenDomain(tab); 66 | } 67 | 68 | function onStart(tabId, step, autoSave) { 69 | button.onStart(tabId, step, autoSave); 70 | } 71 | 72 | async function onError(tabId, message, link) { 73 | button.onError(tabId); 74 | try { 75 | if (message) { 76 | await browser.tabs.sendMessage(tabId, { method: "content.error", error: message.toString(), link }); 77 | } 78 | // eslint-disable-next-line no-unused-vars 79 | } catch (error) { 80 | // ignored 81 | } 82 | } 83 | 84 | function onEdit(tabId) { 85 | button.onEdit(tabId); 86 | } 87 | 88 | function onEnd(tabId, autoSave) { 89 | button.onEnd(tabId, autoSave); 90 | } 91 | 92 | function onCancelled(tabId) { 93 | button.onCancelled(tabId); 94 | } 95 | 96 | function onUploadProgress(tabId, index, maxIndex) { 97 | button.onUploadProgress(tabId, index, maxIndex); 98 | } 99 | 100 | function onTabCreated(tab) { 101 | menus.onTabCreated(tab); 102 | } 103 | 104 | function onTabActivated(tab) { 105 | menus.onTabActivated(tab); 106 | } 107 | 108 | function onInit(tab) { 109 | menus.onInit(tab); 110 | } -------------------------------------------------------------------------------- /src/ui/bg/ui-commands.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser */ 25 | 26 | import { queryTabs } from "./../../core/bg/tabs-util.js"; 27 | 28 | const commands = browser.commands; 29 | const BROWSER_COMMANDS_API_SUPPORTED = commands && commands.onCommand && commands.onCommand.addListener; 30 | 31 | let business; 32 | 33 | export { 34 | init 35 | }; 36 | 37 | function init(businessApi) { 38 | business = businessApi; 39 | } 40 | 41 | if (BROWSER_COMMANDS_API_SUPPORTED) { 42 | commands.onCommand.addListener(async command => { 43 | if (command == "save-selected-tabs") { 44 | const highlightedTabs = await queryTabs({ currentWindow: true, highlighted: true }); 45 | business.saveTabs(highlightedTabs, { optionallySelected: true }); 46 | } 47 | if (command == "save-all-tabs") { 48 | const tabs = await queryTabs({ currentWindow: true }); 49 | business.saveTabs(tabs); 50 | } 51 | }); 52 | } -------------------------------------------------------------------------------- /src/ui/bg/ui-help.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, document */ 25 | 26 | let BACKGROUND_SAVE_SUPPORTED, 27 | AUTOCLOSE_SUPPORTED, 28 | AUTO_SAVE_SUPPORTED, 29 | AUTO_OPEN_EDITOR_SUPPORTED, 30 | INFOBAR_SUPPORTED, 31 | BOOKMARKS_API_SUPPORTED, 32 | IDENTITY_API_SUPPORTED, 33 | CLIPBOARD_API_SUPPORTED, 34 | NATIVE_API_API_SUPPORTED, 35 | WEB_BLOCKING_API_SUPPORTED, 36 | SHARE_API_SUPPORTED, 37 | SELECTABLE_TABS_SUPPORTED; 38 | browser.runtime.sendMessage({ method: "config.getConstants" }).then(data => { 39 | ({ 40 | BACKGROUND_SAVE_SUPPORTED, 41 | AUTOCLOSE_SUPPORTED, 42 | AUTO_SAVE_SUPPORTED, 43 | AUTO_OPEN_EDITOR_SUPPORTED, 44 | INFOBAR_SUPPORTED, 45 | BOOKMARKS_API_SUPPORTED, 46 | IDENTITY_API_SUPPORTED, 47 | CLIPBOARD_API_SUPPORTED, 48 | NATIVE_API_API_SUPPORTED, 49 | WEB_BLOCKING_API_SUPPORTED, 50 | SHARE_API_SUPPORTED, 51 | SELECTABLE_TABS_SUPPORTED 52 | } = data); 53 | init(); 54 | }); 55 | 56 | function init() { 57 | if (!AUTO_SAVE_SUPPORTED) { 58 | document.getElementById("autoSaveSection").hidden = true; 59 | document.getElementById("autoSaveOptions").hidden = true; 60 | document.getElementById("autoSaveMenu").hidden = true; 61 | document.getElementById("autoSaveHint").hidden = true; 62 | } 63 | if (!AUTOCLOSE_SUPPORTED) { 64 | document.getElementById("autoCloseOption").hidden = true; 65 | } 66 | if (!BACKGROUND_SAVE_SUPPORTED) { 67 | document.getElementById("backgroundSaveOption").hidden = true; 68 | document.getElementById("confirmFilenameOption").hidden = true; 69 | document.getElementById("filenameConflictActionOption").hidden = true; 70 | } 71 | if (!BOOKMARKS_API_SUPPORTED) { 72 | document.getElementById("bookmarksSection").hidden = true; 73 | document.getElementById("bookmarksOptions").hidden = true; 74 | } 75 | if (!AUTO_OPEN_EDITOR_SUPPORTED) { 76 | document.getElementById("autoOpenEditorOption").hidden = true; 77 | } 78 | if (!INFOBAR_SUPPORTED) { 79 | document.getElementById("displayInfobarOption").hidden = true; 80 | } 81 | if (!IDENTITY_API_SUPPORTED) { 82 | document.getElementById("saveToGDriveOption").hidden = true; 83 | document.getElementById("saveToGDriveHint").hidden = true; 84 | document.getElementById("saveToDropboxOption").hidden = true; 85 | } 86 | if (!CLIPBOARD_API_SUPPORTED) { 87 | document.getElementById("saveToClipboardOption").hidden = true; 88 | } 89 | if (!NATIVE_API_API_SUPPORTED) { 90 | document.getElementById("saveWithCompanionOption").hidden = true; 91 | } 92 | if (!WEB_BLOCKING_API_SUPPORTED) { 93 | document.getElementById("passReferrerOnErrorOption").hidden = true; 94 | } 95 | if (!SHARE_API_SUPPORTED) { 96 | document.getElementById("sharePageOption").hidden = true; 97 | } 98 | if (!SELECTABLE_TABS_SUPPORTED) { 99 | document.getElementById("selectableTabsMenu").hidden = true; 100 | document.getElementById("shortcutsSection").hidden = true; 101 | } 102 | } -------------------------------------------------------------------------------- /src/ui/bg/ui-options-editor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, document, alert */ 25 | 26 | const titleLabel = document.getElementById("titleLabel"); 27 | 28 | const optionsInput = document.getElementById("optionsInput"); 29 | const saveButton = document.getElementById("saveButton"); 30 | 31 | titleLabel.textContent = browser.i18n.getMessage("optionsEditorTitle"); 32 | saveButton.textContent = browser.i18n.getMessage("optionsEditorSaveButton"); 33 | const invalidJSONMessage = browser.i18n.getMessage("optionsEditorInvalidJSON"); 34 | const configSavedMessage = browser.i18n.getMessage("optionsEditorConfigSaved"); 35 | 36 | init(); 37 | saveButton.addEventListener("click", async () => { 38 | let config; 39 | try { 40 | config = JSON.parse(optionsInput.value); 41 | // eslint-disable-next-line no-unused-vars 42 | } catch (error) { 43 | alert(invalidJSONMessage); 44 | } 45 | if (config) { 46 | await browser.runtime.sendMessage({ method: "config.set", config }); 47 | await refreshExternalComponents(config); 48 | alert(configSavedMessage); 49 | } 50 | saveButton.blur(); 51 | }); 52 | 53 | async function init() { 54 | const config = await browser.runtime.sendMessage({ method: "config.get" }); 55 | optionsInput.value = JSON.stringify(config, null, 4); 56 | } 57 | 58 | async function refreshExternalComponents(config) { 59 | try { 60 | await browser.runtime.sendMessage({ method: "ui.refreshMenu" }); 61 | for (const profileName of Object.keys(config.profiles)) { 62 | await browser.runtime.sendMessage({ method: "options.refresh", profileName }); 63 | await browser.runtime.sendMessage({ method: "options.refreshPanel", profileName }); 64 | } 65 | // eslint-disable-next-line no-unused-vars 66 | } catch (error) { 67 | // ignored 68 | } 69 | } -------------------------------------------------------------------------------- /src/ui/bg/ui-panel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, document */ 25 | 26 | const optionsTab = document.getElementById("tab-options"); 27 | const pendingsTab = document.getElementById("tab-pendings"); 28 | const batchSaveUrlsTab = document.getElementById("tab-batch-save-urls"); 29 | const viewPanel = document.getElementById("view-panel"); 30 | 31 | optionsTab.textContent = optionsTab.title = browser.i18n.getMessage("optionsTitle"); 32 | pendingsTab.textContent = pendingsTab.title = browser.i18n.getMessage("pendingsTitle"); 33 | batchSaveUrlsTab.textContent = batchSaveUrlsTab.title = browser.i18n.getMessage("batchSaveUrlsTitle"); 34 | 35 | optionsTab.onclick = () => { 36 | optionsTab.classList.add("tab-selected"); 37 | pendingsTab.classList.remove("tab-selected"); 38 | batchSaveUrlsTab.classList.remove("tab-selected"); 39 | viewPanel.src = "options.html#side-panel"; 40 | }; 41 | pendingsTab.onclick = () => { 42 | optionsTab.classList.remove("tab-selected"); 43 | pendingsTab.classList.add("tab-selected"); 44 | batchSaveUrlsTab.classList.remove("tab-selected"); 45 | viewPanel.src = "pendings.html#side-panel"; 46 | }; 47 | batchSaveUrlsTab.onclick = () => { 48 | optionsTab.classList.remove("tab-selected"); 49 | pendingsTab.classList.remove("tab-selected"); 50 | batchSaveUrlsTab.classList.add("tab-selected"); 51 | viewPanel.src = "batch-save-urls.html#side-panel"; 52 | }; -------------------------------------------------------------------------------- /src/ui/bg/ui-pendings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global browser, window, document, setInterval, location */ 25 | 26 | const URLLabel = document.getElementById("URLLabel"); 27 | const titleLabel = document.getElementById("titleLabel"); 28 | const resultsTable = document.getElementById("resultsTable"); 29 | const cancelAllButton = document.getElementById("cancelAllButton"); 30 | const addUrlsButton = document.getElementById("addUrlsButton"); 31 | document.title = browser.i18n.getMessage("pendingsTitle"); 32 | cancelAllButton.textContent = browser.i18n.getMessage("pendingsCancelAllButton"); 33 | addUrlsButton.textContent = browser.i18n.getMessage("pendingsAddUrlsButton"); 34 | URLLabel.textContent = browser.i18n.getMessage("pendingsURLTitle"); 35 | titleLabel.textContent = browser.i18n.getMessage("pendingsTitleTitle"); 36 | document.getElementById("statusLabel").textContent = browser.i18n.getMessage("pendingsStatusTitle"); 37 | const statusText = { 38 | pending: browser.i18n.getMessage("pendingsPendingStatus"), 39 | processing: browser.i18n.getMessage("pendingsProcessingStatus"), 40 | cancelling: browser.i18n.getMessage("pendingsCancellingStatus") 41 | }; 42 | const noPendingsText = browser.i18n.getMessage("pendingsNoPendings"); 43 | cancelAllButton.onclick = async () => { 44 | await browser.runtime.sendMessage({ method: "downloads.cancelAll" }); 45 | await refresh(); 46 | }; 47 | addUrlsButton.onclick = () => window.open("batch-save-urls.html", "sf-add-urls"); 48 | if (location.href.endsWith("#side-panel")) { 49 | document.documentElement.classList.add("side-panel"); 50 | } 51 | let URLDisplayed = true; 52 | document.getElementById("URLTitleLabel").onclick = () => { 53 | URLDisplayed = !URLDisplayed; 54 | refresh(true); 55 | }; 56 | let previousState; 57 | setInterval(refresh, 1000); 58 | refresh(); 59 | 60 | function resetTable() { 61 | resultsTable.innerHTML = ""; 62 | } 63 | 64 | function updateTable(results) { 65 | if (results.length) { 66 | results.sort((taskInfo1, taskInfo2) => taskInfo1.index - taskInfo2.index); 67 | results.forEach((taskInfo) => { 68 | const row = document.createElement("div"); 69 | const cellURL = document.createElement("span"); 70 | const cellStatus = document.createElement("span"); 71 | const cellCancel = document.createElement("span"); 72 | const buttonCancel = document.createElement("button"); 73 | row.className = "result-row"; 74 | if (URLDisplayed) { 75 | cellURL.textContent = taskInfo.url; 76 | } else { 77 | cellURL.textContent = taskInfo.title; 78 | } 79 | cellURL.className = "result-url-title"; 80 | cellURL.onclick = () => selectTab(taskInfo.tabId); 81 | if (taskInfo.cancelled) { 82 | cellStatus.textContent = statusText.cancelling; 83 | } else { 84 | cellStatus.textContent = statusText[taskInfo.status]; 85 | buttonCancel.textContent = "×"; 86 | buttonCancel.onclick = () => cancel(taskInfo.id); 87 | cellCancel.appendChild(buttonCancel); 88 | } 89 | cellStatus.className = "result-status"; 90 | cellCancel.className = "result-cancel"; 91 | row.appendChild(cellURL); 92 | row.appendChild(cellStatus); 93 | row.appendChild(cellCancel); 94 | resultsTable.appendChild(row); 95 | }); 96 | } 97 | } 98 | 99 | async function cancel(taskId) { 100 | await browser.runtime.sendMessage({ method: "downloads.cancel", taskId }); 101 | await refresh(); 102 | } 103 | 104 | async function selectTab(tabId) { 105 | await browser.runtime.sendMessage({ method: "tabs.activate", tabId }); 106 | await refresh(); 107 | } 108 | 109 | async function refresh(force) { 110 | const results = await browser.runtime.sendMessage({ method: "downloads.getInfo" }); 111 | const currentState = JSON.stringify(results); 112 | if (previousState != currentState || force) { 113 | previousState = currentState; 114 | resetTable(); 115 | if (URLDisplayed) { 116 | URLLabel.className = ""; 117 | titleLabel.className = "unselected"; 118 | } else { 119 | URLLabel.className = "unselected"; 120 | titleLabel.className = ""; 121 | } 122 | updateTable(results); 123 | if (!results.length) { 124 | const row = document.createElement("div"); 125 | row.className = "result-row"; 126 | const cell = document.createElement("span"); 127 | cell.colSpan = 3; 128 | cell.className = "no-result"; 129 | cell.textContent = noPendingsText; 130 | row.appendChild(cell); 131 | resultsTable.appendChild(row); 132 | } 133 | } 134 | cancelAllButton.disabled = !results.length; 135 | } -------------------------------------------------------------------------------- /src/ui/bg/ui-viewer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2020 Gildas Lormeau 3 | * contact : gildas.lormeau gmail.com 4 | * 5 | * This file is part of SingleFile. 6 | * 7 | * The code in this file is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License 9 | * (GNU AGPL) as published by the Free Software Foundation, either version 3 10 | * of the License, or (at your option) any later version. 11 | * 12 | * The code in this file is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 15 | * General Public License for more details. 16 | * 17 | * As additional permission under GNU AGPL version 3 section 7, you may 18 | * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 19 | * AGPL normally required by section 4, provided you include this license 20 | * notice and a URL through which recipients can access the Corresponding 21 | * Source. 22 | */ 23 | 24 | /* global document, location, singlefile, fetch, URLSearchParams, prompt */ 25 | 26 | import * as zip from "../../../lib/single-file-zip.js"; 27 | 28 | globalThis.zip = zip; 29 | globalThis.onload = async () => { 30 | const params = new URLSearchParams(location.search); 31 | const blobURI = params.get("blobURI"); 32 | if (blobURI.startsWith("blob:")) { 33 | const compressed = params.has("compressed"); 34 | const response = await fetch(blobURI); 35 | if (compressed) { 36 | const blob = await response.blob(); 37 | const { docContent } = await singlefile.helper.extract(blob, { prompt }); 38 | await singlefile.helper.display(document, docContent); 39 | } else { 40 | const text = await response.text(); 41 | document.write(text); 42 | document.close(); 43 | } 44 | } 45 | }; -------------------------------------------------------------------------------- /src/ui/content/content-ui-editor-init-web.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 3 | (() => { 4 | 5 | document.currentScript.remove(); 6 | processNode(document); 7 | 8 | function processNode(node) { 9 | node.querySelectorAll("template[shadowrootmode]").forEach(element => { 10 | let shadowRoot = element.parentElement.shadowRoot; 11 | if (!shadowRoot) { 12 | try { 13 | shadowRoot = element.parentElement.attachShadow({ 14 | mode: element.getAttribute("shadowrootmode") 15 | }); 16 | shadowRoot.innerHTML = element.innerHTML; 17 | element.remove(); 18 | // eslint-disable-next-line no-unused-vars 19 | } catch (error) { 20 | // ignored 21 | } 22 | if (shadowRoot) { 23 | processNode(shadowRoot); 24 | } 25 | } 26 | }); 27 | } 28 | 29 | })(); -------------------------------------------------------------------------------- /src/ui/pages/batch-save-urls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |   7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 | 34 |
35 |
36 |
37 |
38 | 41 |
42 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/ui/pages/editor-frame-web.css: -------------------------------------------------------------------------------- 1 | .single-file-highlight-yellow, .single-file-highlight-yellow-mode ::selection { 2 | background-color: #ffff7c !important; 3 | color: black !important; 4 | } 5 | 6 | .single-file-highlight-pink, .single-file-highlight-pink-mode ::selection { 7 | background-color: #ffbbb6 !important; 8 | color: black !important; 9 | } 10 | 11 | .single-file-highlight-blue, .single-file-highlight-blue-mode ::selection { 12 | background-color: #95d0ff !important; 13 | color: black !important; 14 | } 15 | 16 | .single-file-highlight-green, .single-file-highlight-green-mode ::selection { 17 | background-color: #93ef8d !important; 18 | color: black !important; 19 | } 20 | 21 | span.single-file-highlight-yellow, span.single-file-highlight-pink, span.single-file-highlight-blue, span.single-file-highlight-green { 22 | display: inline !important; 23 | } 24 | 25 | .single-file-highlight-hidden { 26 | background-color: inherit !important; 27 | color: inherit !important; 28 | } 29 | 30 | .single-file-mask { 31 | all: initial; 32 | display: contents !important; 33 | } 34 | 35 | .single-file-mask.single-file-page-mask { 36 | opacity: .99 !important; 37 | } 38 | 39 | single-file-note { 40 | all: initial !important; 41 | display: contents !important; 42 | } 43 | 44 | .single-file-cut-hover, .single-file-cut-outer-hover, .single-file-cut-selected, .single-file-cut-outer-selected { 45 | transition: outline-width 125ms !important; 46 | outline-offset: -4px !important; 47 | outline-width: 4px !important; 48 | } 49 | 50 | .single-file-cut-hover, .single-file-cut-outer-hover { 51 | outline-style: dotted !important; 52 | } 53 | 54 | .single-file-cut-selected, .single-file-cut-outer-selected { 55 | outline-style: dashed !important; 56 | } 57 | 58 | .single-file-cut-hover, .single-file-cut-selected { 59 | outline-color: red !important; 60 | } 61 | 62 | .single-file-cut-outer-hover, .single-file-cut-outer-selected { 63 | outline-color: green !important; 64 | } 65 | 66 | .single-file-cut-mode, .single-file-cut-mode * { 67 | pointer-events: auto !important; 68 | touch-action: none !important; 69 | } 70 | 71 | .single-file-cut-hover, .single-file-cut-outer-hover, .single-file-remove-highlights-mode .single-file-highlight:hover { 72 | cursor: crosshair !important; 73 | } 74 | 75 | .single-file-removed { 76 | display: none !important; 77 | float: none !important; 78 | position: static !important; 79 | visibility: collapse !important; 80 | } 81 | 82 | a[href], img { 83 | -webkit-touch-callout: none; 84 | } -------------------------------------------------------------------------------- /src/ui/pages/editor-mask-web.css: -------------------------------------------------------------------------------- 1 | .note-mask { 2 | all: initial; 3 | position: fixed; 4 | z-index: 2147483645; 5 | pointer-events: none; 6 | background-color: transparent; 7 | transition: background-color 125ms; 8 | } 9 | 10 | .note-mask-moving.note-yellow { 11 | background-color: rgba(255, 255, 124, .3); 12 | } 13 | 14 | .note-mask-moving.note-pink { 15 | background-color: rgba(255, 187, 182, .3); 16 | } 17 | 18 | .note-mask-moving.note-blue { 19 | background-color: rgba(149, 208, 255, .3); 20 | } 21 | 22 | .note-mask-moving.note-green { 23 | background-color: rgba(156, 255, 149, .3); 24 | } 25 | 26 | .page-mask { 27 | all: initial; 28 | position: fixed; 29 | top: 0; 30 | left: 0; 31 | width: 0; 32 | height: 0; 33 | z-index: 2147483646; 34 | } 35 | 36 | .page-mask-active { 37 | width: 100vw; 38 | height: 100vh; 39 | } -------------------------------------------------------------------------------- /src/ui/pages/editor-note-web.css: -------------------------------------------------------------------------------- 1 | .note { 2 | all: initial; 3 | display: flex; 4 | flex-direction: column; 5 | height: 150px; 6 | width: 150px; 7 | position: absolute; 8 | top: 10px; 9 | left: 10px; 10 | border: 1px solid rgb(191, 191, 191); 11 | z-index: 2147483646; 12 | box-shadow: 3px 3px 3px rgba(33, 33, 33, .7); 13 | min-height: 100px; 14 | min-width: 100px; 15 | } 16 | 17 | .note-selected { 18 | z-index: 2147483647; 19 | } 20 | 21 | .note-hidden { 22 | display: none; 23 | } 24 | 25 | .note-collapsed { 26 | min-height: 30px; 27 | max-height: 30px; 28 | overflow: hidden; 29 | } 30 | 31 | .note blockquote { 32 | all: initial; 33 | padding: 1px; 34 | height: 100%; 35 | } 36 | 37 | .note textarea { 38 | all: initial; 39 | white-space: break-spaces; 40 | font-family: Arial, Helvetica, sans-serif; 41 | font-size: 14px; 42 | height: 100%; 43 | width: 100%; 44 | padding: 2px; 45 | border: 1px solid transparent; 46 | resize: none; 47 | color: black; 48 | } 49 | 50 | .note textarea:focus { 51 | border: 1px dotted rgb(160, 160, 160); 52 | } 53 | 54 | .note header { 55 | all: initial; 56 | min-height: 30px; 57 | cursor: grab; 58 | user-select: none; 59 | } 60 | 61 | .note .note-remove { 62 | all: initial; 63 | position: absolute; 64 | right: 0px; 65 | top: 2px; 66 | padding: 5px; 67 | opacity: .5; 68 | cursor: pointer; 69 | user-select: none; 70 | width: 16px; 71 | height: 16px; 72 | } 73 | 74 | .note .note-anchor { 75 | all: initial; 76 | position: absolute; 77 | left: 0px; 78 | top: 2px; 79 | padding: 5px; 80 | opacity: .25; 81 | cursor: pointer; 82 | width: 16px; 83 | height: 16px; 84 | } 85 | 86 | .note .note-resize { 87 | all: initial; 88 | position: absolute; 89 | bottom: -5px; 90 | right: -5px; 91 | height: 15px; 92 | width: 15px; 93 | cursor: nwse-resize; 94 | user-select: none; 95 | } 96 | 97 | .note .note-remove:hover { 98 | opacity: 1; 99 | } 100 | 101 | .note .note-anchor:hover { 102 | opacity: .5; 103 | } 104 | 105 | .note-anchored .note-anchor { 106 | opacity: .5; 107 | } 108 | 109 | .note-anchored .note-anchor:hover { 110 | opacity: 1; 111 | } 112 | 113 | .note-moving { 114 | opacity: .75; 115 | box-shadow: 6px 6px 3px rgba(33, 33, 33, .7); 116 | } 117 | 118 | .note-moving * { 119 | cursor: grabbing; 120 | } 121 | 122 | .note-yellow header { 123 | background-color: #f5f545; 124 | } 125 | 126 | .note-yellow blockquote { 127 | background-color: #ffff7c; 128 | } 129 | 130 | .note-pink header { 131 | background-color: #ffa59f; 132 | } 133 | 134 | .note-pink blockquote { 135 | background-color: #ffbbb6; 136 | } 137 | 138 | .note-blue header { 139 | background-color: #84c8ff; 140 | } 141 | 142 | .note-blue blockquote { 143 | background-color: #95d0ff; 144 | } 145 | 146 | .note-green header { 147 | background-color: #93ef8d; 148 | } 149 | 150 | .note-green blockquote { 151 | background-color: #9cff95; 152 | } -------------------------------------------------------------------------------- /src/ui/pages/editor.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | overflow: hidden; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | display: flex; 10 | flex-direction: column; 11 | user-select: none; 12 | background-color: #efefef; 13 | } 14 | 15 | .toolbar { 16 | display: flex; 17 | background-color: transparent; 18 | } 19 | 20 | .buttons { 21 | display: flex; 22 | flex-direction: row; 23 | margin: 1px; 24 | flex: none; 25 | } 26 | 27 | img[type=button] { 28 | cursor: pointer; 29 | background-color: #404040; 30 | padding: 4px; 31 | margin-right: 2px; 32 | border-radius: 4px; 33 | max-width: 24px; 34 | max-height: 24px; 35 | } 36 | 37 | @media (hover: hover) { 38 | img[type=button]:hover { 39 | filter: brightness(0.75); 40 | } 41 | } 42 | 43 | img[type=button].edit-disabled, 44 | img[type=button].format-disabled, 45 | img[type=button].cut-disabled, 46 | img[type=button].highlight-disabled, 47 | img[type=button].remove-highlight-disabled { 48 | background-color: #7b7b7b; 49 | filter: brightness(1.25); 50 | } 51 | 52 | @media (hover: hover) { 53 | img[type=button].edit-disabled:hover, 54 | img[type=button].cut-disabled:hover, 55 | .toolbar:not(.cut-inner-mode):not(.remove-highlight-mode) img[type=button].highlight-disabled:hover, 56 | .toolbar:not(.cut-inner-mode) img[type=button].remove-highlight-disabled:hover { 57 | filter: brightness(0.875); 58 | } 59 | } 60 | 61 | .separator { 62 | display: inline-block; 63 | width: 2px; 64 | height: 22px; 65 | background-color: #404040; 66 | margin-left: 2px; 67 | margin-right: 2px; 68 | margin-top: 6px; 69 | } 70 | 71 | .editor-container { 72 | position: relative; 73 | display: flex; 74 | flex: auto; 75 | } 76 | 77 | .editor { 78 | background-color: white; 79 | flex: auto; 80 | border: none; 81 | border-top-width: 1px; 82 | border-top-style: solid; 83 | border-top-color: #cccccc; 84 | } 85 | 86 | @media (orientation: portrait) { 87 | body { 88 | flex-direction: row; 89 | } 90 | 91 | .toolbar { 92 | flex-direction: column; 93 | min-width: 34px; 94 | max-width: 34px; 95 | } 96 | 97 | .buttons { 98 | flex-direction: column; 99 | } 100 | 101 | .separator { 102 | margin-top: 2px; 103 | margin-bottom: 2px; 104 | margin-left: 6px; 105 | width: 22px; 106 | height: 2px; 107 | } 108 | 109 | img[type="button"] { 110 | margin-right: 0; 111 | margin-bottom: 2px; 112 | } 113 | 114 | @media (max-height: 495px) { 115 | .separator { 116 | display: none; 117 | } 118 | } 119 | } 120 | 121 | @media (prefers-color-scheme: dark) { 122 | body { 123 | background-color: #3a3a3a; 124 | } 125 | 126 | .editor { 127 | border-top-color: #4a4a4a; 128 | } 129 | 130 | img[type=button] { 131 | background-color: #1f1f1f; 132 | } 133 | 134 | img[type=button].edit-disabled, 135 | img[type=button].format-disabled, 136 | img[type=button].cut-disabled, 137 | img[type=button].highlight-disabled, 138 | img[type=button].remove-highlight-disabled { 139 | background-color: #4a4a4a; 140 | } 141 | 142 | .separator { 143 | background-color: #b3b3b3; 144 | } 145 | } -------------------------------------------------------------------------------- /src/ui/pages/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Please wait... 11 | 12 | 13 | 14 |
15 |
16 | 18 | 20 | 22 | 24 | 26 |
27 |
28 |
29 | 31 | 33 | 35 | 37 | 39 | 41 |
42 |
43 |
44 | 46 | 48 |
49 |
50 |
51 | 53 | 55 | 57 | 59 | 61 |
62 |
63 |
64 | 66 | 68 |
69 |
70 |
71 | 75 |
76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/ui/pages/help.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #eee; 3 | font-size: 12pt; 4 | } 5 | 6 | body>div { 7 | position: relative; 8 | background-color: #fff; 9 | font-family: sans-serif; 10 | max-width: 1024px; 11 | margin-left: auto; 12 | margin-right: auto; 13 | border: 1px #bfbfbf solid; 14 | border-radius: 4px; 15 | padding-left: 10px; 16 | padding-right: 10px; 17 | padding-bottom: 20px; 18 | } 19 | 20 | div>iframe { 21 | margin-top: 30px; 22 | margin-left: auto; 23 | margin-right: auto; 24 | display: block; 25 | margin-left: auto; 26 | } 27 | 28 | .option { 29 | font-size: 1.1em; 30 | font-style: normal; 31 | font-family: sans-serif; 32 | color: graytext; 33 | } 34 | 35 | .notice { 36 | font-style: italic; 37 | font-family: serif; 38 | font-size: 1.1em; 39 | } 40 | 41 | ol { 42 | -webkit-padding-start: 30px; 43 | margin: 0px; 44 | padding-right: 10px; 45 | } 46 | 47 | ol>li { 48 | padding-top: 2em; 49 | } 50 | 51 | a[id] { 52 | font-weight: bold; 53 | } 54 | 55 | ol>li>ul { 56 | padding-left: 25px; 57 | } 58 | 59 | ol>li>ul>li { 60 | padding-top: .5em; 61 | } 62 | 63 | ol>li>ul>li>ul { 64 | padding-left: 25px; 65 | } 66 | 67 | .icon { 68 | height: 1em; 69 | } 70 | 71 | .button { 72 | height: .9em; 73 | background-color: #8e8e8e; 74 | padding: 3px; 75 | border-radius: 4px; 76 | } 77 | 78 | #titleIcon { 79 | width: 1.2em; 80 | height: 1.2em; 81 | } 82 | 83 | #title { 84 | padding-left: .3em; 85 | vertical-align: top; 86 | } 87 | 88 | #titleBorder { 89 | margin-top: 10px; 90 | padding-left: 10px; 91 | padding-bottom: 10px; 92 | margin-bottom: 20px; 93 | } 94 | 95 | #index { 96 | padding-left: 10px; 97 | font-size: .9em; 98 | display: inline-block; 99 | line-height: 150%; 100 | } 101 | 102 | #index a { 103 | white-space: nowrap; 104 | } 105 | 106 | h2, h4 { 107 | margin-bottom: 0px; 108 | } 109 | 110 | li { 111 | line-height: 1.5em; 112 | } 113 | 114 | #logo-html5 { 115 | position: absolute; 116 | right: 0px; 117 | bottom: 20px; 118 | } 119 | 120 | .availability { 121 | font-size: 11pt; 122 | } 123 | 124 | code { 125 | font-size: 1.1em; 126 | } 127 | 128 | kbd, .key { 129 | display: inline; 130 | display: inline-block; 131 | min-width: 1em; 132 | padding: .2em .3em; 133 | font: normal .85em/1 "Lucida Grande", Lucida, Arial, sans-serif; 134 | text-align: center; 135 | text-decoration: none; 136 | -moz-border-radius: .3em; 137 | -webkit-border-radius: .3em; 138 | border-radius: .3em; 139 | border: none; 140 | cursor: default; 141 | -moz-user-select: none; 142 | -webkit-user-select: none; 143 | user-select: none; 144 | } 145 | 146 | kbd[title], .key[title] { 147 | cursor: help; 148 | } 149 | 150 | kbd, kbd.dark, .dark-keys kbd, .key, .key.dark, .dark-keys .key { 151 | background: rgb(80, 80, 80); 152 | background: -moz-linear-gradient(top, rgb(60, 60, 60), rgb(80, 80, 80)); 153 | background: -webkit-gradient(linear, left top, left bottom, from(rgb(60, 60, 60)), to(rgb(80, 80, 80))); 154 | color: rgb(250, 250, 250); 155 | text-shadow: -1px -1px 0 rgb(70, 70, 70); 156 | -moz-box-shadow: inset 0 0 1px rgb(150, 150, 150), inset 0 -.05em .4em rgb(80, 80, 80), 0 .1em 0 rgb(30, 30, 30), 0 .1em .1em rgba(0, 0, 0, .3); 157 | -webkit-box-shadow: inset 0 0 1px rgb(150, 150, 150), inset 0 -.05em .4em rgb(80, 80, 80), 0 .1em 0 rgb(30, 30, 30), 0 .1em .1em rgba(0, 0, 0, .3); 158 | box-shadow: inset 0 0 1px rgb(150, 150, 150), inset 0 -.05em .4em rgb(80, 80, 80), 0 .1em 0 rgb(30, 30, 30), 0 .1em .1em rgba(0, 0, 0, .3); 159 | } 160 | 161 | kbd.light, .light-keys kbd, .key.light, .light-keys .key { 162 | background: rgb(250, 250, 250); 163 | background: -moz-linear-gradient(top, rgb(210, 210, 210), rgb(255, 255, 255)); 164 | background: -webkit-gradient(linear, left top, left bottom, from(rgb(210, 210, 210)), to(rgb(255, 255, 255))); 165 | color: rgb(50, 50, 50); 166 | text-shadow: 0 0 2px rgb(255, 255, 255); 167 | -moz-box-shadow: inset 0 0 1px rgb(255, 255, 255), inset 0 0 .4em rgb(200, 200, 200), 0 .1em 0 rgb(130, 130, 130), 0 .11em 0 rgba(0, 0, 0, .4), 0 .1em .11em rgba(0, 0, 0, .9); 168 | -webkit-box-shadow: inset 0 0 1px rgb(255, 255, 255), inset 0 0 .4em rgb(200, 200, 200), 0 .1em 0 rgb(130, 130, 130), 0 .11em 0 rgba(0, 0, 0, .4), 0 .1em .11em rgba(0, 0, 0, .9); 169 | box-shadow: inset 0 0 1px rgb(255, 255, 255), inset 0 0 .4em rgb(200, 200, 200), 0 .1em 0 rgb(130, 130, 130), 0 .11em 0 rgba(0, 0, 0, .4), 0 .1em .11em rgba(0, 0, 0, .9); 170 | } 171 | 172 | @media (max-width:800px) { 173 | body { 174 | background-color: white; 175 | margin: 0px; 176 | } 177 | body>div { 178 | border-width: 0px; 179 | } 180 | ol { 181 | padding-left: 20px; 182 | } 183 | ul { 184 | padding-left: 10px; 185 | } 186 | } 187 | 188 | @media (prefers-color-scheme: dark) { 189 | body { 190 | background-color: #373737; 191 | } 192 | @media (max-width:800px) { 193 | body { 194 | background-color: #202023; 195 | } 196 | } 197 | body>div { 198 | background-color: #202023; 199 | border-color: rgb(81, 81, 81); 200 | } 201 | body>div, a { 202 | color: #fdfdfd; 203 | } 204 | .option { 205 | color: #afafaf; 206 | } 207 | .button { 208 | height: .9em; 209 | background-color: #484848; 210 | } 211 | } -------------------------------------------------------------------------------- /src/ui/pages/options-editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SingleFile options editor 7 | 8 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |

30 | 31 | 32 | 33 |

34 |
35 | 37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/ui/pages/panel.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | font-family: sans-serif; 5 | font-size: 12px; 6 | } 7 | 8 | body { 9 | display: flex; 10 | flex-direction: column; 11 | margin: 0; 12 | height: 100%; 13 | overflow: hidden; 14 | background-color: #fbfbfb; 15 | } 16 | 17 | header { 18 | display: flex; 19 | flex-direction: row; 20 | } 21 | 22 | .tab { 23 | height: 30px; 24 | padding-top: 8px; 25 | padding-left: 8px; 26 | padding-right: 8px; 27 | opacity: .5; 28 | cursor: pointer; 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | white-space: nowrap; 32 | } 33 | 34 | .tab-selected { 35 | opacity: 1; 36 | } 37 | 38 | main { 39 | flex: 1; 40 | } 41 | 42 | iframe { 43 | height: 100%; 44 | width: 100%; 45 | border-width: 0; 46 | } 47 | 48 | @media (prefers-color-scheme: dark) { 49 | body { 50 | background-color: #38383d; 51 | color: #fdfdfd; 52 | } 53 | } -------------------------------------------------------------------------------- /src/ui/pages/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Panel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ui/pages/pendings.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #f0f0f0; 3 | color: black; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | margin-left: 5px; 9 | margin-right: 5px; 10 | } 11 | 12 | main, 13 | header { 14 | font-family: sans-serif; 15 | } 16 | 17 | body>main { 18 | background-color: #fff; 19 | border: solid 1px rgb(191, 191, 191); 20 | } 21 | 22 | body>main, 23 | body>header { 24 | font-size: 12px; 25 | font-family: sans-serif; 26 | margin: 0; 27 | margin-left: auto; 28 | margin-right: auto; 29 | max-width: 1024px; 30 | } 31 | 32 | body>header { 33 | display: flex; 34 | flex-direction: column; 35 | align-items: flex-end; 36 | } 37 | 38 | button { 39 | background-color: #fbfbfb; 40 | border-color: rgb(191, 191, 191); 41 | border-style: solid; 42 | border-radius: 2px; 43 | border-width: 1px; 44 | color: black; 45 | cursor: pointer; 46 | } 47 | 48 | button:not(:disabled):hover { 49 | background-color: #ededed; 50 | } 51 | 52 | button:disabled { 53 | opacity: .25; 54 | cursor: default; 55 | } 56 | 57 | body>header button { 58 | margin-top: 5px; 59 | margin-bottom: 5px; 60 | align-self: flex-end; 61 | padding: 5px; 62 | padding-left: 10px; 63 | padding-right: 10px; 64 | margin-left: 8px; 65 | } 66 | 67 | .result-row { 68 | display: flex; 69 | flex-direction: row; 70 | padding-top: 5px; 71 | padding-bottom: 5px; 72 | min-height: 40px; 73 | } 74 | 75 | .result-row:not(:first-child) { 76 | border-top: #bfbfbf 1px dashed; 77 | } 78 | 79 | .result-head { 80 | background-color: #ececec; 81 | } 82 | 83 | .result-row>span { 84 | padding: 10px; 85 | align-self: center; 86 | } 87 | 88 | .result-row>span>span { 89 | padding: 5px; 90 | } 91 | 92 | .result-head .unselected { 93 | opacity: .5; 94 | } 95 | 96 | .result-row.result-head .result-head-separator { 97 | padding-left: 2px; 98 | padding-right: 2px; 99 | } 100 | 101 | .result-url-title { 102 | flex: 1; 103 | overflow: hidden; 104 | text-overflow: ellipsis; 105 | display: inline-block; 106 | white-space: nowrap; 107 | user-select: none; 108 | cursor: pointer; 109 | } 110 | 111 | .result-row:not(.result-head) .result-url-title { 112 | cursor: pointer; 113 | } 114 | 115 | .result-status { 116 | min-width: 120px; 117 | } 118 | 119 | .result-cancel { 120 | text-align: right; 121 | width: 19px; 122 | } 123 | 124 | .result-cancel button { 125 | background-color: #fbfbfb; 126 | width: 19px; 127 | padding: 0; 128 | } 129 | 130 | .no-result { 131 | color: #888; 132 | } 133 | 134 | html.side-panel body { 135 | margin: 0; 136 | } 137 | 138 | html.side-panel, 139 | .side-panel .result-head, 140 | .side-panel body>main { 141 | background-color: #fbfbfb; 142 | } 143 | 144 | .side-panel .header-buttons { 145 | position: absolute; 146 | right: 12px; 147 | display: flex; 148 | } 149 | 150 | html.side-panel #addUrlsButton { 151 | display: none; 152 | } 153 | 154 | .side-panel body>main { 155 | border: 0; 156 | margin-left: 8px; 157 | margin-right: 12px; 158 | } 159 | 160 | .side-panel .result-status { 161 | display: none; 162 | } 163 | 164 | .side-panel .result-row>span, 165 | .side-panel .result-row>span>span { 166 | padding: 0px; 167 | } 168 | 169 | .side-panel .result-head { 170 | border-bottom: dashed 1px rgb(191, 191, 191); 171 | } 172 | 173 | @media (max-width:400px) { 174 | 175 | body>main, 176 | body>header, 177 | .header-buttons>button { 178 | font-size: 11px; 179 | } 180 | 181 | .result-row { 182 | padding-top: 3px; 183 | padding-bottom: 3px; 184 | min-height: 30px; 185 | } 186 | 187 | .side-panel .header-buttons { 188 | top: 0px; 189 | } 190 | } 191 | 192 | @media (max-width:300px) { 193 | .side-panel :not(.result-cancel)>button { 194 | display: flex; 195 | } 196 | 197 | .side-panel .header-buttons { 198 | flex-direction: column; 199 | } 200 | 201 | .result-head { 202 | padding-bottom: 40px; 203 | } 204 | } 205 | 206 | @media (prefers-color-scheme: dark) { 207 | html { 208 | background-color: #373737; 209 | color: #fdfdfd; 210 | } 211 | 212 | body>main { 213 | border-color: rgb(81, 81, 81); 214 | } 215 | 216 | body>main, 217 | button { 218 | background-color: #202023; 219 | color: #fdfdfd; 220 | } 221 | 222 | .result-row { 223 | color: #dedede; 224 | } 225 | 226 | .result-head { 227 | background-color: #191919; 228 | color: #fdfdfd; 229 | } 230 | 231 | .no-result { 232 | color: #888; 233 | } 234 | 235 | .result-cancel button { 236 | color: #202023; 237 | } 238 | 239 | .result-cancel button:hover { 240 | background-color: #ccc; 241 | } 242 | 243 | button:not(:disabled):hover { 244 | color: #2A2A2E; 245 | } 246 | 247 | textarea { 248 | background-color: #fff; 249 | } 250 | 251 | button:focus { 252 | color: gray; 253 | border-color: gray; 254 | background-color: #d2d2d2; 255 | } 256 | 257 | html.side-panel, 258 | .side-panel .result-head, 259 | .side-panel body>main, 260 | .side-panel button { 261 | background-color: #38383d; 262 | } 263 | 264 | .side-panel .result-cancel button { 265 | color: #fdfdfd; 266 | } 267 | } -------------------------------------------------------------------------------- /src/ui/pages/pendings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |   7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 | 24 | / 25 | 26 | 27 | 28 |   29 |
30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/ui/pages/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page viewer 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/ui/resources/button_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_cancel.png -------------------------------------------------------------------------------- /src/ui/resources/button_cut_inner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_cut_inner.png -------------------------------------------------------------------------------- /src/ui/resources/button_cut_outer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_cut_outer.png -------------------------------------------------------------------------------- /src/ui/resources/button_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_delete.png -------------------------------------------------------------------------------- /src/ui/resources/button_delete_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_delete_all.png -------------------------------------------------------------------------------- /src/ui/resources/button_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_download.png -------------------------------------------------------------------------------- /src/ui/resources/button_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_edit.png -------------------------------------------------------------------------------- /src/ui/resources/button_highlighter_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_highlighter_blue.png -------------------------------------------------------------------------------- /src/ui/resources/button_highlighter_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_highlighter_delete.png -------------------------------------------------------------------------------- /src/ui/resources/button_highlighter_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_highlighter_green.png -------------------------------------------------------------------------------- /src/ui/resources/button_highlighter_hidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_highlighter_hidden.png -------------------------------------------------------------------------------- /src/ui/resources/button_highlighter_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_highlighter_pink.png -------------------------------------------------------------------------------- /src/ui/resources/button_highlighter_visible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_highlighter_visible.png -------------------------------------------------------------------------------- /src/ui/resources/button_highlighter_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_highlighter_yellow.png -------------------------------------------------------------------------------- /src/ui/resources/button_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_new.png -------------------------------------------------------------------------------- /src/ui/resources/button_note_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_note_blue.png -------------------------------------------------------------------------------- /src/ui/resources/button_note_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_note_edit.png -------------------------------------------------------------------------------- /src/ui/resources/button_note_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_note_format.png -------------------------------------------------------------------------------- /src/ui/resources/button_note_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_note_green.png -------------------------------------------------------------------------------- /src/ui/resources/button_note_hidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_note_hidden.png -------------------------------------------------------------------------------- /src/ui/resources/button_note_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_note_pink.png -------------------------------------------------------------------------------- /src/ui/resources/button_note_visible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_note_visible.png -------------------------------------------------------------------------------- /src/ui/resources/button_note_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_note_yellow.png -------------------------------------------------------------------------------- /src/ui/resources/button_ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_ok.png -------------------------------------------------------------------------------- /src/ui/resources/button_print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_print.png -------------------------------------------------------------------------------- /src/ui/resources/button_redo_cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_redo_cut.png -------------------------------------------------------------------------------- /src/ui/resources/button_undo_all_cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_undo_all_cut.png -------------------------------------------------------------------------------- /src/ui/resources/button_undo_cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/button_undo_cut.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait0.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait1.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait2.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait3.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait4.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait5.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait6.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait7.png -------------------------------------------------------------------------------- /src/ui/resources/icon_128_wait8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_128_wait8.png -------------------------------------------------------------------------------- /src/ui/resources/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_16.png -------------------------------------------------------------------------------- /src/ui/resources/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_32.png -------------------------------------------------------------------------------- /src/ui/resources/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_48.png -------------------------------------------------------------------------------- /src/ui/resources/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gildas-lormeau/SingleFile/f550b1daf07efa86169c732dc4dc6f710d783e77/src/ui/resources/icon_64.png --------------------------------------------------------------------------------