diff --git a/bin/fimfic2epub b/bin/fimfic2epub index 31d8d7b..a7113a8 100755 --- a/bin/fimfic2epub +++ b/bin/fimfic2epub @@ -14,7 +14,13 @@ if (outputStdout) { console.log('Outputting to stdout') } -ffc.fetch() +ffc.on('progress', (percent, status) => { + if (status) { + console.log('fimfic2epub: ' + status) + } +}) + +ffc.fetchAll() .then(ffc.build.bind(ffc)) .then(() => { let filename = process.argv[3] || ffc.filename diff --git a/extension/inject.css b/extension/inject.css index d792e2d..c91616f 100644 --- a/extension/inject.css +++ b/extension/inject.css @@ -4,9 +4,11 @@ top: 0; left: 0; z-index: 1001; + padding: 0; + margin: 0; } #epubDialogContainer .drop-down-pop-up-container .drop-down-pop-up { - width: 600px; + width: 650px; } #epubDialogContainer .drop-down-pop-up-container .drop-down-pop-up h1 { cursor: move; @@ -17,8 +19,8 @@ } #epubDialogContainer table.properties textarea { - line-height: 1em; - font-size: 1em; + line-height: 1.2em; + font-size: 0.95em; resize: vertical; } @@ -32,11 +34,12 @@ } #epubDialogContainer div.rating_container { - padding-top: 4px; + padding-top: 10px; } #epubDialogContainer div.rating_container div.bar_container { width: 150px; + margin-right: 4px; } #epubDialogContainer div.rating_container div.bar_dislike { diff --git a/src/FimFic2Epub.js b/src/FimFic2Epub.js index 15575ab..b8b482a 100644 --- a/src/FimFic2Epub.js +++ b/src/FimFic2Epub.js @@ -13,6 +13,7 @@ import Emitter from 'es6-event-emitter' import { styleCss, coverstyleCss, titlestyleCss } from './styles' import { cleanMarkup } from './cleanMarkup' +import htmlWordCount from './html-wordcount' import fetch from './fetch' import fetchRemote from './fetchRemote' import * as template from './templates' @@ -87,15 +88,27 @@ class FimFic2Epub extends Emitter { this.options = { addCommentsLink: true, includeAuthorNotes: true, - addChapterHeadings: true + addChapterHeadings: true, + includeExternal: true, + + joinSubjects: false } - this.fetchPromise = null + // promise cache + this.pcache = { + metadata: null, + chapters: null, + remoteResources: null, + coverImage: null, + fetchAll: null + } this.storyInfo = null this.description = '' this.subjects = [] this.chapters = [] + this.chaptersHtml = [] + this.remoteResourcesCached = false this.remoteResources = new Map() this.coverUrl = '' this.coverImage = null @@ -103,6 +116,10 @@ class FimFic2Epub extends Emitter { this.coverType = '' this.coverImageDimensions = {width: 0, height: 0} + this.hasRemoteResources = { + titlePage: false + } + this.cachedFile = null this.categories = [] this.tags = [] @@ -110,103 +127,236 @@ class FimFic2Epub extends Emitter { this.zip = null } - setCoverImage (buffer) { - let info = fileType(isNode ? buffer : new Uint8Array(buffer)) - if (!info || info.mime.indexOf('image/') !== 0) { - throw new Error('Invalid image') + fetchAll () { + if (this.pcache.fetchAll) { + return this.pcache.fetchAll } - this.coverImage = buffer - this.coverFilename = 'Images/cover.' + info.ext - this.coverType = info.mime - 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) - }) - .then(this.fetchTitlePage.bind(this)) - .then(() => cleanMarkup(this.description)).then((html) => { - this.storyInfo.description = html - this.findRemoteResources('description', 'description', html) - }) - } - - setTitle (title) { - this.storyInfo.title = title.trim() - this.filename = FimFic2Epub.getFilename(this.storyInfo) - } - setAuthorName (name) { - this.storyInfo.author.name = name.trim() - this.filename = FimFic2Epub.getFilename(this.storyInfo) - } - - fetch () { - if (this.fetchPromise) { - return this.fetchPromise - } - - this.chapters.length = 0 - this.remoteResources.clear() this.progress(0, 0, 'Fetching...') - 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)) + this.pcache.fetchAll = this.fetchMetadata() .then(this.fetchChapters.bind(this)) + .then(this.fetchCoverImage.bind(this)) .then(this.fetchRemoteFiles.bind(this)) .then(() => { - this.fetchPromise = null + this.progress(0, 0.95) + this.pcache.fetchAll = null }) - return this.fetchPromise + return this.pcache.fetchAll + } + + fetchMetadata () { + if (this.pcache.metadata) { + return this.pcache.metadata + } + if (this.storyInfo) { + return Promise.resolve() + } + this.storyInfo = null + this.description = '' + this.subjects = [] + + this.progress(0, 0, 'Fetching metadata...') + + this.pcache.metadata = 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.5) + }) + .then(this.fetchTitlePage.bind(this)) + .then(() => { + this.progress(0, 1) + }) + .then(() => cleanMarkup(this.description)).then((html) => { + this.storyInfo.description = html + this.findRemoteResources('description', 'description', html) + }).then(() => { + this.pcache.metadata = null + }) + return this.pcache.metadata + } + + fetchChapters () { + if (this.pcache.chapters) { + return this.pcache.chapters + } + // chapters have already been fetched + if (this.chapters.length !== 0) { + return Promise.resolve() + } + this.chapters.length = 0 + this.chaptersHtml.length = 0 + + this.progress(0, 0, 'Fetching chapters...') + this.pcache.chapters = new Promise((resolve, reject) => { + let chapters = this.storyInfo.chapters + let chapterCount = this.storyInfo.chapters.length + let currentChapter = 0 + let completeCount = 0 + + if (chapterCount === 0) { + resolve() + return + } + + let recursive = () => { + let index = currentChapter++ + let ch = chapters[index] + if (!ch) { + return + } + // console.log('Fetching chapter ' + (index + 1) + ' of ' + chapters.length + ': ' + ch.title) + let url = ch.link.replace('http://www.fimfiction.net', '') + fetch(url).then((html) => { + let chapter = this.parseChapterPage(html) + Promise.all([ + cleanMarkup(chapter.content), + cleanMarkup(chapter.notes) + ]).then((values) => { + chapter.content = values[0] + chapter.notes = values[1] + ch.realWordCount = htmlWordCount(chapter.content) + this.chapters[index] = chapter + + completeCount++ + this.progress(0, completeCount / chapterCount, 'Fetched chapter ' + (completeCount) + ' / ' + chapters.length) + if (completeCount < chapterCount) { + recursive() + } else { + resolve() + } + }) + }) + } + + // concurrent downloads! + recursive() + recursive() + recursive() + recursive() + }).then(() => { + this.pcache.chapters = null + }) + return this.pcache.chapters + } + + fetchRemoteFiles () { + if (!this.options.includeExternal) { + return Promise.resolve() + } + if (this.pcache.remoteResources) { + return this.pcache.remoteResources + } + if (this.remoteResourcesCached) { + return Promise.resolve() + } + + this.progress(0, 0, 'Fetching remote files...') + this.pcache.remoteResources = new Promise((resolve, reject) => { + let iter = this.remoteResources.entries() + let count = 0 + let completeCount = 0 + + let recursive = () => { + let r = iter.next().value + if (!r) { + if (completeCount === this.remoteResources.size) { + resolve() + } + return + } + let url = r[0] + r = r[1] + + count++ + + fetchRemote(url, 'arraybuffer').then((data) => { + r.dest = null + let info = fileType(isNode ? data : new Uint8Array(data)) + if (info) { + let type = info.mime + r.type = type + let isImage = type.indexOf('image/') === 0 + let folder = isImage ? 'Images' : 'Misc' + let dest = folder + '/*.' + info.ext + r.dest = dest.replace('*', r.filename) + r.data = data + } + completeCount++ + this.progress(0, completeCount / this.remoteResources.size, 'Fetched remote file ' + completeCount + ' / ' + this.remoteResources.size) + recursive() + }) + } + + // concurrent downloads! + recursive() + recursive() + recursive() + recursive() + }).then(() => { + this.remoteResourcesCached = true + this.pcache.remoteResources = null + }) + return this.pcache.remoteResources + } + + buildChapters () { + let chain = Promise.resolve() + this.chaptersHtml.length = 0 + + for (let i = 0; i < this.chapters.length; i++) { + let ch = this.storyInfo.chapters[i] + let chapter = this.chapters[i] + chain = chain.then(template.createChapter.bind(null, ch, chapter, this)).then((html) => { + this.findRemoteResources('ch_' + zeroFill(3, i + 1), i, html) + this.chaptersHtml[i] = html + }) + } + + return chain } build () { this.cachedFile = null this.zip = null - this.replaceRemoteResources() + return this.buildChapters().then(() => { + this.replaceRemoteResources() - this.zip = new JSZip() + this.zip = new JSZip() - this.zip.file('mimetype', 'application/epub+zip') - this.zip.file('META-INF/container.xml', containerXml) + this.zip.file('mimetype', 'application/epub+zip') + this.zip.file('META-INF/container.xml', containerXml) - this.zip.file('OEBPS/content.opf', template.createOpf(this)) + this.zip.file('OEBPS/content.opf', template.createOpf(this)) - if (this.coverImage) { - this.zip.file('OEBPS/' + this.coverFilename, this.coverImage) - } - this.zip.file('OEBPS/Text/cover.xhtml', template.createCoverPage(this)) - this.zip.file('OEBPS/Styles/coverstyle.css', coverstyleCss) + if (this.coverImage) { + this.zip.file('OEBPS/' + this.coverFilename, this.coverImage) + } + this.zip.file('OEBPS/Text/cover.xhtml', template.createCoverPage(this)) + this.zip.file('OEBPS/Styles/coverstyle.css', coverstyleCss) - this.zip.file('OEBPS/Text/title.xhtml', template.createTitlePage(this)) - this.zip.file('OEBPS/Styles/titlestyle.css', titlestyleCss) + this.zip.file('OEBPS/Text/title.xhtml', template.createTitlePage(this)) + this.zip.file('OEBPS/Styles/titlestyle.css', titlestyleCss) - this.zip.file('OEBPS/Text/nav.xhtml', template.createNav(this)) - this.zip.file('OEBPS/toc.ncx', template.createNcx(this)) + this.zip.file('OEBPS/Text/nav.xhtml', template.createNav(this)) + this.zip.file('OEBPS/toc.ncx', template.createNcx(this)) - for (let i = 0; i < this.chapters.length; i++) { - let filename = 'OEBPS/Text/chapter_' + zeroFill(3, i + 1) + '.xhtml' - let html = this.chapters[i] - this.zip.file(filename, html) - } + for (let i = 0; i < this.chapters.length; i++) { + let filename = 'OEBPS/Text/chapter_' + zeroFill(3, i + 1) + '.xhtml' + let html = this.chaptersHtml[i] + this.zip.file(filename, html) + } - this.zip.file('OEBPS/Styles/style.css', styleCss) + this.zip.file('OEBPS/Styles/style.css', styleCss) - this.remoteResources.forEach((r) => { - this.zip.file('OEBPS/' + r.dest, r.data) + this.remoteResources.forEach((r) => { + if (r.dest) { + this.zip.file('OEBPS/' + r.dest, r.data) + } + }) }) } @@ -218,7 +368,7 @@ class FimFic2Epub extends Emitter { if (this.cachedFile) { return Promise.resolve(this.cachedFile) } - this.progress(6, 0, 'Compressing...') + this.progress(0, 0.95, 'Compressing...') return this.zip .generateAsync({ @@ -228,7 +378,7 @@ class FimFic2Epub extends Emitter { compressionOptions: {level: 9} }) .then((file) => { - this.progress(6, 0.3, 'Complete!') + this.progress(0, 1, 'Complete!') this.cachedFile = file return file }) @@ -249,11 +399,30 @@ class FimFic2Epub extends Emitter { }) } + setTitle (title) { + this.storyInfo.title = title.trim() + this.filename = FimFic2Epub.getFilename(this.storyInfo) + } + setAuthorName (name) { + this.storyInfo.author.name = name.trim() + this.filename = FimFic2Epub.getFilename(this.storyInfo) + } + setCoverImage (buffer) { + let info = fileType(isNode ? buffer : new Uint8Array(buffer)) + if (!info || info.mime.indexOf('image/') !== 0) { + throw new Error('Invalid image') + } + this.coverImage = buffer + this.coverFilename = 'Images/cover.' + info.ext + this.coverType = info.mime + this.coverImageDimensions = sizeOf(new Buffer(buffer)) + } + // Internal/private methods progress (part, percent, status) { - let parts = 6.3 - let partsize = 1 / parts - percent = (part / parts) + percent * partsize + // let parts = 6.3 + // let partsize = 1 / parts + // percent = (part / parts) + percent * partsize this.trigger('progress', percent, status) } @@ -283,19 +452,21 @@ class FimFic2Epub extends Emitter { } fetchCoverImage () { + if (this.pcache.coverImage) { + return this.pcache.coverImage + } if (this.coverImage) { - return this.coverImage + return Promise.resolve(this.coverImage) } this.coverImage = null let url = this.coverUrl || this.storyInfo.full_image if (!url) { - return null + return Promise.resolve(null) } - this.progress(0, 0.6, 'Fetching cover image...') + this.progress(0, 0, 'Fetching cover image...') - return fetchRemote(url, 'arraybuffer').then((data) => { - this.progress(0, 1) + this.pcache.coverImage = fetchRemote(url, 'arraybuffer').then((data) => { let info = fileType(isNode ? data : new Uint8Array(data)) if (info) { let type = info.mime @@ -314,7 +485,10 @@ class FimFic2Epub extends Emitter { } else { return null } + }).then(() => { + this.pcache.coverImage = null }) + return this.pcache.coverImage } fetchTitlePage () { @@ -397,7 +571,7 @@ class FimFic2Epub extends Emitter { let authorNotesPos = html.indexOf('
Author\'s Note:') authorNotes = html.substring(authorNotesPos + 22) authorNotes = authorNotes.substring(0, authorNotes.indexOf('\t\n\t
')) @@ -417,115 +591,45 @@ class FimFic2Epub extends Emitter { return {content: chapter, notes: authorNotes, notesFirst: authorNotesPos < chapterPos} } - fetchChapters () { - this.progress(1, 0, 'Fetching chapters...') - return new Promise((resolve, reject) => { - let chapters = this.storyInfo.chapters - let chapterCount = this.storyInfo.chapters.length - let currentChapter = 0 - let completeCount = 0 - - if (chapterCount === 0) { - resolve() - return - } - - let recursive = () => { - let index = currentChapter++ - let ch = chapters[index] - if (!ch) { - return - } - // console.log('Fetching chapter ' + (index + 1) + ' of ' + chapters.length + ': ' + ch.title) - let url = ch.link.replace('http://www.fimfiction.net', '') - fetch(url).then((html) => { - html = this.parseChapterPage(html) - template.createChapter(ch, html, this).then((html) => { - this.findRemoteResources('ch_' + zeroFill(3, index + 1), index, html) - this.chapters[index] = html - completeCount++ - this.progress(1, (completeCount / chapterCount) * 4) - if (completeCount < chapterCount) { - recursive() - } else { - resolve() - } - }) - }) - } - - // concurrent downloads! - recursive() - recursive() - recursive() - recursive() - }) - } - - fetchRemoteFiles () { - this.progress(5, 0, 'Fetching remote files...') - return new Promise((resolve, reject) => { - let iter = this.remoteResources.entries() - let count = 0 - let completeCount = 0 - - let recursive = () => { - let r = iter.next().value - if (!r) { - if (completeCount === this.remoteResources.size) { - resolve() - } - return - } - let url = r[0] - r = r[1] - - // console.log('Fetching remote file ' + (count + 1) + ' of ' + this.remoteResources.size + ': ' + r.filename, url) - count++ - - fetchRemote(url, 'arraybuffer').then((data) => { - r.dest = null - let info = fileType(isNode ? data : new Uint8Array(data)) - if (info) { - let type = info.mime - r.type = type - let isImage = type.indexOf('image/') === 0 - let folder = isImage ? 'Images' : 'Misc' - let dest = folder + '/*.' + info.ext - r.dest = dest.replace('*', r.filename) - r.data = data - } - completeCount++ - this.progress(5, completeCount / this.remoteResources.size) - recursive() - }) - } - - // concurrent downloads! - recursive() - recursive() - recursive() - 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 + if (!this.options.includeExternal) { + this.remoteResources.forEach((r, url) => { + if (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') { + if (ourl.test(this.chapters[w])) { + this.storyInfo.chapters[w].remote = true + } + } else if (w === 'description') { + if (ourl.test(this.storyInfo.description)) { + this.hasRemoteResources.titlePage = true + } + } else if (w === 'tags') { + this.hasRemoteResources.titlePage = true + } } } - } - }) + }) + } else { + 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.chaptersHtml[w] = this.chaptersHtml[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 + } + } + } + }) + } } } diff --git a/src/main.js b/src/main.js index 97f8ecc..5e9387f 100644 --- a/src/main.js +++ b/src/main.js @@ -44,13 +44,15 @@ let checkbox = { } } -let ffcProgress = m.prop(-1) +let ffcProgress = m.prop(0) let ffcStatus = m.prop('') let dialog = { controller (args) { const ctrl = this + ffcProgress(0) + this.isLoading = m.prop(true) this.dragging = m.prop(false) this.xpos = m.prop(0) @@ -62,10 +64,33 @@ let dialog = { this.title = m.prop('') this.author = m.prop('') - this.subjects = m.prop(ffc.subjects) + this.subjects = m.prop([]) this.addCommentsLink = m.prop(ffc.options.addCommentsLink) this.includeAuthorNotes = m.prop(ffc.options.includeAuthorNotes) this.addChapterHeadings = m.prop(ffc.options.addChapterHeadings) + this.includeExternal = m.prop(ffc.options.includeExternal) + this.joinSubjects = m.prop(ffc.options.joinSubjects) + + this.onOpen = function (el, isInitialized) { + if (!isInitialized) { + this.el(el) + this.center() + this.isLoading(true) + ffc.fetchMetadata().then(() => { + this.isLoading(false) + ffcProgress(-1) + this.title(ffc.storyInfo.title) + this.author(ffc.storyInfo.author.name) + this.subjects(ffc.subjects.slice(0)) + m.redraw(true) + this.center() + ffc.fetchChapters().then(() => { + ffcProgress(-1) + m.redraw() + }) + }) + } + } this.setCoverFile = (e) => { this.coverFile(e.target.files ? e.target.files[0] : null) @@ -73,7 +98,13 @@ let dialog = { this.setSubjects = function () { // 'this' is the textarea - ctrl.subjects(this.value.split('\n').map((s) => s.trim()).filter((s) => !!s)) + let set = new Set() + ctrl.subjects(this.value.split('\n').map((s) => s.trim()).filter((s) => { + if (!s) return false + if (set.has(s)) return false + set.add(s) + return true + })) this.value = ctrl.subjects().join('\n') autosize.update(this) } @@ -96,31 +127,21 @@ let dialog = { window.addEventListener('mousemove', onmove, false) window.addEventListener('mouseup', onup, false) } - this.onOpen = function (el, isInitialized) { - if (!isInitialized) { - this.el(el) - this.center() - this.isLoading(true) - ffc.fetchMetadata().then(() => { - this.isLoading(false) - this.title(ffc.storyInfo.title) - this.author(ffc.storyInfo.author.name) - m.redraw(true) - this.center() - }) - } - } + this.move = (xpos, ypos) => { - this.xpos(Math.max(0, xpos)) - this.ypos(Math.max(0, ypos)) + let bc = document.querySelector('.body_container') + let rect = this.el().firstChild.getBoundingClientRect() + this.xpos(Math.max(0, Math.min(xpos, bc.offsetWidth - rect.width))) + this.ypos(Math.max(0, Math.min(ypos, bc.offsetHeight - rect.height))) this.el().style.left = this.xpos() + 'px' this.el().style.top = this.ypos() + 'px' } this.center = () => { + if (this.dragging()) return let rect = this.el().firstChild.getBoundingClientRect() this.move( - (window.innerWidth / 2) - (rect.width / 2) + document.body.scrollLeft, - (window.innerHeight / 2) - (rect.height / 2) + document.body.scrollTop + Math.max(document.body.scrollLeft, (window.innerWidth / 2) - (rect.width / 2) + document.body.scrollLeft), + Math.max(document.body.scrollTop, (window.innerHeight / 2) - (rect.height / 2) + document.body.scrollTop) ) } @@ -141,10 +162,13 @@ let dialog = { ffc.options.addCommentsLink = this.addCommentsLink() ffc.options.includeAuthorNotes = this.includeAuthorNotes() ffc.options.addChapterHeadings = this.addChapterHeadings() + ffc.options.includeExternal = this.includeExternal() + ffc.subjects = this.subjects() + ffc.options.joinSubjects = this.joinSubjects() m.redraw() chain - .then(ffc.fetch.bind(ffc)) + .then(ffc.fetchAll.bind(ffc)) .then(ffc.build.bind(ffc)) .then(ffc.getFile.bind(ffc)).then((file) => { console.log('Saving file...') @@ -163,8 +187,8 @@ 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})), - 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('.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('table.properties', [ m('tr', m('td.section_header', {colspan: 3}, m('b', 'General settings'))), m('tr', m('td.label', 'Title'), m('td', {colspan: 2}, m('input', {type: 'text', value: ctrl.title(), onchange: m.withAttr('value', ctrl.title)}))), m('tr', m('td.label', 'Author'), m('td', {colspan: 2}, m('input', {type: 'text', value: ctrl.author(), onchange: m.withAttr('value', ctrl.author)}))), @@ -176,25 +200,27 @@ let dialog = { ), m('tr', m('td.label', ''), m('td', {colspan: 2}, m(checkbox, {checked: ctrl.addChapterHeadings(), onchange: m.withAttr('checked', ctrl.addChapterHeadings)}, 'Add chapter headings'), - m(checkbox, {checked: ctrl.addCommentsLink(), onchange: m.withAttr('checked', ctrl.addCommentsLink)}, 'Add links to online comments'), - m(checkbox, {checked: ctrl.includeAuthorNotes(), onchange: m.withAttr('checked', ctrl.includeAuthorNotes)}, 'Include author\'s notes') + m(checkbox, {checked: ctrl.addCommentsLink(), onchange: m.withAttr('checked', ctrl.addCommentsLink)}, 'Add link to online comments (at the end of chapters)'), + m(checkbox, {checked: ctrl.includeAuthorNotes(), onchange: m.withAttr('checked', ctrl.includeAuthorNotes)}, 'Include author\'s notes'), + m(checkbox, {checked: ctrl.includeExternal(), onchange: m.withAttr('checked', ctrl.includeExternal)}, 'Download & include remote content (embed images)'), + m('div', {style: 'font-size: 0.9em; line-height: 1em; margin-top: 4px; margin-bottom: 6px; color: #777;'}, 'Note: Disabling this creates invalid EPUBs and requires internet access to see remote content. Only cover image will be embedded.') )), m('tr', m('td.section_header', {colspan: 3}, m('b', 'Metadata customization'))), - m('tr', m('td.label', 'Categories'), m('td', {colspan: 2}, + m('tr', m('td.label', {style: 'vertical-align: top;'}, 'Categories'), m('td', {colspan: 2}, m('textarea', {rows: 2, config: autosize, onchange: ctrl.setSubjects}, ctrl.subjects().join('\n')), - m(checkbox, {checked: false}, 'Join categories into one (iBooks only)') + m(checkbox, {checked: ctrl.joinSubjects(), onchange: m.withAttr('checked', ctrl.joinSubjects)}, 'Join categories into one, separated by commas') )) ]), m('.drop-down-pop-up-footer', [ - m('button.styled_button', {onclick: ctrl.createEpub, disabled: ffcProgress() >= 0 && ffcProgress() < 1}, 'Create EPUB'), - m('.rating_container', + m('button.styled_button', {onclick: ctrl.createEpub, disabled: ffcProgress() >= 0 && ffcProgress() < 1, style: 'float: right'}, 'Download EPUB'), + ffcProgress() >= 0 ? m('.rating_container', m('.bars_container', m('.bar_container', m('.bar_dislike', m('.bar.bar_like', {style: {width: Math.max(0, ffcProgress()) * 100 + '%'}})))), ' ', - ffcProgress() >= 0 && ffcProgress() < 1 ? m('i.fa.fa-spin.fa-spinner') : null, - ' ', + ffcProgress() >= 0 && ffcProgress() < 1 ? [ m('i.fa.fa-spin.fa-spinner'), m.trust('  ') ] : null, ffcStatus() - ) + ) : null, + m('div', {style: 'clear: both'}) ]) ]) ])) @@ -219,7 +245,6 @@ function clickButton () { if (!ffc) { ffc = new FimFic2Epub(STORY_ID) ffc.on('progress', (percent, status) => { - console.log(Math.round(percent * 100), status) ffcProgress(percent) if (status) { ffcStatus(status) diff --git a/src/templates.js b/src/templates.js index 690cef8..ed924ec 100644 --- a/src/templates.js +++ b/src/templates.js @@ -4,8 +4,6 @@ import render from './lib/mithril-node-render' import { pd as pretty } from 'pretty-data' import zeroFill from 'zero-fill' -import htmlWordCount from './html-wordcount' -import { cleanMarkup } from './cleanMarkup' import { NS } from './constants' function nth (d) { @@ -25,24 +23,19 @@ function prettyDate (d) { } export function createChapter (ch, chapter, ffc) { - return Promise.all([ - cleanMarkup(chapter.content), - cleanMarkup(chapter.notes) - ]).then((values) => { - let [chapterContent, authorNotes] = values + return new Promise((resolve, reject) => { + let {content, notes} = chapter - ch.realWordCount = htmlWordCount(chapterContent) - - let content = [ - m.trust(chapterContent), - authorNotes ? m('div#author_notes', {className: chapter.notesFirst ? 'top' : 'bottom'}, [ + let sections = [ + m.trust(content), + ffc.options.includeAuthorNotes && notes ? m('div#author_notes', {className: chapter.notesFirst ? 'top' : 'bottom'}, [ m('p', m('b', 'Author\'s Note:')), - m.trust(authorNotes)]) : null + m.trust(notes)]) : null ] // if author notes are a the beginning of the chapter - if (authorNotes && chapter.notesFirst) { - content.reverse() + if (notes && chapter.notesFirst) { + sections.reverse() } let chapterPage = '\n\n' + pretty.xml(render( @@ -57,7 +50,7 @@ export function createChapter (ch, chapter, ffc) { m('h1', ch.title), m('hr') ]) : null, - content, + sections, ffc.options.addCommentsLink ? m('p.double', {style: 'text-align: center; clear: both;'}, m('a.chaptercomments', {href: ch.link + '#comment_list'}, 'Read chapter comments online') ) : null @@ -65,7 +58,7 @@ export function createChapter (ch, chapter, ffc) { ]) )) - return Promise.resolve(chapterPage) + resolve(chapterPage) }) } @@ -90,14 +83,42 @@ function sortSpineItems (items) { export function createOpf (ffc) { let remotes = [] + let remoteCounter = 0 ffc.remoteResources.forEach((r, url) => { + remoteCounter++ + if (!ffc.options.includeExternal) { + // hack-ish, but what can I do? + // turns out only video and audio can be remote resources.. :I + /* + if (url.indexOf('//') === 0) { + url = 'http:' + url + } + if (url.indexOf('/') === 0) { + url = 'http://www.fimfiction.net' + url + } + let mime = null + if (url.toLowerCase().lastIndexOf('.png')) { + mime = 'image/png' + } else if (url.toLowerCase().lastIndexOf('.jpg')) { + mime = 'image/jpeg' + } + if (mime) { + remotes.push(m('item', {id: 'remote_' + zeroFill(3, remoteCounter), href: url, 'media-type': mime})) + } + */ + return + } if (!r.dest) { return } - let attrs = {id: r.filename, href: r.dest, 'media-type': r.type} - remotes.push(m('item', attrs)) + remotes.push(m('item', {id: r.filename, href: r.dest, 'media-type': r.type})) }) + let subjects = ffc.subjects + if (ffc.options.joinSubjects) { + subjects = [subjects.join(', ')] + } + let contentOpf = '\n' + pretty.xml(render( m('package', {xmlns: NS.OPF, version: '3.0', 'unique-identifier': 'BookId'}, [ m('metadata', {'xmlns:dc': NS.DC, 'xmlns:opf': NS.OPF}, [ @@ -111,10 +132,9 @@ export function createOpf (ffc) { m('dc:source', ffc.storyInfo.url), m('dc:language', 'en'), ffc.coverImage ? m('meta', {name: 'cover', content: 'cover'}) : null, - m('meta', {property: 'dcterms:modified'}, new Date(ffc.storyInfo.date_modified * 1000).toISOString().replace('.000', '')), - m('dc:subject', 'Fimfiction') - ].concat(ffc.categories.map((tag) => - m('dc:subject', tag.name) + m('meta', {property: 'dcterms:modified'}, new Date(ffc.storyInfo.date_modified * 1000).toISOString().replace('.000', '')) + ].concat(subjects.map((s) => + m('dc:subject', s) ), m('meta', {name: 'fimfic2epub version', content: FIMFIC2EPUB_VERSION}))), m('manifest', [ @@ -127,10 +147,10 @@ export function createOpf (ffc) { m('item', {id: 'titlestyle', href: 'Styles/titlestyle.css', 'media-type': 'text/css'}), m('item', {id: 'coverpage', href: 'Text/cover.xhtml', 'media-type': 'application/xhtml+xml', properties: ffc.coverImage ? 'svg' : undefined}), - m('item', {id: 'titlepage', href: 'Text/title.xhtml', 'media-type': 'application/xhtml+xml'}) + m('item', {id: 'titlepage', href: 'Text/title.xhtml', 'media-type': 'application/xhtml+xml', properties: ffc.hasRemoteResources.titlePage ? 'remote-resources' : null}) ].concat(ffc.storyInfo.chapters.map((ch, num) => - m('item', {id: 'chapter_' + zeroFill(3, num + 1), href: 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml', 'media-type': 'application/xhtml+xml'}) + m('item', {id: 'chapter_' + zeroFill(3, num + 1), href: 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml', 'media-type': 'application/xhtml+xml', properties: ch.remote ? 'remote-resources' : null}) ), remotes)), m('spine', {toc: 'ncx'}, sortSpineItems([