mirror of
https://github.com/holo-gfx/mangadex.git
synced 2024-11-25 02:48:21 -05:00
653 lines
22 KiB
JavaScript
653 lines
22 KiB
JavaScript
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.<br>Feel free to write your comments about it here on MangaDex!`, 'info'))
|
|
const link = Alert.container(`${Alert.icon('external-link-alt', 'Website')} <strong>Read the chapter</strong>`, '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 <a target='_blank' rel='noopener noreferrer' href='${data.groupWebsite}'>${Alert.icon('external-link-alt', 'Website')} <strong>website</strong></a>.`, '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 = `<h3>${Alert.icon('warning')} Spoiler Warning</h3><div class="my-3"><div>There seems to be a gap between chapters (${Chapter.getResource(data.prevChapterId).fullTitle} → ${Chapter.getResource(data.chapterId).fullTitle}).</div><div>This may be an attempt to troll you into reading a chapter early.</div>`
|
|
const button = document.createElement('button')
|
|
button.classList.add('btn', 'btn-secondary')
|
|
button.type = 'button'
|
|
button.textContent = "I understand, I'm fine with spoilers!"
|
|
button.addEventListener('click', (evt) => {
|
|
evt.stopPropagation()
|
|
this.view.moveToChapter(data.chapterId, 1, true, true)
|
|
})
|
|
alert.appendChild(button)
|
|
this.el.appendChild(alert)
|
|
} else if (data.isNotFound) {
|
|
this.el.appendChild(Alert.container(`Data not found${data.message ? ': ' + data.message : '.'}`, 'danger'))
|
|
} else if (data.isDeleted) {
|
|
this.el.appendChild(Alert.container(`This chapter has been deleted.`, 'danger'))
|
|
} else if (data.isRestricted) {
|
|
this.el.appendChild(Alert.container(`This chapter is unavailable.`, 'danger'))
|
|
} else if (data.isMangaFailed) {
|
|
const alert = this.createMangaError(data)
|
|
alert.querySelector('.message').textContent = "The manga data failed to load."
|
|
alert.draggable = false
|
|
alert.classList.add('noselect', 'nodrag', 'cursor-pointer')
|
|
this.el.appendChild(alert)
|
|
} else if (data.status === 'unavailable') {
|
|
this.el.appendChild(Alert.container(`This chapter is unavailable.`, 'danger'))
|
|
} else if (data != null) {
|
|
const isError = data instanceof Error || data.error instanceof Error
|
|
const type = data.type || isError ? 'danger' : undefined
|
|
const msg = data.message || data.error && data.error.message || data.error && data.error.status || data
|
|
this.el.appendChild(Alert.container(msg, type))
|
|
// if (isError) {
|
|
// this.el.appendChild(Alert.container('dark', data.stack || data.error.stack))
|
|
// }
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
|
|
static icon(type, title) {
|
|
type = Alert.iconTypes[type] || type
|
|
return `<span class='fas fa-${type} fa-fw' aria-hidden='true'${title ? ` title=${title}` : ''}></span> `
|
|
}
|
|
|
|
static get iconTypes() {
|
|
return {
|
|
success: 'check-circle',
|
|
danger: 'times',
|
|
info: 'info',
|
|
warning: 'exclamation-triangle',
|
|
}
|
|
}
|
|
|
|
static container(message = '', type = 'dark', element = 'div', icon = true) {
|
|
const div = document.createElement(element)
|
|
div.classList.add('alert', `alert-${type}`, 'text-center', 'm-auto')
|
|
div.attributes.role = 'alert'
|
|
if (icon && Alert.iconTypes[type]) {
|
|
div.innerHTML = Alert.icon(type)
|
|
}
|
|
const span = div.appendChild(document.createElement('span'))
|
|
span.classList.add('message')
|
|
span.innerHTML = message
|
|
return div
|
|
}
|
|
}
|
|
|
|
export class Recommendations extends AbstractRenderer {
|
|
get name() { return 'recommendations' }
|
|
|
|
render() {
|
|
if (!this.model.recommendations) {
|
|
this.el.innerHTML = ''
|
|
return this.el.appendChild(Alert.container("No recommendations found. You must be logged in and have followed some titles.", "danger"))
|
|
}
|
|
let recStr = ''
|
|
for (let [manga, chapters] of this.model.recommendations.unreadChaptersGroupedByManga) {
|
|
if (chapters.length > 0) {
|
|
const chapter = chapters[chapters.length - 1]
|
|
const more = chapters.length >= 2 ? ` <em class="ml-1">(+${chapters.length - 1} more)</em>` : ''
|
|
const relativeDate = formatDistance(new Date(chapter.timestamp * 1000), new Date(), { addSuffix: true })
|
|
recStr += `
|
|
<div class="col-xl-3 col-md-4 col-sm-6 border-bottom p-2 text-left">
|
|
<div class="rounded sm_md_logo float-left mr-2">
|
|
<a href="${manga.url}">
|
|
<img class="rounded max-width" src="${manga.coverThumb}">
|
|
</a>
|
|
</div>
|
|
<div>
|
|
<div class="text-truncate py-0 mb-1 border-bottom">
|
|
<span class="fas fa-book fa-fw" aria-hidden="true" title=""></span>
|
|
<a class="manga_title" title="${manga.title}" href="${manga.url}">${manga.title}</a>
|
|
</div>
|
|
<p class="py-0 mb-1 row no-gutters align-items-center flex-nowrap">
|
|
<div class="col-auto">
|
|
<span class="col-auto px-0">
|
|
<span class="rounded flag flag-${chapter.language}"></span>
|
|
</span>
|
|
<a class="" href="${chapter.url}" data-chapter="${chapter.id}">${chapter.fullTitle}</a>
|
|
${more}
|
|
</div>
|
|
</p>
|
|
<p class="text-truncate py-0 mb-1"><span class="far fa-clock fa-fw " aria-hidden="true" title=""></span> ${relativeDate}</span></p>
|
|
</div>
|
|
</div>`
|
|
}
|
|
}
|
|
this.el.innerHTML = `<h2 class="text-left">Recommendations</h2><h3 class="text-left">Unread Follows</h3><div class="row no-gutters">${recStr}</div>`
|
|
const handler = (evt) => {
|
|
const chapter = evt.target.dataset.chapter || evt.currentTarget.dataset.chapter
|
|
if (chapter) {
|
|
evt.preventDefault()
|
|
evt.stopPropagation()
|
|
this.view.moveToChapter(parseInt(chapter), 1)
|
|
// this.model.setRenderer(this.model.settings.renderingMode)
|
|
}
|
|
}
|
|
this.el.querySelectorAll('a').forEach(c => c.addEventListener('click', handler, true))
|
|
return Promise.resolve()
|
|
}
|
|
|
|
pageStateHandler() { }
|
|
|
|
getChapterTitle(ch, numOnly) {
|
|
let title = ''
|
|
if (ch.volume) title += `Vol. ${ch.volume} `
|
|
if (ch.chapter) title += `Ch. ${ch.chapter} `
|
|
if (ch.title && !numOnly) title += `${ch.title}`
|
|
if (!title) title = 'Oneshot'
|
|
return title.trim()
|
|
}
|
|
}
|