diff --git a/README.md b/README.md index 4877ebe..2ebb6de 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,39 @@ -# ePub Creator -- create ePubs from web pages and `overdrive.com`s online reader +# ePub Creator -- offline e-books in a single click - + + + + ePub creator allows you to save web pages opened in the browser as offline ePub e-books. It currently supports: -Instructions: +How to create/save books:
    -
  1. open the page / book you want to save (on overdrive, go to "Loans" and choose "Read now in browser")
  2. +
  3. open the web page or book you want to save
  4. click the extensions icon (blue book with green arrow, should be at the top right of the browser, see screenshot)
  5. -
  6. wait while the animation on the icon is spinning (this can take a while in the e-book contains many pictures etc.)
  7. +
  8. wait while the animation on the icon is spinning
  9. save or open the e-book when prompted
-Whether saving content this way is legal or not depends on the content and your local legislation. Checking this is your own responsibility. Just because you can do it doesn't mean you should. +Whether saving content with this extension is legal or not depends on the content and your local legislation. Checking that is your own responsibility. Just because you can do it doesn't mean you should. + + +What you get & Troubleshooting: + + Permissions used: + ## Development builds -- ![](https://ci.appveyor.com/api/projects/status/github/NiklasGollenstede/epub-creator?svg=true) @@ -52,7 +70,7 @@ The `.zip` file is ready to be uploaded on AMO, and the `build/` directory or th To test the extension in a fresh Firefox profile, use the `{run:1}` or `{run:{bin:'path/to firefox/binary'}}` option. -To build for chrome (which e.g. doesn't support `.svg` icons), add the `{chrome:1}` option. +To build for Chrome (which e.g. doesn't support `.svg` icons), add the `{chrome:1}` option. ## AMO code review notes diff --git a/background/index.js b/background/index.js index 17362dd..5ad06e3 100644 --- a/background/index.js +++ b/background/index.js @@ -1,5 +1,5 @@ (function(global) { 'use strict'; define(({ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - 'node_modules/web-ext-utils/browser/': { BrowserAction, manifest, }, + 'node_modules/web-ext-utils/browser/': { BrowserAction, Tabs, manifest, }, 'node_modules/web-ext-utils/loader/': { runInFrame, }, 'node_modules/web-ext-utils/update/': updated, 'node_modules/web-ext-utils/utils/notify': notify, @@ -12,16 +12,23 @@ async function onClicked(tab) { return spinner.run(async () => { let collector, name = null; if (tab.isInReaderMode) { - name = (await (await require.async('./reader-mode'))(tab.url)); - } else if (/^https:\/\/[^/]*read\.overdrive\.com/.test(tab.url)) { + notify({ + title: `Exit reader mode?`, icon: 'default', // 'prompt', + message: `${manifest.name} can only save this page after closing the reader mode.\nClick here if you want leave reader mode.\nThen try again.`, + }).then(async clicked => + clicked && (await Tabs.get(tab.id)).isInReaderMode && Tabs.toggleReaderMode(tab.id) + ); return; + } else if ((/^https:[/][/][^/]*read[.]overdrive[.]com/).test(tab.url)) { collector = 'overdrive'; - } else if (tab.isArticle) { + } else if (tab.isArticle || !('isArticle' in tab)) { collector = 'readability'; } else { notify({ title: `Force reader mode?`, icon: 'warn', message: `It seems ${manifest.name} doesn't support this page.\nClick here if you want to TRY to force a book from reader mode.`, - }).then(async clicked => clicked && onClicked({ isArticle: true, id: tab.id, })); return; + }).then(async clicked => + clicked && onClicked({ isArticle: true, id: tab.id, }) + ); return; } collector && (name = (await runInFrame(tab.id, 0, @@ -31,7 +38,7 @@ async function onClicked(tab) { return spinner.run(async () => { if (name) { console.info(`Saved book "${name}"`); } else { console.info(`Saving as book was aborted`); } -}).catch(notify.error); } +}).catch(notify.error.bind(null, 'Failed to save as ePub')); } const spinner = { active: 0, strings: String.raw`\ | / –`.split(' '), diff --git a/background/reader-mode.js b/background/reader-mode.js deleted file mode 100644 index d3e7164..0000000 --- a/background/reader-mode.js +++ /dev/null @@ -1,83 +0,0 @@ -(function(global) { 'use strict'; define(({ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - 'node_modules/web-ext-utils/browser/': { Tabs, Sessions, Windows, manifest, }, - 'node_modules/web-ext-utils/loader/views': { openView, }, - 'shim!node_modules/readability/Readability:Readability': Readability, -}) => async url => { /* globals fetch, */ - -/** - * Can't access pages in reader mode (yet?), so this emulates the reader mode - * in an extension view to transparently run content scripts in it. - */ - -// normalize url and start document download -// TODO: should grab the DOM of the rendered page (after 'load' event?) -url = url.replace(/^about:reader\?url=(.*)$/, (_, url) => decodeURIComponent(url)); -const getHtml = fetch(url).then(_=>_.text()); - -// prepare a fake content page (needs to be visible to load) -const { view, tabId, windowId, } = (await openView( - 'reader-mode-fix', 'popup', { width: 500, height: 500, }, // must be large enough for prompt() -)); try { - - // remove this popup from the window history once closed - windowId && Sessions && Windows && Windows.onRemoved.addListener(async function forget(closedId) { - if (closedId !== windowId) { return; } Windows.onRemoved.removeListener(forget); - const session = (await Sessions.getRecentlyClosed({ maxResults: 1, }))[0]; - session && session.window && Sessions.forgetClosedWindow(session.window.sessionId); - }); - - // add "UI" - view.document.head.innerHTML = ` -`; - view.document.body.innerHTML = ` -

