mangadex/scripts/reader/ui.js

356 lines
13 KiB
JavaScript
Raw Normal View History

2021-03-19 16:06:32 -04:00
'use strict'
/* global Renderer */
const utils = {
empty: (node) => {
while (node.firstChild) {
node.removeChild(node.firstChild)
}
}
}
class UI {
constructor(reader) {
this.reader = reader
}
get isSinglePage() { return this.renderingMode === UI.RENDERING_MODE.SINGLE }
get isDoublePage() { return this.renderingMode === UI.RENDERING_MODE.DOUBLE }
get isLongStrip() { return this.renderingMode === UI.RENDERING_MODE.LONG }
get isNoResize() { return this.displayFit === UI.DISPLAY_FIT.NO_RESIZE }
get isFitHeight() { return this.displayFit === UI.DISPLAY_FIT.FIT_HEIGHT }
get isFitWidth() { return this.displayFit === UI.DISPLAY_FIT.FIT_WIDTH }
get isFitBoth() { return this.displayFit === UI.DISPLAY_FIT.FIT_BOTH }
get isDirectionLTR() { return this.direction === UI.DIRECTION.LTR }
get isDirectionRTL() { return this.direction === UI.DIRECTION.RTL }
get renderedPages() {
if (this.renderer == null) {
return 0
} else if (this.isLongStrip) {
return 1
} else {
return this.renderer.renderedPages
}
}
initializeContainer(userIsGuest = false) {
this.container = document.querySelector('div[role="main"]')
this.container.classList.remove('container')
this.container.classList.add('reader', 'row', 'flex-column', 'flex-lg-row', 'no-gutters')
this.imageContainer = this.container.querySelector('.reader-images')
document.querySelector('footer').classList.add('d-none')
document.body.style.removeProperty('margin-bottom')
if (userIsGuest) {
const reportBtn = this.container.querySelector('#report-button')
reportBtn.dataset.toggle = ''
reportBtn.href = '/login'
reportBtn.firstElementChild.classList.replace('fa-flag', 'fa-sign-in-alt')
}
}
setRenderer(mode = this.reader.settings.renderingMode, useHistory = true) {
if (this.renderingMode !== mode) {
if (this.renderer != null) {
this.renderer.destroy()
}
this.renderingMode = mode
switch(mode) {
case UI.RENDERING_MODE.LONG:
this.renderer = new Renderer.LongStrip(this.reader)
break
case UI.RENDERING_MODE.DOUBLE:
this.renderer = new Renderer.DoublePage(this.reader)
break
case UI.RENDERING_MODE.SINGLE:
default:
this.renderer = new Renderer.SinglePage(this.reader)
break
case UI.RENDERING_MODE.ALERT:
this.renderer = new Renderer.Alert(this.reader)
break
case UI.RENDERING_MODE.RECS:
this.renderer = new Renderer.Recommendations(this.reader)
break
}
this.container.dataset.renderer = this.renderer.name
if (useHistory) {
this.pushHistory(this.reader.currentPage)
}
}
}
setDirection(direction) {
this.direction = direction
this.container.dataset.direction = UI.DIRECTION.LTR === direction ? 'ltr' : 'rtl'
}
setDisplayFit(fit) {
this.displayFit = fit
this.container.dataset.display = UI.DISPLAY_FIT_STR[fit]
this.container.classList.toggle('fit-horizontal', this.isFitBoth || this.isFitWidth)
this.container.classList.toggle('fit-vertical', this.isFitBoth || this.isFitHeight)
}
onChapterChange(chapter) {
if (this.renderer) {
this.renderer.initialize()
}
this.container.classList.toggle('native-long-strip', chapter.manga.isLongStrip)
this.setRenderer(chapter.manga.isLongStrip ? UI.RENDERING_MODE.LONG : this.reader.settings.renderer, false)
this.setDisplayFit(chapter.manga.isLongStrip ? UI.DISPLAY_FIT.FIT_WIDTH : this.reader.settings.displayFit)
this.resetPageBar(chapter.totalPages)
// TODO?: it's assumed that the input is already escaped; when not, change innerHTML to textContent
this.updateTitles(chapter)
this.updateChapterDropdown(chapter, !this.reader.settings.showDropdownTitles)
this.updatePageDropdown(chapter.totalPages, this.reader.currentPage)
this.updateGroupList(chapter)
this.updateCommentsButton(chapter)
this.updateChapterLinks(chapter)
}
updateSetting(key, value) {
switch (key) {
case 'direction':
this.setDirection(value)
this.updateChapterLinks()
this.setRenderer()
if (this.reader.currentPage) {
this.render(this.reader.currentPage)
}
break
case 'renderingMode':
this.setRenderer(value)
if (this.reader.currentPage) {
this.render(this.reader.currentPage)
}
break
case 'displayFit':
this.setDisplayFit(value)
break
case 'containerWidth':
if (!value) {
value = null
}
this.imageContainer.style.maxWidth = value ? `${value}px` : null
break
case 'showDropdownTitles':
this.updateChapterDropdown(this.reader.chapter, !value)
break
case 'hideHeader':
this.container.classList.toggle('hide-header', value)
document.querySelector('nav.navbar').classList.toggle('d-none', value)
document.querySelector('#fullscreen-button').classList.toggle('active', value)
break
case 'hideSidebar':
this.container.classList.toggle('hide-sidebar', value)
break
case 'hidePagebar':
this.container.classList.toggle('hide-page-bar', value)
break
}
Array.from(this.container.querySelectorAll(`#modal-settings input[data-setting="${key}"]`)).forEach(n => { n.value = value })
Array.from(this.container.querySelectorAll(`#modal-settings select[data-setting="${key}"]`)).forEach(n => { n.value = value })
Array.from(this.container.querySelectorAll(`#modal-settings button[data-setting="${key}"]`)).forEach(n => { n.classList.toggle('active', n.dataset.value == value) })
}
updateTitles(chapter) {
const manga = chapter.manga
document.title = `${manga.title} - ${manga.getChapterTitle(chapter.id)} - MangaDex`
const mangaFlag = this.container.querySelector('.reader-controls-title .lang-flag')
mangaFlag.parentElement.replaceChild(UI.flagImg(manga.langCode, manga.langName), mangaFlag)
const mangaLink = this.container.querySelector('.manga-link')
mangaLink.href = manga.url
mangaLink.title = manga.title
mangaLink.innerHTML = manga.title
this.container.querySelector('.chapter-title').innerHTML = chapter.title
this.container.querySelector('.chapter-tag-h').classList.toggle('d-none', !manga.isHentai)
this.container.querySelector('.chapter-tag-end').classList.toggle('d-none', !chapter.isLastChapter)
this.container.querySelector('.chapter-tag-doujinshi').classList.toggle('d-none', !manga.isDoujinshi)
}
updateChapterDropdown(chapter, hideTitles) {
if (chapter) {
const manga = chapter.manga
const chapters = this.container.querySelector('#jump-chapter')
utils.empty(chapters)
for (let ch of manga.chapterList.slice().reverse()) {
const option = chapters.appendChild(document.createElement('option'))
option.value = ch.id
option.selected = ch.id === chapter.id
option.appendChild(document.createTextNode(manga.getChapterTitle(ch.id, hideTitles)))
}
}
}
updatePageDropdown(totalPages, currentPage) {
const pages = this.container.querySelector('#jump-page')
utils.empty(pages)
for (let i = 1; i <= totalPages; ++i) {
const option = pages.appendChild(document.createElement('option'))
option.value = i
option.selected = currentPage === i
option.appendChild(document.createTextNode(i))
}
}
updateGroupList(chapter) {
const groups = this.container.querySelector('.reader-controls-groups ul')
utils.empty(groups)
for (let g of chapter.manga.getGroupsOfChapter(chapter.id)) {
const li = groups.appendChild(document.createElement('li'))
const flag = li.appendChild(UI.flagImg(g.lang_code, g.lang_code))
flag.classList.add('mr-1')
const link = li.appendChild(document.createElement(g.id == chapter.id ? 'b' : 'a'))
link.innerHTML = [g.group_name, g.group_name_2, g.group_name_3].filter(n => n).join(' | ')
link.dataset.chapter = g.id
link.href = `/chapter/${g.id}`
}
}
updateCommentsButton(chapter) {
this.container.querySelector('#comment-button').href = this.pageURL(chapter.id) + '/comments'
this.container.querySelector('.comment-amount').textContent = chapter.comments || ''
}
updateChapterLinks(chapter) {
if (chapter) {
const update = (toLeft) => {
if (this.direction != null) {
let id = (toLeft === this.isDirectionLTR) ? chapter.prevChapterId : chapter.nextChapterId
return (a) => {
a.dataset.chapter = id
a.href = this.pageURL(id)
a.title = chapter.manga.getChapterTitle(id) || 'Back to manga'
}
}
}
Array.from(this.container.querySelectorAll('a.chapter-link-left')).forEach(update(true))
Array.from(this.container.querySelectorAll('a.chapter-link-right')).forEach(update(false))
}
}
updatePageLinks(chapter, pg) {
if (chapter && typeof pg === 'number') {
const pgStr = `${pg}${this.renderedPages===2 ? ` - ${pg + 1}` : ''}`
this.container.querySelector('.reader-controls-pages .current-page').textContent = pgStr
this.container.querySelector('.reader-controls-pages .total-pages').textContent = chapter.totalPages
this.container.querySelector('.reader-controls-pages .page-link-left').href = this.pageLeftURL(chapter.id, 1)
this.container.querySelector('.reader-controls-pages .page-link-right').href = this.pageRightURL(chapter.id, 1)
this.container.querySelector('#jump-page').value = pg
}
}
pageURL(id, pg) {
if (id != null && id > 0) {
if (pg != null) {
if (pg === 0) {
return this.pageURL(this.reader.chapter.prevChapterId, -1)
} else if (pg > this.reader.chapter.totalPages) {
return this.pageURL(this.reader.chapter.nextChapterId)
}
return this.isTestReader ? `/?page=chapter_test&id=${id}&p=${pg}` : `/chapter/${id}/${pg}`
}
return this.isTestReader ? `/?page=chapter_test&id=${id}` : `/chapter/${id}`
}
return this.manga.url
}
pageLeftURL(id, pages = this.isDoublePage ? 2 : 1) {
return this.pageURL(id, Math.min(this.reader.currentPage + (this.isDirectionLTR ? -pages : pages)), 0)
}
pageRightURL(id, pages = this.isDoublePage ? 2 : 1) {
return this.pageURL(id, Math.min(this.reader.currentPage + (this.isDirectionLTR ? pages : -pages)), 0)
}
resetPageBar(totalPages) {
if (totalPages) {
const notches = this.container.querySelector('.reader-page-bar .notches')
utils.empty(notches)
for (let i = 1; i <= totalPages; ++i) {
const notch = notches.appendChild(document.createElement('div'))
notch.classList.add('notch', 'col')
notch.style.order = i
notch.dataset.page = i
notch.title = `Page ${i}`
// notch.classList.toggle('trail', i <= pg-this.renderedPages)
// notch.classList.toggle('thumb', i > pg-this.renderedPages && i <= pg)
}
this.updatePageBar(totalPages, 1)
}
}
updatePageBar(totalPages, pg) {
if (totalPages && typeof pg === 'number') {
const trail = this.container.querySelector('.reader-page-bar .trail')
const thumb = this.container.querySelector('.reader-page-bar .thumb')
const notchSize = 100 / Math.max(totalPages, 1)
trail.style.width = Math.min(pg * notchSize, 100) + '%'
thumb.style.width = (100 / pg * Math.max(this.reader.renderedPages, 1)) + '%'
trail.style.right = this.reader.isDirectionLTR ? null : 0
thumb.style.float = this.reader.isDirectionLTR ? 'right' : 'left'
}
}
render(chapter, pg) {
if (!pg || chapter == null || this.ui.renderer == null) {
return Promise.reject()
}
return this.renderer.render(pg).then(() => {
this.updatePageLinks(chapter, this.reader.currentPage)
this.updatePageBar(chapter, this.reader.currentPage + this.renderedPages - 1)
}).catch((err) => this.renderError(err))
}
renderError(err) {
console.error('Render error', err)
this.setRenderer(UI.RENDERING_MODE.ALERT)
return this.renderer.render({ type: 'danger', 'message': err.message, 'err': err })
}
static flagImg (langCode = 'jp', langName = 'Unknown') {
const flag = document.createElement('img')
flag.classList.add('lang-flag')
flag.src = `https://mangadex.org/images/flags/${langCode}.png`
flag.alt = langName
flag.title = langName
return flag
}
}
UI.RENDERING_MODE = {
SINGLE: 1,
DOUBLE: 2,
LONG: 3,
ALERT: 4,
RECS: 5,
}
UI.DIRECTION = {
LTR: 1,
RTL: 2,
}
UI.DISPLAY_FIT = {
FIT_BOTH: 1,
FIT_WIDTH: 2,
FIT_HEIGHT: 3,
NO_RESIZE: 4,
}
UI.DISPLAY_FIT_STR = {
1: 'fit-both',
2: 'fit-width',
3: 'fit-height',
4: 'no-resize',
}
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = UI
} else if (typeof define === 'function' && define.amd) {
define([], function () {
return UI
})
} else if (typeof exports === 'object') {
exports.UI = UI
} else {
(typeof window !== 'undefined' ? window : this).UI = UI
}