diff --git a/extension/inject.css b/extension/inject.css index af0b5ae..d792e2d 100644 --- a/extension/inject.css +++ b/extension/inject.css @@ -6,7 +6,7 @@ z-index: 1001; } #epubDialogContainer .drop-down-pop-up-container .drop-down-pop-up { - width: 500px; + width: 600px; } #epubDialogContainer .drop-down-pop-up-container .drop-down-pop-up h1 { cursor: move; @@ -16,10 +16,21 @@ } +#epubDialogContainer table.properties textarea { + line-height: 1em; + font-size: 1em; + resize: vertical; +} + #epubDialogContainer table.properties td.label { white-space: nowrap; } +#epubDialogContainer table.properties input:invalid { + background-repeat: no-repeat; + background-position: 8px center; +} + #epubDialogContainer div.rating_container { padding-top: 4px; } diff --git a/src/FimFic2Epub.js b/src/FimFic2Epub.js index ebc3c8c..e6a5655 100644 --- a/src/FimFic2Epub.js +++ b/src/FimFic2Epub.js @@ -21,7 +21,7 @@ import { containerXml } from './constants' const entities = new XmlEntities() -module.exports = class FimFic2Epub extends Emitter { +class FimFic2Epub extends Emitter { static getStoryId (id) { if (isNaN(id)) { @@ -113,8 +113,10 @@ module.exports = class FimFic2Epub extends Emitter { this.storyInfo = null this.description = '' + this.subjects = [] this.chapters = [] this.remoteResources = new Map() + this.coverUrl = '' this.coverImage = null this.coverFilename = '' this.coverType = '' @@ -138,63 +140,54 @@ module.exports = class FimFic2Epub extends Emitter { this.coverImageDimensions = sizeOf(new Buffer(buffer)) } + fetchMetadata () { + this.storyInfo = null + this.description = '' + this.subjects.length = 0 + + return FimFic2Epub.fetchStoryInfo(this.storyId).then((storyInfo) => { + this.storyInfo = storyInfo + this.storyInfo.uuid = 'urn:fimfiction:' + this.storyInfo.id + this.filename = FimFic2Epub.getFilename(this.storyInfo) + this.progress(0, 0.3) + }) + .then(this.fetchTitlePage.bind(this)) + .then(() => cleanMarkup(this.description)).then((html) => { + this.storyInfo.description = html + this.findRemoteResources('description', 'description', html) + }) + } + fetch () { if (this.fetchPromise) { return this.fetchPromise } - this.storyInfo = null - this.description = '' this.chapters.length = 0 this.remoteResources.clear() - this.progress(0, 0, 'Fetching metadata...') + this.progress(0, 0, 'Fetching...') - let p = - FimFic2Epub.fetchStoryInfo(this.storyId).then((storyInfo) => { - this.storyInfo = storyInfo - this.storyInfo.uuid = 'urn:fimfiction:' + this.storyInfo.id - this.filename = FimFic2Epub.getFilename(this.storyInfo) - this.progress(0, 0.3) - }) - .then(this.fetchTitlePage.bind(this)) - .then(() => cleanMarkup(this.description)).then((html) => { - this.storyInfo.description = html - this.findRemoteResources('description', 'description', html) - }) + this.fetchPromise = Promise.resolve() + if (!this.storyInfo) { + this.fetchPromise = this.fetchPromise.then(this.fetchMetadata.bind(this)) + } + this.fetchPromise = this.fetchPromise .then(this.fetchCoverImage.bind(this)) .then(this.fetchChapters.bind(this)) - - // .then(this.processChapters.bind(this)) .then(this.fetchRemoteFiles.bind(this)) .then(() => { this.fetchPromise = null }) - this.fetchPromise = p - return p + return this.fetchPromise } build () { this.cachedFile = null this.zip = null - this.remoteResources.forEach((r, url) => { - let dest = '../' + r.dest - if (r.dest && r.originalUrl && r.where) { - let ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g') - for (var i = 0; i < r.where.length; i++) { - let w = r.where[i] - if (typeof w === 'number') { - this.chapters[w] = this.chapters[w].replace(ourl, dest) - } else if (w === 'description') { - this.storyInfo.description = this.storyInfo.description.replace(ourl, dest) - } else if (w === 'tags') { - this.tags.byImage[r.originalUrl].image = dest - } - } - } - }) + this.replaceRemoteResources() this.zip = new JSZip() @@ -226,8 +219,6 @@ module.exports = class FimFic2Epub extends Emitter { this.remoteResources.forEach((r) => { this.zip.file('OEBPS/' + r.dest, r.data) }) - - this.progress(6, 0, 'Complete!') } // for node, resolve a Buffer, in browser resolve a Blob @@ -238,6 +229,7 @@ module.exports = class FimFic2Epub extends Emitter { if (this.cachedFile) { return Promise.resolve(this.cachedFile) } + this.progress(6, 0, 'Compressing...') return this.zip .generateAsync({ @@ -247,6 +239,7 @@ module.exports = class FimFic2Epub extends Emitter { compressionOptions: {level: 9} }) .then((file) => { + this.progress(6, 0.3, 'Complete!') this.cachedFile = file return file }) @@ -269,7 +262,7 @@ module.exports = class FimFic2Epub extends Emitter { // Internal/private methods progress (part, percent, status) { - let parts = 6 + let parts = 6.3 let partsize = 1 / parts percent = (part / parts) + percent * partsize this.trigger('progress', percent, status) @@ -305,7 +298,7 @@ module.exports = class FimFic2Epub extends Emitter { return this.coverImage } this.coverImage = null - let url = this.storyInfo.full_image + let url = this.coverUrl || this.storyInfo.full_image if (!url) { return null } @@ -357,13 +350,16 @@ module.exports = class FimFic2Epub extends Emitter { html = html.substring(endCatsPos + 6) let categories = [] + this.subjects.push('Fimfiction') let matchCategory = /(.*?)<\/a>/g for (let c; (c = matchCategory.exec(catsHtml));) { - categories.push({ + let cat = { url: 'http://www.fimfiction.net' + c[1], className: c[2], name: entities.decode(c[3]) - }) + } + categories.push(cat) + this.subjects.push(cat.name) } this.categories = categories @@ -500,4 +496,25 @@ module.exports = class FimFic2Epub extends Emitter { recursive() }) } + + replaceRemoteResources () { + this.remoteResources.forEach((r, url) => { + let dest = '../' + r.dest + if (r.dest && r.originalUrl && r.where) { + let ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g') + for (var i = 0; i < r.where.length; i++) { + let w = r.where[i] + if (typeof w === 'number') { + this.chapters[w] = this.chapters[w].replace(ourl, dest) + } else if (w === 'description') { + this.storyInfo.description = this.storyInfo.description.replace(ourl, dest) + } else if (w === 'tags') { + this.tags.byImage[r.originalUrl].image = dest + } + } + } + }) + } } + +module.exports = FimFic2Epub diff --git a/src/main.js b/src/main.js index 04f5d2f..415ecf9 100644 --- a/src/main.js +++ b/src/main.js @@ -5,10 +5,12 @@ import FimFic2Epub from './FimFic2Epub' import m from 'mithril' import { saveAs } from 'file-saver' -function blobToDataURL (blob, callback) { - let fr = new FileReader() - fr.onloadend = function (e) { callback(fr.result) } - fr.readAsDataURL(blob) +function blobToDataURL (blob) { + return new Promise((resolve, reject) => { + let fr = new FileReader() + fr.onloadend = function (e) { resolve(fr.result) } + fr.readAsDataURL(blob) + }) } function blobToArrayBuffer (blob) { @@ -33,9 +35,9 @@ document.body.appendChild(dialogContainer) let checkbox = { view: function (ctrl, args, text) { - return m('label.toggleable-switch', [ - m('input', {type: 'checkbox', name: args.name, checked: args.checked}), - m('a'), + return m('label.toggleable-switch', {style: 'white-space: nowrap;'}, [ + m('input', {type: 'checkbox', name: args.name, checked: args.checked, onchange: args.onchange}), + m('a', {style: 'margin-right: 10px'}), text ]) } @@ -46,11 +48,15 @@ let ffcStatus = m.prop('') let dialog = { controller (args) { + this.isLoading = m.prop(true) this.dragging = m.prop(false) this.xpos = m.prop(0) this.ypos = m.prop(0) this.el = m.prop(null) this.coverFile = m.prop(null) + this.coverUrl = m.prop('') + this.checkboxCoverUrl = m.prop(false) + this.subjects = m.prop(ffc.subjects) this.setCoverFile = (e) => { this.coverFile(e.target.files ? e.target.files[0] : null) @@ -76,26 +82,40 @@ let dialog = { window.addEventListener('mousemove', onmove, false) window.addEventListener('mouseup', onup, false) } - this.onOpen = function (el, first) { - if (!first) { + this.onOpen = function (el, isInitialized) { + if (!isInitialized) { this.el(el) - let rect = this.el().firstChild.getBoundingClientRect() - this.xpos((window.innerWidth / 2) - (rect.width / 2) + document.body.scrollLeft) - this.ypos((window.innerHeight / 2) - (rect.height / 2) + document.body.scrollTop) - this.move() + this.center() + this.isLoading(true) + ffc.fetchMetadata().then(() => { + this.isLoading(false) + m.redraw(true) + this.center() + }) } } this.move = () => { this.el().style.left = this.xpos() + 'px' this.el().style.top = this.ypos() + 'px' } + this.center = () => { + let rect = this.el().firstChild.getBoundingClientRect() + this.xpos((window.innerWidth / 2) - (rect.width / 2) + document.body.scrollLeft) + this.ypos((window.innerHeight / 2) - (rect.height / 2) + document.body.scrollTop) + this.move() + } + this.createEpub = (e) => { ffcProgress(0) ffcStatus('') e.target.disabled = true let chain = Promise.resolve() - if (this.coverFile()) { - chain = blobToArrayBuffer(this.coverFile()).then(ffc.setCoverImage.bind(ffc)) + ffc.coverUrl = '' + ffc.coverImage = null + if (this.checkboxCoverUrl()) { + ffc.coverUrl = this.coverUrl() + } else if (this.coverFile()) { + chain = chain.then(blobToArrayBuffer.bind(null, this.coverFile())).then(ffc.setCoverImage.bind(ffc)) } m.redraw() @@ -105,7 +125,7 @@ let dialog = { .then(ffc.getFile.bind(ffc)).then((file) => { console.log('Saving file...') if (typeof safari !== 'undefined') { - blobToDataURL(file, (dataurl) => { + blobToDataURL(file).then((dataurl) => { document.location.href = dataurl alert('Add .epub to the filename of the downloaded file') }) @@ -119,28 +139,27 @@ let dialog = { view (ctrl, args, extras) { return m('.drop-down-pop-up-container', {config: ctrl.onOpen.bind(ctrl)}, m('.drop-down-pop-up', [ m('h1', {onmousedown: ctrl.ondown}, m('i.fa.fa-book'), 'Export to EPUB', m('a.close_button', {onclick: closeDialog})), - m('.drop-down-pop-up-content', [ + ctrl.isLoading() ? m('div', {style: 'text-align:center;'}, m('i.fa.fa-spin.fa-spinner', {style: 'font-size:50px; margin:20px; color:#777;'})) : m('.drop-down-pop-up-content', [ m('table.properties', [ m('tr', m('td.label', 'Custom cover image'), m('td', - // m(checkbox, {name: '', checked: true}, ' Custom cover'), - // m('input', {type: 'url', placeholder: 'Image URL'}), - // '- or -', - m('form', [ - m('input', {type: 'file', accept: 'image/*', onchange: ctrl.setCoverFile}), - m('button', {type: 'reset'}, 'Reset') - ]) + ctrl.checkboxCoverUrl() ? m('input', {type: 'url', placeholder: 'Image URL', onchange: m.withAttr('value', ctrl.coverUrl)}) : m('input', {type: 'file', accept: 'image/*', onchange: ctrl.setCoverFile}) + ), m('td', m(checkbox, {checked: ctrl.checkboxCoverUrl(), onchange: m.withAttr('checked', ctrl.checkboxCoverUrl)}, 'Use image URL'))), + m('tr', m('td.section_header', {colspan: 3}, m('b', 'Metadata'))), + m('tr', m('td.label', 'Categories'), m('td', {colspan: 2}, + m('textarea', {rows: 5}, ctrl.subjects().join('\n')), + m(checkbox, {checked: false}, 'Join categories into one (for iBooks)') )) // m('tr', m('td.label', 'Chapter headings'), m('td', m(checkbox, {checked: true}))) ]), m('.drop-down-pop-up-footer', [ m('button.styled_button', {onclick: ctrl.createEpub, disabled: ffcProgress() >= 0 && ffcProgress() < 1}, 'Create EPUB'), - ffcProgress() >= 0 ? m('.rating_container', - m('.bars_container', m('.bar_container', m('.bar_dislike', m('.bar.bar_like', {style: {width: ffcProgress() * 100 + '%'}})))), + m('.rating_container', + m('.bars_container', m('.bar_container', m('.bar_dislike', m('.bar.bar_like', {style: {width: Math.max(0, ffcProgress()) * 100 + '%'}})))), ' ', - ffcProgress() < 1 ? m('i.fa.fa-spin.fa-spinner') : null, + ffcProgress() >= 0 && ffcProgress() < 1 ? m('i.fa.fa-spin.fa-spinner') : null, ' ', ffcStatus() - ) : null + ) ]) ]) ]))