edit description, put author notes in the end of the ebook

This commit is contained in:
daniel-j 2016-08-29 16:20:20 +02:00
parent 74ccaf1123
commit 7059fa2be5
7 changed files with 160 additions and 49 deletions

View file

@ -40,6 +40,10 @@ ffc.fetchAll()
})
})
.catch((err) => {
console.log('Error: ' + (err || 'Unknown error'))
if (err && err.stack) {
console.error(err.stack)
} else {
console.error('Error: ' + (err || 'Unknown error'))
}
process.exit(1)
})

View file

@ -1,4 +1,22 @@
/* checkbox fixes */
.toggleable-switch {
white-space: nowrap;
}
.toggleable-switch > a, .toggleable-switch > span {
transition: background 0.2s, opacity 0.2s !important;
}
.toggleable-switch input:disabled+a, .toggleable-switch input:disabled~span {
opacity: 0.6;
}
.toggleable-switch input:disabled+a, .toggleable-switch input:checked:disabled+a {
background: #BBB;
}
.toggleable-switch input:disabled+a:before, .toggleable-switch input:checked:disabled+a:before {
color: #666;
}
/* the logo button */
.fimfic2epub-logo {
float: right;
margin-top: 5px;
@ -24,6 +42,7 @@
width: 24px;
}
/* dialog popup */
#epubDialogContainer .drop-down-pop-up-container {
position: absolute;
top: 0;

View file

