2016-06-22 21:54:37 +12:00
|
|
|
/* global chrome, safari */
|
2016-06-20 08:28:28 +12:00
|
|
|
|
2016-06-21 21:32:49 +12:00
|
|
|
import JSZip from 'jszip'
|
2016-06-21 09:04:08 +12:00
|
|
|
import escapeStringRegexp from 'escape-string-regexp'
|
|
|
|
import { saveAs } from 'file-saver'
|
2016-06-23 00:48:51 +12:00
|
|
|
import zeroFill from 'zero-fill'
|
2016-06-20 08:28:28 +12:00
|
|
|
|
2016-06-21 21:32:49 +12:00
|
|
|
import styleCss from './style'
|
|
|
|
import coverstyleCss from './coverstyle'
|
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
import fetch from './fetch'
|
|
|
|
import parseChapter from './parseChapter'
|
|
|
|
import * as template from './templates'
|
|
|
|
import { mimeMap, containerXml } from './constants'
|
2016-06-20 08:28:28 +12:00
|
|
|
|
2016-06-21 09:04:08 +12:00
|
|
|
const STORY_ID = document.location.pathname.match(/^\/story\/(\d*)/)[1]
|
2016-06-20 08:28:28 +12:00
|
|
|
|
2016-06-21 02:36:56 +12:00
|
|
|
let storyInfo
|
|
|
|
let remoteResources = new Map()
|
2016-06-23 00:48:51 +12:00
|
|
|
let chapterContent = []
|
2016-06-24 01:26:01 +12:00
|
|
|
let safariQueue = {}
|
2016-06-20 08:28:28 +12:00
|
|
|
|
2016-06-21 09:04:08 +12:00
|
|
|
let epubButton = document.querySelector('.story_container ul.chapters li.bottom a[title="Download Story (.epub)"]')
|
|
|
|
let isDownloading = false
|
|
|
|
let cachedBlob = null
|
|
|
|
|
|
|
|
if (epubButton) {
|
2016-06-21 18:39:26 +12:00
|
|
|
epubButton.addEventListener('click', function (e) {
|
|
|
|
e.preventDefault()
|
|
|
|
if (isDownloading) {
|
2016-06-22 17:26:38 +12:00
|
|
|
alert("Calm down, I'm working on it (it's processing)")
|
2016-06-21 18:39:26 +12:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if (cachedBlob) {
|
|
|
|
saveStory()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
downloadStory()
|
|
|
|
}, false)
|
2016-06-21 09:04:08 +12:00
|
|
|
}
|
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
function blobToDataURL (blob, callback) {
|
|
|
|
let a = new FileReader()
|
|
|
|
a.onloadend = function (e) { callback(a.result) }
|
|
|
|
a.readAsDataURL(blob)
|
2016-06-21 02:36:56 +12:00
|
|
|
}
|
2016-06-20 08:28:28 +12:00
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
function saveStory () {
|
|
|
|
console.log('Saving epub...')
|
|
|
|
if (typeof safari !== 'undefined') {
|
|
|
|
blobToDataURL(cachedBlob, (dataurl) => {
|
|
|
|
document.location.href = dataurl
|
|
|
|
alert('Rename downloaded file to .epub')
|
2016-06-21 18:39:26 +12:00
|
|
|
})
|
2016-06-24 01:26:01 +12:00
|
|
|
} else {
|
|
|
|
saveAs(cachedBlob, storyInfo.title + ' by ' + storyInfo.author.name + '.epub')
|
2016-06-21 18:39:26 +12:00
|
|
|
}
|
2016-06-20 08:28:28 +12:00
|
|
|
}
|
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
// messaging with the safari extension global page
|
2016-06-22 21:54:37 +12:00
|
|
|
function safariHandler (ev) {
|
|
|
|
let type = ev.message.type
|
|
|
|
let url = ev.message.input
|
|
|
|
let data = ev.message.output // arraybuffer
|
|
|
|
if (!safariQueue[url]) {
|
|
|
|
// console.error("Unable to get callback for " + url, JSON.stringify(safariQueue))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let cb = safariQueue[url].cb
|
|
|
|
let responseType = safariQueue[url].responseType
|
|
|
|
console.log(url, cb, responseType, data)
|
|
|
|
delete safariQueue[url]
|
|
|
|
|
|
|
|
if (responseType === 'blob') {
|
|
|
|
let blob = new Blob([data], {type: type})
|
|
|
|
cb(blob, type)
|
|
|
|
} else {
|
|
|
|
if (!responseType) {
|
|
|
|
let blob = new Blob([data], {type: type})
|
|
|
|
let fr = new FileReader()
|
|
|
|
fr.onloadend = function () {
|
|
|
|
cb(fr.result, type)
|
|
|
|
}
|
|
|
|
fr.readAsText(blob)
|
|
|
|
} else {
|
|
|
|
cb(data, type)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (typeof safari !== 'undefined') {
|
|
|
|
safari.self.addEventListener('message', safariHandler, false)
|
|
|
|
}
|
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
function fetchBackground (url, cb, responseType) {
|
2016-06-22 21:54:37 +12:00
|
|
|
if (typeof chrome !== 'undefined' && chrome.runtime.sendMessage) {
|
|
|
|
chrome.runtime.sendMessage(url, function (objurl) {
|
|
|
|
fetch(objurl, cb, responseType)
|
|
|
|
URL.revokeObjectURL(objurl)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
safariQueue[url] = {cb: cb, responseType: responseType}
|
|
|
|
safari.self.tab.dispatchMessage('remote', url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
function fetchRemote (url, cb, responseType) {
|
|
|
|
if (url.indexOf('//') === 0) {
|
|
|
|
url = 'http:' + url
|
|
|
|
}
|
|
|
|
if (document.location.protocol === 'https:' && url.indexOf('http:') === 0) {
|
|
|
|
fetchBackground(url, cb, responseType)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
fetch(url, (data, type) => {
|
|
|
|
if (!data) {
|
|
|
|
fetchBackground(url, cb, responseType)
|
|
|
|
} else {
|
|
|
|
cb(data, type)
|
|
|
|
}
|
|
|
|
}, responseType)
|
|
|
|
}
|
|
|
|
|
2016-06-22 21:54:37 +12:00
|
|
|
function fetchRemoteFiles (zip, cb) {
|
2016-06-21 18:39:26 +12:00
|
|
|
let iter = remoteResources.entries()
|
|
|
|
let counter = 0
|
|
|
|
|
|
|
|
function recursive () {
|
|
|
|
let r = iter.next().value
|
|
|
|
if (!r) {
|
|
|
|
cb()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let url = r[0]
|
|
|
|
r = r[1]
|
2016-06-23 00:48:51 +12:00
|
|
|
console.log('Fetching remote file ' + (counter + 1) + ' of ' + remoteResources.size + ': ' + r.filename, url)
|
2016-06-24 01:26:01 +12:00
|
|
|
fetchRemote(url, (data, type) => {
|
2016-06-22 21:54:37 +12:00
|
|
|
r.dest = null
|
|
|
|
r.type = type
|
|
|
|
let dest = mimeMap[type]
|
|
|
|
|
|
|
|
if (dest) {
|
|
|
|
r.dest = dest.replace('*', r.filename)
|
|
|
|
zip.file(r.dest, data)
|
2016-06-21 18:39:26 +12:00
|
|
|
}
|
2016-06-22 21:54:37 +12:00
|
|
|
counter++
|
|
|
|
recursive()
|
|
|
|
}, 'arraybuffer')
|
2016-06-21 18:39:26 +12:00
|
|
|
}
|
|
|
|
recursive()
|
2016-06-20 08:28:28 +12:00
|
|
|
}
|
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
function fetchChapters (cb) {
|
|
|
|
let chapters = storyInfo.chapters
|
|
|
|
let chapterCount = storyInfo.chapters.length
|
|
|
|
let currentChapter = 0
|
|
|
|
function recursive () {
|
|
|
|
let ch = chapters[currentChapter]
|
|
|
|
console.log('Fetching chapter ' + (currentChapter + 1) + ' of ' + chapters.length + ': ' + ch.title)
|
|
|
|
fetchRemote(ch.link.replace('http', 'https'), (html) => {
|
|
|
|
parseChapter(currentChapter, ch, html, remoteResources, (html) => {
|
|
|
|
chapterContent[currentChapter] = html
|
|
|
|
currentChapter++
|
|
|
|
if (currentChapter < chapterCount) {
|
|
|
|
recursive()
|
|
|
|
} else {
|
|
|
|
cb()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
recursive()
|
|
|
|
}
|
|
|
|
|
2016-06-21 18:39:26 +12:00
|
|
|
function downloadStory () {
|
|
|
|
isDownloading = true
|
2016-06-21 21:32:49 +12:00
|
|
|
|
|
|
|
const zip = new JSZip()
|
|
|
|
zip.file('mimetype', 'application/epub+zip')
|
2016-06-24 01:26:01 +12:00
|
|
|
zip.folder('META-INF').file('container.xml', containerXml)
|
2016-06-21 21:32:49 +12:00
|
|
|
|
2016-06-23 00:48:51 +12:00
|
|
|
console.log('Fetching story metadata...')
|
2016-06-21 18:39:26 +12:00
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
fetchRemote('https://www.fimfiction.net/api/story.php?story=' + STORY_ID, (raw, type) => {
|
2016-06-21 18:39:26 +12:00
|
|
|
let data
|
|
|
|
try {
|
|
|
|
data = JSON.parse(raw)
|
|
|
|
} catch (e) {
|
|
|
|
console.log('Unable to fetch story json')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
storyInfo = data.story
|
|
|
|
storyInfo.uuid = 'urn:fimfiction:' + storyInfo.id
|
|
|
|
storyInfo.publishDate = '1970-01-01' // TODO!
|
2016-06-23 00:48:51 +12:00
|
|
|
|
2016-06-21 18:39:26 +12:00
|
|
|
remoteResources.set(storyInfo.full_image, {filename: 'cover'})
|
|
|
|
let coverImage = new Image()
|
|
|
|
coverImage.src = storyInfo.full_image
|
|
|
|
|
2016-06-21 21:32:49 +12:00
|
|
|
zip.file('style.css', styleCss)
|
|
|
|
zip.file('coverstyle.css', coverstyleCss)
|
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
coverImage.addEventListener('load', () => {
|
|
|
|
zip.file('toc.ncx', template.createNcx(storyInfo))
|
|
|
|
zip.file('nav.xhtml', template.createNav(storyInfo))
|
2016-06-21 18:39:26 +12:00
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
fetchChapters(() => {
|
|
|
|
fetchRemoteFiles(zip, () => {
|
|
|
|
let coverFilename = ''
|
2016-06-21 18:39:26 +12:00
|
|
|
remoteResources.forEach((r, url) => {
|
2016-06-23 01:43:25 +12:00
|
|
|
if (typeof r.chapter !== 'undefined' && r.originalUrl && r.dest) {
|
2016-06-21 18:39:26 +12:00
|
|
|
chapterContent[r.chapter] = chapterContent[r.chapter].replace(
|
|
|
|
new RegExp(escapeStringRegexp(r.originalUrl), 'g'),
|
|
|
|
r.dest
|
|
|
|
)
|
2016-06-24 01:26:01 +12:00
|
|
|
}
|
|
|
|
if (r.filename === 'cover') {
|
|
|
|
coverFilename = r.dest
|
2016-06-21 18:39:26 +12:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2016-06-23 00:48:51 +12:00
|
|
|
for (let num = 0; num < chapterContent.length; num++) {
|
|
|
|
let html = chapterContent[num]
|
|
|
|
let filename = 'chapter_' + zeroFill(3, num + 1) + '.xhtml'
|
2016-06-21 18:39:26 +12:00
|
|
|
zip.file(filename, html)
|
|
|
|
}
|
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
chapterContent.length = 0
|
2016-06-21 18:39:26 +12:00
|
|
|
|
2016-06-24 01:26:01 +12:00
|
|
|
zip.file('cover.xhtml', template.createCoverPage(coverFilename, coverImage.width, coverImage.height))
|
|
|
|
zip.file('content.opf', template.createOpf(storyInfo, remoteResources))
|
2016-06-21 18:39:26 +12:00
|
|
|
|
2016-06-22 21:54:37 +12:00
|
|
|
console.log('Packaging epub...')
|
2016-06-21 18:39:26 +12:00
|
|
|
|
|
|
|
zip
|
|
|
|
.generateAsync({
|
|
|
|
type: 'blob',
|
|
|
|
mimeType: 'application/epub+zip',
|
|
|
|
compression: 'DEFLATE',
|
|
|
|
compressionOptions: {level: 9}
|
|
|
|
})
|
|
|
|
.then((blob) => {
|
|
|
|
cachedBlob = blob
|
|
|
|
saveStory()
|
|
|
|
isDownloading = false
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}, false)
|
|
|
|
})
|
2016-06-21 09:04:08 +12:00
|
|
|
}
|