Google Chrome Extension - EPUB Press

— 11 minute read

The EPUB PRESS extension allows for an open browser window to submit the content of all its tabs, to a remote server, where it will be converted into an ebook. Each tab is a chapter in said book's table of contents, and made available following the book cover image and title. The extension's popup menu is a form for declaring the book title, description, and optional settings. Each open tab is dynamically listed inside the #tab-list element as a chekbox providing the user the option to exclude content from the final output.

The resulting ebook is the product of work performed outside the browser, and its destination is determined by the value entered in the extension's delivery-email setting (i.e. the popup's input#settings-email-text). When left empty, clicking button#download targets the local file system; if text is entered in this field, the file output will go to the email server instead.

#downloadForm
#title-bar...
.container
input#book-title(placeholder)
input#book-description(placeholder='Built using https://epub.press')

#tab-list.

.btn-box
button#select-all Select All
button#select-none Select None

.btn-box
button#download Download

The popup is divided into two forms: #downloadForm and #settingsForm. The latter is accessed via a button inside the #title-bar, and is where the email output can be specified. It's also where the user specifies filetype (an option inside the select#settings-filetype-select element).

#settingsForm
#title-bar...
.container
select#settings-filetype-select
option(value='epub') .epub
option(value='mobi') .mobi (Kindle)

input#settings-email-text(placeholder='Email')

.btn-box
button#settings-save-btn Save
button#settings-cancel-btn Cancel

Clicking the settings-save-btn triggers the browser to perform the following actions:

  1. setLocalStorage with object properties: email and filetype
  2. return to the #downloadForm

Clicking the button#download, causes each selected input.article-checkbox to be added into a selectedItems object array, which in turn is used by the browser to map promises (i.e. to resolve each tab's `id` and `url` via chrome.tabs.executeScript by reference to the corresponding documentElement.outerHTML, or the content markup found inside each selected browser tab). To achieve this, EPUB Press uses its own `class Browser` and JQuery:

$('#download').click(() => {
const selectedItems = []

$('input.article-checkbox').each((index, checkbox) => {
if ($(checkbox).prop('checked')) {
selectedItems.push({
url: $(checkbox).prop('value'),
id: Number($(checkbox).prop('name'))
})
}
})

if (selectedItems.length <= 0) {...} else {
Browser.getTabsHtml(selectedItems).then((sections) => {
UI.showSection('#downloadSpinner')
Browser.sendMessage({
action: 'download',
book: {
title: $('#book-title').val() || $('#book-title').attr('placeholder'),
description: $('#book-description').val() || undefined,
sections
}
})
}).catch((error) => {
UI.setErrorMessage(`Could not find tab content: ${error}`)
})
}
})

The methods getTabsHtml and sendMessage depend on global properties: chrome.tabs and chrome.runtime, respectively, in order to coordinate a client's download request (i.e. to first getTabsHtml, then sendMessage containing a book object to the background script, where said object can be further processed).

This coordinated action is mainly facilitated by another gloabal property, chrome.storage.local, which data communicated by the user to stored in its local storage via get/set methods. Both are leveraged with promises inside the Browser class (i.e. they are resolved, or rejected, inside static members).

Browser.onForegroundMessage((request) => {
if (request.action === 'download') {
Browser.setLocalStorage({ downloadState: true, publishStatus: '{}' })
const timeout = setTimeout(timeoutDownload, DOWNLOAD_TIMEOUT)

Browser.getLocalStorage(['email', 'filetype']).then((state) => {
const book = new EpubPress(Object.assign({}, request.book))
book.on('statusUpdate', (status) => {
Browser.setLocalStorage({ publishStatus: JSON.stringify(status) })
Browser.sendMessage({
action: 'publish',
progress: status.progress,
message: status.message
})
})
book.publish()
.then(() => {
const email = state.email && state.email.trim()
const { filetype } = state
return email
? book.email(email, filetype)
: Browser.download({
filename: `${book.getTitle()}.${filetype || book.getFiletype()}`,
url: book.getDownloadUrl(filetype)
})
})
.then(() => {
clearTimeout(timeout)
Browser.setLocalStorage({ downloadState: false, publishStatus: '{}' })
Browser.sendMessage({ action: 'download', status: 'complete' })
})
.catch((e) => {
clearTimeout(timeout)
Browser.setLocalStorage({ downloadState: false, publishStatus: '{}' })
Browser.sendMessage({ action: 'download', status: 'failed', error: e.message })
})
})
}
})