Loading ...
Please keep this open.

`; - - // load the module loader - (await new Promise((callback, errback) => { - view.require = { callback, errback, }; - const script = view.document.createElement('script'); script.onerror = errback; - script.src = 'node_modules/pbq/require.js?baseUrl=/'; - view.document.head.appendChild(script); - })); - const { require, } = view, collector = 'about-reader'; - - // try to parse the fetched HTML - const parsed = new Readability(new view.DOMParser().parseFromString((await getHtml), 'text/html'), { }).parse(); - if (!parsed) { const error = new Error(` -The version of the reader mode included with ${manifest.name} was unable to parse this article. -This can happen for pages that load their content dynamically. -Please close the reader mode and try again! - `.trim()); error.title = 'Page could not be parsed'; throw error; } - - // build the reader mode DOM content - const document = new view.DOMParser().parseFromString(` - -
-
- $uri.host -

$title

-
$credits
-
-
$content
-
`, 'text/html'); - document.querySelector('.reader-domain').href = url || ''; - document.querySelector('.reader-domain').textContent = (new URL(url).host || '').replace(/^www\./, ''); - document.querySelector('.reader-title').textContent = parsed.title || ''; - document.querySelector('.reader-credits').textContent = parsed.byline || ''; - document.querySelector('.container>.content').innerHTML = parsed.content; // this is not a live document, so this should be unproblematic: https://github.com/mozilla/readability/issues/404 - - // and now we can pretend that we are in a content script on `about:reader?url=${url}`: - const name = (await require.async('content/collect').then(_=>_(collector, { document, }))); - - view.document.body.innerHTML = name ? `

Done!
Please close this window once the ePub is saved.

` : `

Canceled!
Please close this window.

