import { formatDistance } from 'date-fns'
import Utils from './utils'
import ReaderView from './reader-view'
import Chapter from './resource/Chapter'
import Manga from './resource/Manga'
export default class AbstractRenderer {
constructor(container, model, view) {
this.el = container
this.model = model
this.view = view
this._initialized = false
}
get chapter() { return this.model.chapter }
initialize() {
// console.log('initialize',this.name)
this._initialized = true
this.clearImageContainer()
this.renderedPages = 0
this._pageStateHandler = (page) => {
//console.info('pagestatehandler', this.name, page)
this.pageStateHandler(page)
}
this.model.on('pageloading', this._pageStateHandler)
this.model.on('pageload', this._pageStateHandler)
this.model.on('pageerror', this._pageStateHandler)
}
destroy() {
if (!this._initialized) {
return
}
// console.log('destroy',this.name)
this._initialized = false
this.clearImageContainer()
this.model.off('pageloading', this._pageStateHandler)
this.model.off('pageload', this._pageStateHandler)
this.model.off('pageerror', this._pageStateHandler)
}
reinitialize() {
if (this._initialized) {
this.destroy()
}
this.initialize()
}
createAndAppendWrapper(page) {
return this.el.appendChild(this.updateWrapper(this.createWrapper(), page))
}
createWrapper() {
const wrapper = document.createElement('div')
const classes = [
'reader-image-wrapper',
'col-auto',
'my-auto',
'justify-content-center',
'align-items-center',
'noselect', 'nodrag',
'row', 'no-gutters',
]
wrapper.classList.add(...classes)
wrapper.dataset.state = 0
return wrapper
}
updateWrapper(wrapper, page = {}) {
//console.log('update wrapper to', page.number, page)
if (page.state !== parseInt(wrapper.dataset.state)) {
while (wrapper.firstChild) {
wrapper.removeChild(wrapper.firstChild)
}
switch (page.state) {
case 1: wrapper.appendChild(this.createPageLoading()); break;
case 2: wrapper.appendChild(this.createPageLoaded()); break;
case 3: wrapper.appendChild(this.createPageError()); break;
}
}
wrapper.style.order = page.number || 0
wrapper.dataset.page = page.number || 0
wrapper.dataset.state = page.state || 0
switch (page.state) {
case 1: wrapper.querySelector('.loading-page-number').textContent = page.number; break;
case 2: wrapper.firstChild.src = page.image.src; break;
case 3: wrapper.querySelector('.alert .message').textContent = page.error.message; break;
}
return wrapper
}
createPageLoading() {
const container = document.createElement('div')
container.classList.add('m-5', 'd-flex', 'align-items-center', 'justify-content-center')
container.style.color = '#fff'
container.style.textShadow = '0 0 7px rgba(0,0,0,0.5)'
const spinner = container.appendChild(document.createElement('span'))
spinner.classList.add('fas', 'fa-circle-notch', 'fa-spin', 'position-absolute')
spinner.style.opacity = '0.5'
spinner.style.fontSize = '7em'
const pgNum = container.appendChild(document.createElement('span'))
pgNum.classList.add('loading-page-number')
pgNum.style.fontSize = '2em'
return container
}
createPageLoaded() {
const container = document.createElement('img')
container.draggable = false
container.classList.add('noselect', 'nodrag', 'cursor-pointer')
return container
}
createPageError() {
const container = Alert.container('', 'danger')
const tapMsg = container.appendChild(document.createElement('div'))
tapMsg.innerHTML = "Tap to reload."
container.addEventListener('click', evt => {
evt.preventDefault()
evt.stopPropagation()
const page = this.model.getPageWithoutLoading(parseInt(container.parentElement.dataset.page))
page.reload(true).catch(console.error)
})
return container
}
createMangaError(chapter) {
const container = Alert.container('', 'danger')
const tapMsg = container.appendChild(document.createElement('div'))
tapMsg.innerHTML = "Tap to reload."
container.addEventListener('click', evt => {
evt.preventDefault()
evt.stopPropagation()
container.parentElement.removeChild(container)
this.model.isLoading = true
chapter.loadManga(true)
.then(chapter => {
this.model.isLoading = false
this.model.setChapter(chapter.id)
.then(() => { this.view.moveToPage(1, false) })
})
.catch(err => {
this.model.isLoading = false
this.model.trigger('readererror', [err])
})
})
return container
}
clearImageContainer() {
while (this.el && this.el.firstChild) {
this.el.removeChild(this.el.firstChild)
}
}
render() {
throw new Error("Not implemented")
}
pageStateHandler() {
throw new Error("Not implemented")
}
}
export class SinglePage extends AbstractRenderer {
get name() { return 'single-page' }
initialize() {
super.initialize()
this.renderedPages = 1
this.pageToRender = null
this.pageWrapper = this.createAndAppendWrapper()
}
pageStateHandler(page) {
if (this.pageToRender === page) {
this.updateWrapper(this.pageWrapper, page)
}
}
render(pg) {
const page = this.model.getPageWithoutLoading(pg)
this.pageToRender = page
this.updateWrapper(this.pageWrapper, page)
return page.load().catch(p => Promise.resolve(p))
}
}
export class DoublePage extends AbstractRenderer {
get name() { return 'double-page' }
get renderedPages() { return this.pagesToRender.length }
set renderedPages(v) { }
isPageTurnForwards() { return this.previousPage < this.model.currentPage }
isSinglePageBackwards() { return this.previousPage === this.model.currentPage + 1 }
isImageTooWide(img) {
return img && img.naturalWidth > img.naturalHeight && img.naturalWidth > this.el.offsetWidth / 2
}
initialize() {
super.initialize()
this.pageWrapperLoading = this.createAndAppendWrapper({ state: 1, number: '', })
this.previousPage = 0
this.pageWrappers = [this.createAndAppendWrapper(), this.createAndAppendWrapper()]
this.pagesToRender = []
this.setLoading(true)
}
pageStateHandler(page) {
if (this.pagesToRender.includes(page)) {
this.checkRender()
}
}
render(pg) {
this.pagesToRender = [pg, pg + 1]
.map(p => this.model.getPageWithoutLoading(p))
.filter(p => p)
this.checkRender()
return Promise.all(
this.pagesToRender.map(page =>
page.load().catch(p => Promise.resolve(p))
)
)
}
checkRender() {
const pagesDone = this.pagesToRender.every(p => p.isDone)
if (pagesDone) {
if (this.pagesToRender.length > 1 && this.pagesToRender.some(p => this.isImageTooWide(p.image))) {
if (this.isPageTurnForwards() || this.isSinglePageBackwards()) {
this.pagesToRender.pop()
} else {
this.pagesToRender.shift()
this.model.setCurrentPage(this.pagesToRender[0].number)
}
}
this.updateWrapper(this.pageWrappers[0], this.pagesToRender[0])
this.updateWrapper(this.pageWrappers[1], this.pagesToRender[1])
this.previousPage = this.model.currentPage
}
this.setLoading(!pagesDone)
}
setLoading(state) {
this.pageWrapperLoading.classList.toggle('d-none', !state)
for (let wrapper of this.pageWrappers) {
wrapper.classList.toggle('d-none', state)
}
}
}
export class LongStrip extends AbstractRenderer {
get name() { return 'long-strip' }
get renderedPages() { return this._renderedPageSet.length }
set renderedPages(v) { }
get lastRenderedPage() { return this._renderedPageSet[this._renderedPageSet.length - 1] }
initialize() {
super.initialize()
this._pageWrapperMap = new Map()
this._renderedPageSet = []
this._scrollY = -1
this.observer = new MutationObserver((mutationsList) => {
// this is horrible
if (this._scrollY === -1 && this.model.currentPage !== 1) {
this._scrollY = -2
requestAnimationFrame(() => {
this.getPageWrapper(this.model.currentPage).scrollIntoView(true)
requestAnimationFrame(() => {
this._scrollY = window.pageYOffset || -1
// console.log('did it',this._scrollY)
if (this._scrollY !== -1) {
ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
this.observer.disconnect()
}
})
})
//this.scrollToPage(this.model.currentPage)
}
})
this.observer.observe(this.el, { childList: true, subtree: true })
for (let page of this.model.getAllPages()) {
this._pageWrapperMap.set(page.number, this.createAndAppendWrapper(page))
if (page.isDone) {
this._renderedPageSet.push(page.number)
}
}
Utils.stableSort(this._renderedPageSet)
this.renderEndBlock()
this.render(this.model.currentPage)
this.addScrollHandler()
this._currentPageHandler = (pg) => {
this.render(pg + 1)
.then(() => {
this.render(pg - 1)
})
}
this.model.on('currentpagechange', this._currentPageHandler)
}
destroy() {
if (!this._initialized) {
return
}
super.destroy()
this.observer.disconnect()
//this.el.scrollIntoView(true)
this._pageWrapperMap.clear()
this.removeScrollHandler()
window.scrollTo(0, 0)
this.model.off('currentpagechange', this._currentPageHandler)
}
pageStateHandler(page) {
this.updateWrapper(this.getPageWrapper(page.number), page)
if (page.isDone || page.loading) {
if (!this.isRendered(page.number)) {
this._renderedPageSet.push(page.number)
Utils.stableSort(this._renderedPageSet)
}
if (this.isChapterFullyRendered) {
this.showEndBlock()
}
if (this._scrollY >= 0) {
this.updateCurrentPage()
} //else {
// this._scrollY = -2
// requestAnimationFrame(() => {
// this.getPageWrapper(this.model.currentPage).scrollIntoView(true)
// requestAnimationFrame(() => {
// ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
// requestAnimationFrame(() => {
// this._scrollY = window.pageYOffset || -1
// console.log('did it',this._scrollY)
// })
// })
// })
//this.scrollToPage(this.model.currentPage)
// }
}
}
getPageWrapper(pg) {
if (!this._pageWrapperMap.has(pg)) {
throw new Error("No wrapper for page ", pg)
}
return this._pageWrapperMap.get(pg)
}
isRendered(pg) {
return this._renderedPageSet.includes(pg)
}
get isChapterFullyRendered() {
return this.renderedPages === this.model.totalPages
}
render(pg) {
if (!this.isChapterFullyRendered && !this.isRendered(pg)) {
return this.model.getPage(pg)
.catch(p => Promise.resolve(p))
}
return Promise.resolve()
}
renderEndBlock() {
this._endBlock = this.createAndAppendWrapper({
number: this.model.totalPages + 1,
chapter: this.model.chapter.id,
})
this._endBlock.textContent = 'End of chapter / Go to next'
this._endBlock.classList.add('reader-image-block', 'py-3', 'd-none')
this._endBlock.addEventListener('click', (evt) => {
evt.stopPropagation()
this.view.moveToChapter(this.model.chapter.nextChapterId)
}, { once: true })
if (this.isChapterFullyRendered) {
this.showEndBlock()
}
}
showEndBlock() {
this._endBlock.classList.remove('d-none')
}
updateCurrentPage() {
if (this.renderedPages > 0 && !this._updating) {
this._updating = true
if (ReaderView.isScrolledToTop) {
this.model.setCurrentPage(this._renderedPageSet[0])
this.view.replaceHistory()
} else if (ReaderView.isScrolledToBottom) {
this.model.setCurrentPage(this.lastRenderedPage)
this.view.replaceHistory()
} else {
const scrollY = Math.floor(window.pageYOffset)
for (let i = this._renderedPageSet.length - 1; i >= 0; --i) {
const pg = this._renderedPageSet[i]
const wrapper = this.getPageWrapper(pg)
if (scrollY >= wrapper.offsetTop) {
if (this.model.setCurrentPage(pg)) {
this.view.replaceHistory()
}
break
}
}
}
this._updating = false
}
}
scrollToPage(pg) {
// requestAnimationFrame(() => {
const wrapper = this.getPageWrapper(pg)
if (this.isRendered(pg) && wrapper) {
// console.log('scrolling to', pg, wrapper.offsetTop + 1)
//window.scrollTo(window.pageXOffset, wrapper.offsetTop + 1)
wrapper.scrollIntoView(true)
if (!ReaderView.isScrolledToBottom) {
ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
}
// requestAnimationFrame(() => {
// })
}
// })
}
addScrollHandler() {
if (!this._scrollHandler) {
const update = () => {
if (this.model.chapter) {
this.updateCurrentPage()
}
}
if (Modernizr.requestanimationframe) {
let wait = false
this._scrollHandler = () => {
if (!wait) {
wait = true
requestAnimationFrame(() => {
update()
wait = false
})
}
}
} else {
this._scrollHandler = () => {
update()
}
}
window.addEventListener('scroll', this._scrollHandler)
}
}
removeScrollHandler() {
if (this._scrollHandler) {
window.removeEventListener('scroll', this._scrollHandler)
this._scrollHandler = null
}
}
}
export class Alert extends AbstractRenderer {
get name() { return 'alert' }
pageStateHandler() { }
renderChapterButtons(data) {
const chBtnContainer = this.el.appendChild(document.createElement('div'))
chBtnContainer.classList.add('row', 'm-auto', 'justify-content-center', 'directional')
const buttons = [
{ text: 'Previous chapter', id: data.prevChapterId, order: 1 },
{ text: 'Next chapter', id: data.nextChapterId, order: 2 },
]
const classes = ['col-auto', 'hover', 'text-dark']
for (let btn of buttons) {
const link = chBtnContainer.appendChild(Alert.container(btn.text, 'dark', 'a'))
link.setAttribute('href', this.view.pageURL(btn.id))
link.dataset.action = 'chapter'
link.dataset.chapter = btn.id
link.classList.add(...classes)
link.classList.replace('m-auto', 'm-1')
link.style.order = btn.order
}
}
render(data) {
this.clearImageContainer()
if (typeof data !== 'object') {
return Promise.reject({ message: "Data is not an object", data: data, revert: true })
}
if (data.isExternal) {
this.el.appendChild(Alert.container(`This chapter can be read for free on the official publisher's website.
Feel free to write your comments about it here on MangaDex!`, 'info'))
const link = Alert.container(`${Alert.icon('external-link-alt', 'Website')} Read the chapter`, 'success', 'a', false)
link.target = '_blank'
link.rel = 'noopener noreferrer'
link.href = data.pages
this.el.appendChild(link)
this.renderChapterButtons(data)
} else if (data.isDelayed) {
const now = new Date()
const release = new Date(data.timestamp * 1000)
const relativeDate = release > now ? formatDistance(release, now, { addSuffix: true }) : 'within a few minutes'
this.el.appendChild(Alert.container(`Due to the group's delay policy, this chapter will be available ${relativeDate}.`, 'danger'))
this.el.appendChild(Alert.container(`You might be able to read it on the group's ${Alert.icon('external-link-alt', 'Website')} website.`, 'info'))
this.renderChapterButtons(data)
} else if (data.isSpoilerNet) {
const alert = document.createElement('div')
alert.classList.add('alert', `alert-warning`, 'text-center', 'm-auto')
alert.attributes.role = 'alert'
alert.innerHTML = `