@ -4,7 +4,7 @@
"name": "fimfic2epub",
"short_name": "fimfic2epub",
"description": "Improved EPUB exporter for Fimfiction",
"version": "1.4.4",
"version": "1.4.5",
"icons": {
"128": "icon-128.png"

View file

@ -1,6 +1,6 @@
{
"name": "fimfic2epub",
"version": "1.4.4",
"version": "1.4.5",
"description": "Tool to generate improved EPUB ebooks from Fimfiction stories",
"author": "djazz",
"repository": {

View file

@ -88,6 +88,7 @@ class FimFic2Epub extends Emitter {
this.options = {
addCommentsLink: true,
includeAuthorNotes: true,
useAuthorNotesIndex: false,
addChapterHeadings: true,
includeExternal: true,
@ -108,6 +109,9 @@ class FimFic2Epub extends Emitter {
this.subjects = []
this.chapters = []
this.chaptersHtml = []
this.notesHtml = []
this.hasAuthorNotes = false
this.chaptersWithNotes = []
this.remoteResourcesCached = false
this.remoteResources = new Map()
this.coverUrl = ''
@ -189,6 +193,8 @@ class FimFic2Epub extends Emitter {
}
this.chapters.length = 0
this.chaptersHtml.length = 0
this.hasAuthorNotes = false
this.chaptersWithNotes.length = 0
this.progress(0, 0, 'Fetching chapters...')
this.pcache.chapters = new Promise((resolve, reject) => {
@ -218,6 +224,10 @@ class FimFic2Epub extends Emitter {
]).then((values) => {
chapter.content = values[0]
chapter.notes = values[1]
if (chapter.notes) {
this.hasAuthorNotes = true
this.chaptersWithNotes.push(index)
}
ch.realWordCount = htmlWordCount(chapter.content)
this.chapters[index] = chapter
@ -226,6 +236,7 @@ class FimFic2Epub extends Emitter {
if (completeCount < chapterCount) {
recursive()
} else {
this.chaptersWithNotes.sort((a, b) => a - b)
resolve()
}
})
@ -303,14 +314,31 @@ class FimFic2Epub extends Emitter {
buildChapters () {
let chain = Promise.resolve()
this.chaptersHtml.length = 0
this.notesHtml.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)
chain = chain.then(template.createChapter.bind(null, {
title: this.options.addChapterHeadings ? ch.title : null,
link: this.options.addCommentsLink ? ch.link : null,
linkNotes: this.options.includeAuthorNotes && this.options.useAuthorNotesIndex && chapter.notes ? 'note_' + zeroFill(3, i + 1) + '.xhtml' : null,
content: chapter.content,
notes: !this.options.useAuthorNotesIndex ? chapter.notes : '',
notesFirst: chapter.notesFirst
})).then((html) => {
this.findRemoteResources('ch_' + zeroFill(3, i + 1), {chapter: i}, html)
this.chaptersHtml[i] = html
})
if (this.options.includeAuthorNotes && this.options.useAuthorNotesIndex && chapter.notes) {
chain = chain.then(template.createChapter.bind(null, {
title: 'Author\'s Note: ' + ch.title,
content: chapter.notes
})).then((html) => {
this.findRemoteResources('note_' + zeroFill(3, i + 1), {note: i}, html)
this.notesHtml[i] = html
})
}
}
return chain
@ -338,7 +366,7 @@ class FimFic2Epub extends Emitter {
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/Text/nav.xhtml', template.createNav(this, 0))
this.zip.file('OEBPS/toc.ncx', template.createNcx(this))
for (let i = 0; i < this.chapters.length; i++) {
@ -347,6 +375,17 @@ class FimFic2Epub extends Emitter {
this.zip.file(filename, html)
}
if (this.options.includeAuthorNotes && this.options.useAuthorNotesIndex && this.hasAuthorNotes) {
this.zip.file('OEBPS/Text/notesnav.xhtml', template.createNav(this, 1))
for (let i = 0; i < this.chapters.length; i++) {
if (!this.chapters[i].notes) continue
let filename = 'OEBPS/Text/note_' + zeroFill(3, i + 1) + '.xhtml'
let html = this.notesHtml[i]
this.zip.file(filename, html)
}
}
this.zip.file('OEBPS/Styles/style.css', styleCss)
this.remoteResources.forEach((r) => {
@ -617,8 +656,10 @@ class FimFic2Epub extends Emitter {
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)
if (typeof w === 'object' && w.chapter !== undefined && this.chaptersHtml[w.chapter]) {
this.chaptersHtml[w.chapter] = this.chaptersHtml[w.chapter].replace(ourl, dest)
} else if (typeof w === 'object' && w.note !== undefined && this.notesHtml[w.note]) {
this.notesHtml[w.note] = this.notesHtml[w.note].replace(ourl, dest)
} else if (w === 'description') {
this.storyInfo.description = this.storyInfo.description.replace(ourl, dest)
} else if (w === 'tags') {

View file

@ -79,10 +79,12 @@ document.body.appendChild(dialogContainer)
let checkbox = {
view: function (ctrl, args, text) {
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
return m('label.toggleable-switch', [
m('input', Object.assign({
type: 'checkbox'
}, args)),
m('a'),
text ? m('span', text) : null
])
}
}
@ -107,9 +109,11 @@ let dialog = {
this.title = m.prop('')
this.author = m.prop('')
this.description = m.prop('')
this.subjects = m.prop([])
this.addCommentsLink = m.prop(ffc.options.addCommentsLink)
this.includeAuthorNotes = m.prop(ffc.options.includeAuthorNotes)
this.useAuthorNotesIndex = m.prop(ffc.options.useAuthorNotesIndex)
this.addChapterHeadings = m.prop(ffc.options.addChapterHeadings)
this.includeExternal = m.prop(ffc.options.includeExternal)
this.joinSubjects = m.prop(ffc.options.joinSubjects)
@ -124,6 +128,7 @@ let dialog = {
ffcProgress(-1)
this.title(ffc.storyInfo.title)
this.author(ffc.storyInfo.author.name)
this.description(ffc.storyInfo.short_description)
this.subjects(ffc.subjects.slice(0))
m.redraw(true)
this.center()
@ -152,6 +157,12 @@ let dialog = {
autosize.update(this)
}
this.setDescription = function () {
ctrl.description(this.value.trim())
this.value = ctrl.description()
autosize.update(this)
}
this.ondown = (e) => {
let rect = this.el().firstChild.getBoundingClientRect()
let offset = {x: e.pageX - rect.left - document.body.scrollLeft, y: e.pageY - rect.top - document.body.scrollTop}
@ -202,8 +213,10 @@ let dialog = {
}
ffc.setTitle(this.title())
ffc.setAuthorName(this.author())
ffc.storyInfo.short_description = this.description()
ffc.options.addCommentsLink = this.addCommentsLink()
ffc.options.includeAuthorNotes = this.includeAuthorNotes()
ffc.options.useAuthorNotesIndex = this.useAuthorNotesIndex()
ffc.options.addChapterHeadings = this.addChapterHeadings()
ffc.options.includeExternal = this.includeExternal()
ffc.subjects = this.subjects()
@ -214,7 +227,6 @@ let dialog = {
.then(ffc.fetchAll.bind(ffc))
.then(ffc.build.bind(ffc))
.then(ffc.getFile.bind(ffc)).then((file) => {
console.log('Saving file...')
if (typeof safari !== 'undefined') {
blobToDataURL(file).then((dataurl) => {
document.location.href = dataurl
@ -245,11 +257,13 @@ let dialog = {
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 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.useAuthorNotesIndex(), onchange: m.withAttr('checked', ctrl.useAuthorNotesIndex), disabled: !ctrl.includeAuthorNotes()}, 'Put all notes at the end of the ebook'),
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', {style: 'vertical-align: top;'}, 'Description'), m('td', {colspan: 2}, m('textarea', {config: autosize, onchange: ctrl.setDescription}, ctrl.description()))),
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: ctrl.joinSubjects(), onchange: m.withAttr('checked', ctrl.joinSubjects)}, 'Join categories and separate with commas (for iBooks only)')

View file

@ -22,19 +22,19 @@ function prettyDate (d) {
return d.getDate() + nth(d) + ' ' + months[d.getMonth()].substring(0, 3) + ' ' + d.getFullYear()
}
export function createChapter (ch, chapter, ffc) {
export function createChapter (ch) {
return new Promise((resolve, reject) => {
let {content, notes} = chapter
let {content, notes, notesFirst, title, link, linkNotes} = ch
let sections = [
m.trust(content),
ffc.options.includeAuthorNotes && notes ? m('div#author_notes', {className: chapter.notesFirst ? 'top' : 'bottom'}, [
m.trust(content || ''),
notes ? m('div#author_notes', {className: notesFirst ? 'top' : 'bottom'}, [
m('p', m('b', 'Author\'s Note:')),
m.trust(notes)]) : null
]
// if author notes are a the beginning of the chapter
if (notes && chapter.notesFirst) {
if (notes && notesFirst) {
sections.reverse()
}
@ -43,17 +43,18 @@ export function createChapter (ch, chapter, ffc) {
m('head', [
m('meta', {charset: 'utf-8'}),
m('link', {rel: 'stylesheet', type: 'text/css', href: '../Styles/style.css'}),
m('title', ch.title)
m('title', title)
]),
m('body', [
ffc.options.addChapterHeadings ? m('.chapter-title', [
m('h1', ch.title),
title ? m('.chapter-title', [
m('h1', title),
m('hr')
]) : null,
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
m('p.double', {style: 'text-align: center; clear: both;'},
link ? m('a.chaptercomments', {href: link + '#comment_list'}, 'Read chapter comments online') : null,
linkNotes ? m('a.chaptercomments', {href: linkNotes}, 'Read author\'s note') : null
)
])
])
))
@ -114,6 +115,23 @@ export function createOpf (ffc) {
remotes.push(m('item', {id: r.filename, href: r.dest, 'media-type': r.type}))
})
let manifestChapters = 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', properties: ch.remote ? 'remote-resources' : null})
)
let spineChapters = ffc.storyInfo.chapters.map((ch, num) =>
m('itemref', {idref: 'chapter_' + zeroFill(3, num + 1)})
)
let manifestNotes = []
let spineNotes = []
if (ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes) {
spineNotes.push(m('itemref', {idref: 'notesnav'}))
ffc.chaptersWithNotes.forEach((num) => {
let id = 'note_' + zeroFill(3, num + 1)
manifestNotes.push(m('item', {id: id, href: 'Text/' + id + '.xhtml', 'media-type': 'application/xhtml+xml'}))
spineNotes.push(m('itemref', {idref: id}))
})
}
let subjects = ffc.subjects
if (ffc.options.joinSubjects) {
subjects = [subjects.join(', ')]
@ -128,7 +146,7 @@ export function createOpf (ffc) {
m('meta', {refines: '#cre', property: 'role', scheme: 'marc:relators'}, 'aut'),
m('dc:date', new Date((ffc.storyInfo.publishDate || ffc.storyInfo.date_modified) * 1000).toISOString().substring(0, 10)),
m('dc:publisher', 'Fimfiction'),
m('dc:description', ffc.storyInfo.short_description || ffc.storyInfo.description),
m('dc:description', ffc.storyInfo.short_description),
m('dc:source', ffc.storyInfo.url),
m('dc:language', 'en'),
ffc.coverImage ? m('meta', {name: 'cover', content: 'cover'}) : null,
@ -141,6 +159,7 @@ export function createOpf (ffc) {
ffc.coverImage ? m('item', {id: 'cover', href: ffc.coverFilename, 'media-type': ffc.coverType, properties: 'cover-image'}) : null,
m('item', {id: 'ncx', href: 'toc.ncx', 'media-type': 'application/x-dtbncx+xml'}),
m('item', {id: 'nav', 'href': 'Text/nav.xhtml', 'media-type': 'application/xhtml+xml', properties: 'nav'}),
ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex ? m('item', {id: 'notesnav', 'href': 'Text/notesnav.xhtml', 'media-type': 'application/xhtml+xml'}) : null,
m('item', {id: 'style', href: 'Styles/style.css', 'media-type': 'text/css'}),
m('item', {id: 'coverstyle', href: 'Styles/coverstyle.css', 'media-type': 'text/css'}),
@ -149,18 +168,16 @@ export function createOpf (ffc) {
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', 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', properties: ch.remote ? 'remote-resources' : null})
), remotes)),
].concat(manifestChapters, manifestNotes, remotes)),
m('spine', {toc: 'ncx'}, sortSpineItems([
m('itemref', {idref: 'coverpage'}),
m('itemref', {idref: 'titlepage'}),
m('itemref', {idref: 'nav', linear: ffc.storyInfo.chapters.length <= 1 ? 'no' : undefined})
].concat(ffc.storyInfo.chapters.map((ch, num) =>
m('itemref', {idref: 'chapter_' + zeroFill(3, num + 1)})
)))),
m('itemref', {idref: 'nav', linear: ffc.storyInfo.chapters.length <= 1 && !(ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes) ? 'no' : undefined})
].concat(
spineChapters,
spineNotes
))),
m('guide', [
m('reference', {type: 'cover', title: 'Cover', href: 'Text/cover.xhtml'}),
m('reference', {type: 'toc', title: 'Contents', href: 'Text/nav.xhtml'})
@ -174,7 +191,7 @@ export function createOpf (ffc) {
function navPoints (list) {
let arr = []
for (let i = 0; i < list.length; i++) {
list[i]
if (!list[i]) continue
arr.push(m('navPoint', {id: 'navPoint-' + (i + 1), playOrder: i + 1}, [
m('navLabel', m('text', list[i][0])),
m('content', {src: list[i][1]})
@ -182,7 +199,6 @@ function navPoints (list) {
}
return arr
}
export function createNcx (ffc) {
let tocNcx = '<?xml version="1.0" encoding="utf-8" ?>\n' + pretty.xml(render(
m('ncx', {version: '2005-1', xmlns: NS.DAISY}, [
@ -197,33 +213,50 @@ export function createNcx (ffc) {
['Cover', 'Text/cover.xhtml']
].concat(ffc.storyInfo.chapters.map((ch, num) =>
[ch.title, 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml']
))))
), ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes ? [['Author\'s Notes', 'Text/notesnav.xhtml']] : null)))
])
))
// console.log(tocNcx)
return tocNcx
}
export function createNav (ffc) {
export function createNav (ffc, mode = 0) {
let title, list
switch (mode) {
case 0:
title = 'Contents'
list = [m('li', {hidden: ''}, m('a', {href: 'cover.xhtml'}, 'Cover'))]
.concat(ffc.storyInfo.chapters.map((ch, num) =>
m('li', [
m('a.leftalign', {href: 'chapter_' + zeroFill(3, num + 1) + '.xhtml'}, ch.title)
// m('span.date', [m('b', ' · '), prettyDate(new Date(ch.date_modified * 1000)), m('span', {style: 'display: none'}, ' · ')]),
// m('.floatbox', m('span.wordcount', ch.realWordCount.toLocaleString('en-GB')))
])
))
if (ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes) {
list.push(m('li', m('a.leftalign', {href: 'notesnav.xhtml'}, 'Author\'s Notes')))
}
break
case 1:
title = 'Author\'s Notes'
list = ffc.chaptersWithNotes.map((num) => {
let ch = ffc.storyInfo.chapters[num]
return m('li', m('a.leftalign', {href: 'note_' + zeroFill(3, num + 1) + '.xhtml'}, ch.title))
})
break
}
let navDocument = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(render(
m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS}, [
m('head', [
m('meta', {charset: 'utf-8'}),
m('link', {rel: 'stylesheet', type: 'text/css', href: '../Styles/style.css'}),
m('title', 'Contents')
m('title', title)
]),
m('body#navpage', [
m('nav#toc', {'epub:type': 'toc'}, [
m('h3', 'Contents'),
m('ol', [
m('li', {hidden: ''}, m('a', {href: 'cover.xhtml'}, 'Cover'))
].concat(ffc.storyInfo.chapters.map((ch, num) =>
m('li', [
m('a.leftalign', {href: 'chapter_' + zeroFill(3, num + 1) + '.xhtml'}, ch.title)
// m('span.date', [m('b', ' · '), prettyDate(new Date(ch.date_modified * 1000)), m('span', {style: 'display: none'}, ' · ')]),
// m('.floatbox', m('span.wordcount', ch.realWordCount.toLocaleString('en-GB')))
])
)))
m('nav#toc', mode === 0 ? {'epub:type': 'toc'} : null, [
m('h3', title),
m('ol', list)
])
])
])