mangadex/scripts/reader/reader-model.js
2021-03-19 13:06:32 -07:00

446 lines
13 KiB
JavaScript

import EventEmitter from 'wolfy87-eventemitter'
import Chapter from './resource/Chapter'
import Manga from './resource/Manga'
import Follows from './resource/Follows'
import Utils from './utils.js'
import ReaderPageModel from './ReaderPageModel'
import ReaderSetting from './ReaderSetting'
export default class ReaderModel extends EventEmitter {
constructor() {
super()
this._state = ReaderModel.readerState.READING
this._isLoading = false
this._currentPage = 0
this._chapter = null
this._renderingMode = 0
this._displayFit = 0
this._direction = 0
this._appMeta = {}
this._settings = {}
this._settingsShortcut = {}
this._pageCache = new Map()
this._preloadSet = new Set()
}
get appMeta() { return this._appMeta }
set appMeta(val) {
this._appMeta = val
}
get isUserGuest() { return this.appMeta.guest !== '0' }
get isLoading() { return this._isLoading }
set isLoading(val) {
val = !!val
if (this._isLoading !== val && !this.exiting) {
this._isLoading = val
this.trigger('loadingchange', [val])
}
}
get currentPage() { return this._currentPage }
setCurrentPage(val) {
if (this._currentPage !== val && !isNaN(val) && !this.exiting) {
this._currentPage = val
this.trigger('currentpagechange', [val])
return true
}
}
get totalPages() { return this.chapter ? this.chapter.totalPages : 0 }
get state() { return this._state }
set state(val) {
if (this._state !== val && !this.exiting) {
this._state = val
this.trigger('statechange', [val])
}
}
get isStateReading() { return this._state === ReaderModel.readerState.READING }
get isStateRecommendations() { return this._state === ReaderModel.readerState.RECS }
get isStateGap() { return this._state === ReaderModel.readerState.GAP }
get exiting() { return this._state === ReaderModel.readerState.EXITING }
exitReader() {
this.state = ReaderModel.readerState.EXITING
// bfcache
window.onunload = () => { this.state = ReaderModel.readerState.READING }
}
get renderingMode() { return this._renderingMode }
set renderingMode(val) {
if (this._renderingMode !== val && !this.exiting) {
this._renderingMode = val
this.trigger('renderingmodechange', [val])
}
}
get displayFit() { return this._displayFit }
set displayFit(val) {
if (this._displayFit !== val && !this.exiting) {
this._displayFit = val
this.trigger('displayfitchange', [val])
}
}
get direction() { return this._direction }
set direction(val) {
if (this._direction !== val && !this.exiting) {
this._direction = val
this.trigger('directionchange', [val])
}
}
get isSinglePage() { return this.renderingMode === ReaderModel.renderingModeState.SINGLE }
get isDoublePage() { return this.renderingMode === ReaderModel.renderingModeState.DOUBLE }
get isLongStrip() { return this.renderingMode === ReaderModel.renderingModeState.LONG }
get isNoResize() { return this.displayFit === ReaderModel.displayFitState.NO_RESIZE }
get isFitHeight() { return this.displayFit === ReaderModel.displayFitState.FIT_HEIGHT }
get isFitWidth() { return this.displayFit === ReaderModel.displayFitState.FIT_WIDTH }
get isFitBoth() { return this.displayFit === ReaderModel.displayFitState.FIT_BOTH }
get isDirectionLTR() { return this.direction === ReaderModel.directionState.LTR }
get isDirectionRTL() { return this.direction === ReaderModel.directionState.RTL }
get settings() { return this._settingsShortcut }
get settingDefaults() {
return Array.from(Object.values(this._settings)).reduce((acc, setting) => {
acc[setting.name] = setting.default
return acc
}, {})
}
saveSetting(key, value) {
const setting = this._settings[key]
setting.save(value)
this._settingsShortcut[setting.name] = setting.value
if (['renderingMode', 'displayFit', 'direction'].includes(key)) {
this[key] = setting.value
}
this.trigger('settingchange', [setting.name, setting.value])
}
loadSettings() {
const defaults = [
new ReaderSetting('displayFit', ReaderModel.displayFitState.FIT_WIDTH),
new ReaderSetting('direction', ReaderModel.directionState.LTR),
new ReaderSetting('renderingMode', ReaderModel.renderingModeState.SINGLE),
new ReaderSetting('showAdvancedSettings', 0),
new ReaderSetting('scrollingMethod', 0),
new ReaderSetting('swipeDirection', 0),
new ReaderSetting('swipeSensitivity', 3),
new ReaderSetting('pageTapTurn', 1),
new ReaderSetting('pageTurnLongStrip', 1),
new ReaderSetting('pageWheelTurn', 0),
new ReaderSetting('showDropdownTitles', 1),
new ReaderSetting('tapTargetArea', 1),
new ReaderSetting('hideHeader', 0),
new ReaderSetting('hideSidebar', 0),
new ReaderSetting('hidePagebar', 0),
new ReaderSetting('collapserStyle', 0),
new ReaderSetting('hideCursor', 0),
new ReaderSetting('betaRecommendations', 0),
new ReaderSetting('imageServer', '0'),
new ReaderSetting('dataSaverV2', 0),
new ReaderSetting('gapWarning', 1),
new ReaderSetting('restrictChLang', 1, null, (val) => {
val = parseInt(val)
if (this.chapter && this.manga) {
this.manga.updateChapterList(this.chapter, val)
this.trigger('chapterlistchange', [])
}
return val
}),
new ReaderSetting('preloadPages', 10, () => true, (val) => {
if (isNaN(parseInt(val)))
return 10
const clamped = Utils.clamp(parseInt(val), 0, this.preloadMax)
return !isNaN(clamped) ? clamped : 0
}),
new ReaderSetting('containerWidth', null, (val) => {
return !val || !isNaN(parseInt(val))
}),
]
for (let setting of defaults) {
setting.load()
this._settings[setting.name] = setting
this._settingsShortcut[setting.name] = setting.value
this.trigger('settingchange', [setting.name, setting.value])
}
this.saveSetting('gapWarning', 1)
ReaderSetting.clearAllExcept(defaults.map(s => s.name))
}
get chapter() { return this._chapter }
get manga() { return this._chapter ? this._chapter.manga : null }
getChapterParams() {
return {
server: this.settings.imageServer !== '0' ? this.settings.imageServer : null,
saver: this.settings.dataSaverV2,
}
}
async setChapter(id, pg = 1) {
if (this.exiting || this.isLoading) {
throw new Error(`Trying to set chapter while ${this.exiting ? 'exiting' : 'loading'}`)
} else if (isNaN(id) || id <= 0) {
throw new Error("Trying to set invalid chapter: " + id)
}
try {
this.isLoading = true
let chapter = Chapter.getResource(id)
if (chapter && !chapter.isFullyLoaded) {
chapter = await Chapter.load(id, this.getChapterParams())
} else if (!chapter) {
chapter = await Chapter.loadWithManga(id, this.getChapterParams())
await Manga.loadChapterList(chapter.manga.id)
}
const oldManga = this.manga
this._chapter = chapter
this._currentPage = pg
this.isLoading = false
chapter.manga.updateChapterList(chapter, this.settings.restrictChLang)
this._createPageCache(chapter)
this.state = ReaderModel.readerState.READING
this.trigger('chapterchange', [chapter])
if (!oldManga || oldManga.id !== chapter.manga.id) {
this.trigger('mangachange', [chapter.manga])
}
if (chapter.error || chapter.isExternal) {
throw chapter
} else if (chapter.totalPages === 0) {
chapter.error = new Error("Chapter has no pages.")
throw chapter
}
if (chapter.isNetworkServer) {
this.preload(this.currentPage, Infinity)
}
return chapter
} catch (error) {
console.error(error)
this.isLoading = false
this.state = ReaderModel.readerState.ERROR
this.trigger('readererror', [error])
return error
}
}
async reloadChapterList() {
if (this.manga.isChapterListOutdated) {
this.isLoading = true
try {
await Manga.loadChapterList(this.manga.id)
} catch (error) { }
this.isLoading = false
}
}
_createPageCache(chapter) {
for (let [i, page] of this._pageCache) {
page.unload()
page.off()
}
this._pageCache.clear()
this._preloadSet.clear()
this._preloading = false
let pgNum = 1
for (let [url, fallbackURL] of chapter.getAllPageUrls()) {
const page = new ReaderPageModel(pgNum, chapter.id, url, fallbackURL)
this._pageCache.set(pgNum, page)
page.on('statechange', (page) => {
switch (page.state) {
case ReaderPageModel.STATE_LOADING: return this.trigger('pageloading', [page])
case ReaderPageModel.STATE_LOADED: return this.trigger('pageload', [page])
case ReaderPageModel.STATE_ERROR: return this.trigger('pageerror', [page])
}
})
++pgNum
}
}
_loadPage(pg, skipCache = false) {
const page = this._pageCache.get(pg)
if (!page) {
return Promise.reject(new Error(`Page ${pg} not in cache`))
} else {
return page.load(skipCache)
}
}
getPage(pg) {
return new Promise((resolve, reject) => {
if (this.chapter == null || this._pageCache.size === 0) {
return reject(new Error("Tried to get a page before chapter has loaded"))
} else if (isNaN(pg) || pg < 1 || pg > this.totalPages) {
return resolve(null)
// return reject(new Error("Page not a number or out of bounds"))
} else if (this._pageCache.get(pg).loaded) {
return resolve(this._pageCache.get(pg))
} else {
return resolve(this._loadPage(pg))
}
})
}
reloadErrorPages() {
Array.from(this._pageCache.values()).filter(i => i.hasError).forEach(i => i.reload())
}
getPageWithoutLoading(pg) {
if (!this._pageCache.has(pg)) {
//throw new Error(`No page ${pg} set.`)
return null
}
return this._pageCache.get(pg)
}
get currentPageObject() { return this.getPageWithoutLoading(this.currentPage) }
get isPageCacheEmpty() { return this._pageCache.size === 0 }
getAllPages() {
return Array.from(this._pageCache.values())
}
getLoadedPages() {
return this.getAllPages().filter(i => i.loaded || i.hasError)
}
_preloadNextInSet() {
if (this._preloadSet.size > 0) {
this._preloading = true
const pg = [...this._preloadSet][0]
this._preloadSet.delete(pg)
this._loadPage(pg)
.catch((page) => { /*console.warn(`Preload failed for page ${pg}`)*/ })
.then(() => this._preloadNextInSet())
} else {
this._preloading = false
}
}
_preloadArray(pages) {
if (pages.length > 0) {
pages
.filter(pg => !this.getPageWithoutLoading(pg).loaded)
.forEach(pg => this._preloadSet.add(pg))
if (!this._preloading) {
return this._preloadNextInSet()
}
}
}
get preloadMax() {
return this.isUserGuest ? PRELOAD_MAX_GUEST : PRELOAD_MAX_USER
}
// TODO: preload backwards iff [current, end] already loaded
preload(start = this.currentPage + 1, amount = this.settings.preloadPages) {
if (this.isPageCacheEmpty) {
return
}
if (amount == null) {
amount = 10
}
if (amount !== Infinity) {
amount = Utils.clamp(amount, 0, this.chapter.isNetworkServer ? this.totalPages : this.preloadMax)
}
start = Utils.clamp(start, 1, this.totalPages + 1)
const end = Utils.clamp(start + amount, 1, this.totalPages + 1)
return this._preloadArray(Utils.range(start, end))
}
preloadEverything() {
return this.preload(1, Infinity) // lol
}
moveToPage(pg) {
if (!this.chapter || isNaN(pg)) {
return Promise.resolve()
} else if (pg <= -1) {
return this.moveToPage(this.totalPages)
} else if (pg === 0) {
return Promise.reject({ chapter: this.chapter.prevChapterId, page: -1 })
// return this.moveToChapter(this.chapter.prevChapterId, -1)
} else if (pg > this.totalPages) {
return Promise.reject({ chapter: this.chapter.nextChapterId, page: 1 })
// return this.moveToChapter(this.chapter.nextChapterId, 1)
} else {
if (this.currentPage === pg) {
this.trigger('currentpagechange', [pg])
} else {
this.setCurrentPage(pg)
}
return Promise.resolve()
}
}
moveToChapter(id, pg = 1) {
if (id <= 0) {
return Promise.reject({ chapter: id })
} else {
return this.setChapter(id, pg).then(() => {
if (this.chapter && this.totalPages > 0 && this.isStateReading) {
return this.moveToPage(pg)
} else {
return Promise.resolve()
}
})
}
}
moveToRecommendations() {
this.isLoading = true
return Follows.load({}, false)
.then(recs => {
this.recommendations = recs
this.isLoading = false
this.state = ReaderModel.readerState.RECS
return Promise.resolve(recs)
})
.catch(err => {
this.recommendations = null
this.isLoading = false
this.state = ReaderModel.readerState.ERROR
this.trigger('readererror', [err])
return Promise.reject(err)
})
}
}
const PRELOAD_MAX_USER = 20
const PRELOAD_MAX_GUEST = 5
ReaderModel.renderingModeState = {
SINGLE: 1,
DOUBLE: 2,
LONG: 3,
ALERT: 4,
RECS: 5,
}
ReaderModel.directionState = {
LTR: 1,
RTL: 2,
}
ReaderModel.displayFitState = {
FIT_BOTH: 1,
FIT_WIDTH: 2,
FIT_HEIGHT: 3,
NO_RESIZE: 4,
}
ReaderModel.readerState = {
ERROR: 0,
READING: 1,
RECS: 2,
EXITING: 3,
GAP: 4,
}