`; - return name || false; -} catch (error) { - Tabs.remove(tabId); - throw error; -} - -}); })(this); diff --git a/build-config.js b/build-config.js index f571448..9028009 100644 --- a/build-config.js +++ b/build-config.js @@ -1,11 +1,14 @@ /*eslint strict: ["error", "global"], no-implicit-globals: "off"*/ 'use strict'; /* globals module, */ // license: MPL-2.0 -module.exports = function({ options, /*packageJson,*/ manifestJson, files, }) { +module.exports = function({ options, packageJson, manifestJson, files, }) { + + manifestJson.description = `Create offline e-books from web pages and 'overdrive.com' with a single click on the icon.`; + manifestJson.homepage_url = packageJson.homepage; manifestJson.permissions.push( 'notifications', 'sessions', // to remove own closed popups 'activeTab', - '', // for fetch in background/reader-mode.js + '', // required to fetch DORS stuff ); manifestJson.browser_action = { diff --git a/common/options.js b/common/options.js index 63d3db9..ca75b23 100644 --- a/common/options.js +++ b/common/options.js @@ -6,7 +6,7 @@ const model = { setNavProperty: { title: `Set 'nav' property`, - description: `Standard compliant when set, but disables the navigation in Sumatra PDF`, + description: `Standard compliant when set, but disables the navigation in Sumatra PDF.`, default: false, input: { type: 'boolean', }, }, diff --git a/content/collect/about-reader.js b/content/collect/about-reader.js index 3398226..5c7f522 100644 --- a/content/collect/about-reader.js +++ b/content/collect/about-reader.js @@ -7,7 +7,7 @@ const doc = document.querySelector('.container').cloneNode(true); -const resources = (await Promise.all(Array.map(doc.querySelectorAll('img'), async img => { +const resources = (await Promise.all(Array.from(doc.querySelectorAll('img'), async img => { const { src, } = img, name = img.src = (await sha1(src)) +'/'+ src.match(/[^/]*[/]?(?:[?]|#|$)/)[0]; return { src, name, }; }))); @@ -19,8 +19,8 @@ const author = global.prompt('Please enter/confirm the authors name', ( ).textContent.replace(/\s+/g, ' ') || ''); if (author == null) { return null; } -Array.forEach(doc.querySelectorAll('style, link, menu'), element => element.remove()); -Array.forEach(doc.querySelectorAll('*'), element => { +doc.querySelectorAll('style, link, menu').forEach(element => element.remove()); +doc.querySelectorAll('*').forEach(element => { for (let i = element.attributes.length; i-- > 0;) { const attr = element.attributes[i]; if ([ 'class', 'src', 'href', 'title', 'alt', ].includes(attr.name)) { continue; } diff --git a/content/collect/overdrive.js b/content/collect/overdrive.js index 48591e8..b7e7dd9 100644 --- a/content/collect/overdrive.js +++ b/content/collect/overdrive.js @@ -56,7 +56,7 @@ const chapters = (await Promise.all(bData.spine.map(async ({ .italic { font-style: italic; } .underline { text-decoration: underline; } .bold { font-weight: bold; } -` ); + `); options.styles && (css += Array.from(styles, ([ style, index, ]) => `\t\t.inline-${index} { ${style} }\n`).join('')); // html clean-up @@ -91,7 +91,7 @@ const chapters = (await Promise.all(bData.spine.map(async ({ return ({ name: name && decodeURI(name) || 'unnamed'+ index +'.html', title, - content: toXML(document).replace(/^.*$/m, ``), + content: toXML(document).replace(/^.*?>/, `\n\n`), mimeType: 'xhtml', // toXML produces xhtml linear, }); diff --git a/content/collect/readability.js b/content/collect/readability.js index 742f578..31a9bb6 100644 --- a/content/collect/readability.js +++ b/content/collect/readability.js @@ -3,10 +3,12 @@ 'shim!node_modules/readability/Readability:Readability': Readability, aboutReader, }) => async function collect({ document: srcDoc = global.document.cloneNode(true), url = srcDoc.URL, } = { }) { - /** - * Can't access pages in reader mode any more, so this emulates the reader mode + * Collects the contents of any website that can be opened in the `about:reader` view. + * Since extensions can't access pages in reader mode any more, this emulates the reader mode * in the original content page and then proceeds as if it was loaded in `about:reader`. + * (This workaround is kept for now to be able to switch back to the reader view if it ever becomes accessible again.) + * @return {object} Options that can be passed as argument to the EPub constructor. */ const parsed = new Readability(srcDoc, { }).parse(); diff --git a/package.json b/package.json index 981f4c3..32bbb47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epub", - "version": "0.5.4", + "version": "0.5.5", "title": "ePub Creator", "description": "ePub Creator is a WebExtension that creates ePub e-books from web pages and 'overdrive.com's online reader.", "author": "Niklas Gollenstede", @@ -9,6 +9,8 @@ "type": "git", "url": "https://github.com/NiklasGollenstede/epub-creator" }, + "homepage": "https://github.com/NiklasGollenstede/epub-creator#readme", + "keywords": [ "book", "e-book", "ePub", "website", "article", "save", "offline", "one click", "overdrive.com", "reader mode" ], "contributions": [ { "what": { "name": "Readability.js", "url": "https://github.com/mozilla/readability" }, "who": [ "Mozilla", "Arc90" ], @@ -26,10 +28,10 @@ "es6lib": "0.0.3", "jszip": "github:Stuk/jszip#v3.1.5", "multiport": "0.2.3", - "pbq": "0.3.5", - "readability": "github:mozilla/readability#d8c8370", + "pbq": "0.4.1", + "readability": "github:mozilla/readability#26379fe", "web-ext-build": "0.0.10", - "web-ext-utils": "0.1.8" + "web-ext-utils": "0.1.11" }, "devDependencies": { "eslintrc": "github:NiklasGollenstede/eslintrc#5837452", diff --git a/resources/chrome-web-store-screenshot-640x400.png b/resources/chrome-web-store-screenshot-640x400.png new file mode 100644 index 0000000..5184714 Binary files /dev/null and b/resources/chrome-web-store-screenshot-640x400.png differ diff --git a/resources/get-chrome-ext-206x58.png b/resources/get-chrome-ext-206x58.png new file mode 100644 index 0000000..c541262 Binary files /dev/null and b/resources/get-chrome-ext-206x58.png differ diff --git a/resources/get-firefox-ext-172x60.png b/resources/get-firefox-ext-172x60.png new file mode 100644 index 0000000..952734f Binary files /dev/null and b/resources/get-firefox-ext-172x60.png differ