import EventEmitter from 'wolfy87-eventemitter' import ReaderModel from './reader-model.js' import * as ReaderComponent from './reader-component.js' import KeyboardShortcuts from './keyboard-shortcuts.js' import * as Renderer from './renderer.js' import Utils from './utils.js' const HISTORY_UPDATE_DELAY = 100 export default class ReaderView extends EventEmitter { constructor(model) { super() this.model = model this.el = null this.imageContainer = null this.renderer = null this._isRendering = false this._lastHistoryUpdate = Date.now() this._listeners = {} } get renderedPages() { if (this.renderer == null) { return 0 } else if (this.model.isLongStrip) { return 1 } else { return this.renderer.renderedPages } } get isRendering() { return this._isRendering } set isRendering(val) { this._isRendering = val this.onLoadingChange() } initialize(container) { if (!container) { throw new Error("Main container missing") } this.el = container this.imageContainer = this.el.querySelector('.reader-images') if (!this.imageContainer) { throw new Error("Image container (.reader-images) missing") } this.el.classList.remove('container') this.el.classList.add('reader', 'row', 'flex-column', 'no-gutters') this.el.classList.add('layout-horizontal') const footer = document.querySelector('footer') if (footer) { footer.classList.add('d-none') } document.body.style.removeProperty('margin-bottom') if (this.model.isUserGuest) { const reportBtn = this.el.querySelector('#report-button') if (reportBtn) { reportBtn.dataset.toggle = '' reportBtn.href = '/login' reportBtn.firstElementChild.classList.replace('fa-flag', 'fa-sign-in-alt') } } for (let [key, value] of Object.entries(this.model.settings)) { this.onSettingChange(key, value) } this.onRenderingModeChange() this.onDisplayFitChange() this.onDirectionChange() this.initializeModelEventHandlers() } addListeners() { this.listenSettingEvents() this.listenButtonEvents() this.listenInputs() } initializeModelEventHandlers() { this.model.on('loadingchange', () => this.onLoadingChange()) this.model.on('renderingmodechange', () => this.onRenderingModeChange()) this.model.on('displayfitchange', () => this.onDisplayFitChange()) this.model.on('directionchange', () => this.onDirectionChange()) this.model.on('chapterchange', () => this.onChapterChange()) this.model.on('chapterlistchange', () => this.onChapterListChange()) this.model.on('mangachange', () => this.onMangaChange()) this.model.on('statechange', () => this.onStateChange()) this.model.on('currentpagechange', () => this.onCurrentPageChange()) this.model.on('readererror', (error) => this.onReaderError(error)) this.model.on('pageload', (page) => this.onPageLoad(page)) this.model.on('pageerror', (page) => this.onPageError(page)) this.model.on('settingchange', (key, value) => this.onSettingChange(key, value)) } onLoadingChange() { this.el.classList.toggle('is-loading', this.model.isLoading || this._isRendering) } onRenderingModeChange() { if (this.renderer != null) { this.renderer.destroy() } const rendererClass = ReaderView.getRendererClass(this.model.renderingMode) this.renderer = new rendererClass(this.imageContainer, this.model, this) this.el.dataset.renderer = this.renderer.name this.toggleListener('go to top scroll', this.model.isLongStrip) this.resetGoToTop() if (this.model.chapter) { this.renderer.initialize() if (this.renderer.name === 'long-strip') { try { this.renderer.scrollToPage(this.model.currentPage) } catch (e) { } } } } renderPageInNewMode(mode, page) { if (this.model.renderingMode !== mode) { this.model.renderingMode = mode } return this.renderPage(page) } onDisplayFitChange() { this.el.dataset.display = ReaderView.getDisplayFitString(this.model.displayFit) this.el.classList.toggle('fit-horizontal', this.model.isFitBoth || this.model.isFitWidth) this.el.classList.toggle('fit-vertical', this.model.isFitBoth || this.model.isFitHeight) this.forceImageLayoutRefresh() } forceImageLayoutRefresh() { // tries to fix a Chrome bug by forcing a refresh on the image positions if (Modernizr.requestanimationframe) { Array.from(this.el.querySelectorAll('.reader-image-wrapper img')).forEach(i => { i.classList.add('m-0') requestAnimationFrame(() => i.classList.remove('m-0')) }) } } onDirectionChange() { this.el.dataset.direction = ReaderModel.directionState.LTR === this.model.direction ? 'ltr' : 'rtl' if (this.model.chapter) { this.updateChapterLinks() this.updatePageBar() } } onChapterChange() { if (!this.model.chapter || !this.model.manga) { return } const nativeLongStrip = this.model.manga.isLongStrip this.model.renderingMode = nativeLongStrip ? ReaderModel.renderingModeState.LONG : this.model.settings.renderingMode this.model.displayFit = nativeLongStrip ? ReaderModel.displayFitState.FIT_WIDTH : this.model.settings.displayFit if (this.renderer) { this.renderer.reinitialize() } this.el.dataset.mangaId = this.model.manga.id || 0 this.el.dataset.chapterId = this.model.chapter.id || 0 this.el.dataset.totalPages = this.model.totalPages || 0 this.el.classList.toggle('native-long-strip', nativeLongStrip) this.resetPageBar() this.updateUI() const preloadAllBtn = document.querySelector('#preload-all') if (preloadAllBtn) { preloadAllBtn.disabled = this.model.isUserGuest preloadAllBtn.textContent = this.model.isUserGuest ? 'Logged in users only' : 'Start preloading' } } onChapterListChange() { if (!this.model.chapter || !this.model.manga) { return } this.updateChapterDropdown() this.updateGroupList() this.updateChapterLinks() } onMangaChange() { if (!this.model.chapter || !this.model.manga) { return } if (this.model.settings.gapWarning && this.model.chapter.isSequentialWith(this.model.chapter.prevChapter) === false) { this.moveToGapCheck(this.model.chapter.id, this.model.chapter.prevChapterId) } } onStateChange() { this.toggleListener('page tap', this.model.isStateReading) this.toggleListener('page wheel', this.model.isStateReading) switch (this.model.state) { case ReaderModel.readerState.RECS: this.imageContainer.classList.remove('cursor-pointer') // this.model.renderingMode = ReaderModel.renderingModeState.RECS this.renderPageInNewMode(ReaderModel.renderingModeState.RECS, {}) break case ReaderModel.readerState.GAP: this.renderPageInNewMode(ReaderModel.renderingModeState.ALERT, { isSpoilerNet: true, chapterId: this.gapChapterId, prevChapterId: this.gapPrevChapterId }) break case ReaderModel.readerState.EXITING: this.el.classList.add('is-loading') break case ReaderModel.readerState.ERROR: this.imageContainer.classList.remove('cursor-pointer') break case ReaderModel.readerState.READING: default: this.imageContainer.classList.toggle('cursor-pointer', this.model.settings.tapTargetArea) this.model.renderingMode = this.model.settings.renderingMode break } } onCurrentPageChange() { this.el.dataset.currentPage = this.model.currentPage || 0 if (this.model.currentPage > 0) { this.model.preload(this.model.currentPage + this.renderedPages) this.renderPage(this.model.currentPage) } } onPageLoad(page) { if (this.model.chapter && page.chapter === this.model.chapter.id) { const notch = this.el.querySelector(`.notch[data-page="${page.number}"]`) if (notch) { notch.classList.add('loaded') notch.classList.remove('failed') } } } onPageError(page) { if (this.model.chapter && page.chapter === this.model.chapter.id) { const notch = this.el.querySelector(`.notch[data-page="${page.number}"]`) if (notch) { notch.classList.add('failed') } } } onReaderError(error) { this.renderPageInNewMode(ReaderModel.renderingModeState.ALERT, error) } get swipeThreshold() { return screen.availWidth / ((this.model.settings.swipeSensitivity + 1) * 0.9) } onSettingChange(key, value) { let el = null switch (key) { case 'renderingMode': if (this.model.chapter) { this.renderPage() } break case 'showAdvancedSettings': el = this.el.querySelector('#modal-settings') if (el) { el.classList.toggle('show-advanced', !!value) } break case 'swipeSensitivity': if (jQuery) { jQuery(this.imageContainer).swipe('option', 'threshold', this.swipeThreshold) } break case 'containerWidth': if (!value) { value = null } this.imageContainer.classList.toggle('constrained', !!parseInt(value)) this.imageContainer.style.maxWidth = parseInt(value) ? `${value}px` : null break case 'showDropdownTitles': if (this.model.chapter) { this.updateChapterDropdown() } break case 'tapTargetArea': this.imageContainer.classList.toggle('cursor-pointer', !!value) break case 'hideCursor': this.toggleListener('hide cursor', value) if (!value) { clearTimeout(this._cursorHideDebounce) this.el.classList.remove('hide-cursor') } break case 'hideHeader': this.el.classList.toggle('hide-header', value) el = document.querySelector('nav.navbar') if (el) { el.classList.toggle('d-none', value) } el = document.querySelector('#hide-header-button') if (el) { el.classList.toggle('active', value) } break case 'hideSidebar': this.el.classList.toggle('hide-sidebar', value) break case 'hidePagebar': this.el.classList.toggle('hide-page-bar', value) break case 'collapserStyle': this.el.dataset.collapser = value === 0 ? 'button' : 'bar' break case 'preloadPages': this.el.querySelector('.preload-max-value').textContent = this.model.preloadMax const input = this.el.querySelector('input[data-setting="preloadPages"]') input.max = this.model.preloadMax input.placeholder = `The amount of images (default: ${this.model.settingDefaults.preloadPages})` if (this.model.chapter) { this.model.preload(this.model.currentPage + this.renderedPages) } break case 'betaRecommendations': const recsBtn = document.querySelector('#recommendations-button') recsBtn.classList.toggle('d-none', !value) break } Array.from(this.el.querySelectorAll(`select[data-setting="${key}"]`)).forEach((n) => { n.value = value }) Array.from(this.el.querySelectorAll(`input[data-setting="${key}"]`)).forEach((n) => { n.value = value }) Array.from(this.el.querySelectorAll(`input[type="checkbox"][data-setting="${key}"]`)).forEach((n) => { n.checked = !!value }) Array.from(this.el.querySelectorAll(`button[data-setting="${key}"]`)).forEach((n) => { n.classList.toggle('active', n.dataset.value == value) }) } updateUI() { this.updateTitles() this.updateChapterDropdown() this.resetPageDropdown() this.updatePageDropdown() this.updatePageLinks() this.updateGroupList() this.updateCommentsButton() this.updateChapterLinks() } updatePage() { if (this.model.chapter) { this.updatePageBar() this.updatePageDropdown() this.updatePageLinks() } } updateTitles() { if (!this.model.chapter || !this.model.manga) { return } const chapter = this.model.chapter const manga = this.model.manga document.title = Utils.htmlTextDecodeHack(`${manga.title} - ${chapter.fullTitle} - MangaDex`) ReaderComponent.Flag.render(manga, this.el.querySelector('.reader-controls-title .flag')) ReaderComponent.Link.render(manga, this.el.querySelector('.manga-link')) this.el.querySelector('.chapter-title').textContent = Utils.htmlTextDecodeHack(chapter.title) this.el.querySelector('.chapter-title').dataset.chapterId = chapter.id this.el.querySelector('.chapter-tag-h').classList.toggle('d-none', !manga.isHentai) this.el.querySelector('.chapter-tag-end').classList.toggle('d-none', !chapter.isLastChapter) this.el.querySelector('.chapter-tag-doujinshi').classList.toggle('d-none', !manga.isDoujinshi) } resetChapterDropdown() { if (!this.model.chapter || !this.model.manga) { return } ReaderComponent.ChapterDropdown.render(this.model, this.el.querySelector('#jump-chapter')) } updateChapterDropdown() { this.resetChapterDropdown() } resetPageDropdown() { if (!this.model.chapter || !this.model.manga) { return } ReaderComponent.PageDropdown.render(this.model, this.el.querySelector('#jump-page')) } updatePageDropdown() { this.el.querySelector('#jump-page').selectedIndex = this.model.currentPage - 1 } updateGroupList() { if (!this.model.chapter || !this.model.manga) { return } const groups = this.el.querySelector('.reader-controls-groups ul') Utils.emptyNode(groups) for (let ch of this.model.manga.getAltChapters(this.model.chapter.id)) { ch.isCurrentChapter = ch.id == this.model.chapter.id groups.appendChild(ReaderComponent.GroupItem.render(ch)) } } updateCommentsButton() { if (!this.model.chapter || !this.model.manga) { return } this.el.querySelector('#comment-button').href = `${this.pageURL(this.model.chapter.id)}/comments` this.el.querySelector('.comment-amount').textContent = this.model.chapter.comments || '' } updateChapterLinks() { if (!this.model.chapter || !this.model.manga) { return } const update = (previous) => { let id = previous ? this.model.chapter.prevChapterId : this.model.chapter.nextChapterId return (a) => { a.dataset.chapter = id a.href = this.pageURL(id) a.title = this.model.chapter.fullTitle || 'Back to manga' } } Array.from(this.el.querySelectorAll('.chapter-link-left')).forEach(update(this.model.isDirectionLTR)) Array.from(this.el.querySelectorAll('.chapter-link-right')).forEach(update(this.model.isDirectionRTL)) } updatePageLinks() { if (!this.model.chapter || !this.model.manga) { return } const pg = this.model.currentPage const pgStr = pg + (this.renderedPages === 2 ? ` - ${pg + 1}` : '') const ctrlPages = this.el.querySelector('.reader-controls-pages') ctrlPages.querySelector('.current-page').textContent = pgStr ctrlPages.querySelector('.total-pages').textContent = this.model.totalPages ctrlPages.querySelector('.page-link-left').href = this.pageLeftURL(1) ctrlPages.querySelector('.page-link-right').href = this.pageRightURL(1) this.el.querySelector('#jump-page').value = pg } resetPageBar() { if (!this.model.chapter || !this.model.manga) { return } // TODO: make less demanding on chrome const notches = this.el.querySelector('.reader-page-bar .notches') if (notches) { Utils.emptyNode(notches) for (let i = 1; i <= this.model.totalPages; ++i) { const notch = notches.appendChild(document.createElement('div')) notch.classList.add('notch', 'col') notch.style.order = i notch.dataset.page = i // const wrapper = notch.appendChild(document.createElement('div')) // const page = wrapper.appendChild(document.createElement('div')) // page.textContent = `${i} / ${this.model.totalPages}` } this.updatePageBar() } } updatePageBar() { if (!this.model.chapter || !this.model.manga) { return } const trail = this.el.querySelector('.reader-page-bar .trail') const thumb = this.el.querySelector('.reader-page-bar .thumb') if (trail && thumb) { const total = Math.max(this.model.totalPages, 1) const rendered = Math.max(this.renderedPages, 1) const pg = Math.max(this.model.currentPage + rendered - 1, 1) const notchSize = 100 / total trail.style.width = Math.min(pg * notchSize, 100) + '%' thumb.style.width = (100 / pg * rendered) + '%' trail.style.right = this.model.isDirectionLTR ? null : 0 thumb.style.float = this.model.isDirectionLTR ? 'right' : 'left' } } renderPage(pg = this.model.currentPage) { return new Promise((resolve, reject) => { if (!this.renderer) { return reject("No renderer") } else if (this.isRendering) { return reject("Already rendering") } else { //this.isRendering = true this.updatePage() return resolve(this.renderer.render(pg)) } }).then(() => { //this.isRendering = false this.el.dataset.renderedPages = this.renderedPages if (!this.model.isLongStrip) { this.scrollPageIntoView() } this.forceImageLayoutRefresh() if (this.model.chapter) { this.model.preload(this.model.currentPage + this.renderedPages) this.updatePage() } }) .catch((err) => { console.error(err) if (err && err.revert === true) { this.renderPageInNewMode(this.model.settings.renderingMode, pg) } // this.isRendering = false }) } scrollPageIntoView() { const scrollView = () => { // this.imageContainer.scrollIntoView(this.model.isFitWidth || this.model.isNoResize) this.imageContainer.scrollIntoView(true) if (this.model.isFitHeight || this.model.isNoResize) { ReaderView.scroll(document.body.scrollWidth * (this.model.isDirectionRTL ? 1 : -1), 0) } if (this.model.isFitWidth || this.model.isNoResize) { const nav = document.querySelector('nav.navbar') if (nav) { ReaderView.scroll(0, -nav.offsetHeight) } } } if (Modernizr.requestanimationframe) { // use RAF to hopefully avoid scrolling before the image has properly rendered window.requestAnimationFrame(() => { scrollView() }) } else { scrollView() } } getHistoryStateObject() { return { page: this.model.currentPage, chapter: this.model.chapter.id, state: this.model.state, } } pushHistory(chapter = this.model.chapter.id, page = this.model.currentPage) { if (Modernizr.history && this._lastHistoryUpdate + HISTORY_UPDATE_DELAY < Date.now()) { const curState = window.history.state page = !page ? null : page if (!(curState && curState.page == page && curState.chapter == chapter && curState.state == this.model.state)) { this._lastHistoryUpdate = Date.now() const url = this.pageURL(chapter, page) const newState = { chapter, page, state: this.model.state } try { window.history.pushState(newState, '', url) } catch (e) { console.warn(e) } } } } replaceHistory(chapter = this.model.chapter.id, page = this.model.currentPage) { if (Modernizr.history && this._lastHistoryUpdate + HISTORY_UPDATE_DELAY < Date.now()) { this._lastHistoryUpdate = Date.now() const url = this.pageURL(chapter, page) const newState = { chapter, page, state: this.model.state } try { window.history.replaceState(newState, '', url) } catch (e) { console.warn(e) } } } turnPageLeft(pages) { this.turnPage(this.model.isDirectionRTL, pages) } turnPageRight(pages) { this.turnPage(this.model.isDirectionLTR, pages) } turnPageBackward(pages) { this.turnPage(false, pages) } turnPageForward(pages) { this.turnPage(true, pages) } turnPage(forward, pages = this.model.isDoublePage ? 2 : 1) { if (this.model.isDoublePage && ( (forward && this.renderedPages === 1) || (!forward && this.model.currentPage <= 2))) { pages = 1 } pages = forward ? pages : -pages this.moveToPage(Math.max(this.model.currentPage, 1) + pages) } moveToPage(pg, useHistory = true) { if (pg === 'recs') { return this.moveToRecommendations(useHistory) } else if (pg === 'gap') { this.moveToGapCheck(this.model.chapter.id, this.model.chapter.prevChapterId, useHistory) return Promise.resolve() } else { const oldState = this.model.state if (!this.model.isStateReading) { this.model.state = ReaderModel.readerState.READING } return this.model.moveToPage(pg) .then(() => { if (this.model.isLongStrip) { this.renderer.scrollToPage(pg) } if (useHistory) { if (!this.model.isLongStrip) { this.pushHistory() } else { this.replaceHistory() } } }) .catch((err) => { if (err && err.chapter != null) { return this.moveToChapter(err.chapter, err.page, useHistory, oldState === ReaderModel.readerState.GAP) } else { console.error(err) } return Promise.resolve() }) } } async moveToChapter(id, pg = 1, useHistory = true, skipGapCheck = this.model.isStateGap) { if (this.model.isLoading || isNaN(id) || this.model.exiting) { return } if (id === -1) { // going backwards from the first chapter // this.exitToURL(this.model.manga.url) } else if (id === 0) { // reload chapter list data to see if there has been an update await this.model.reloadChapterList() if (this.model.chapter.nextChapterId !== 0) { return this.moveToChapter(this.model.chapter.nextChapterId, 1, useHistory) } else if (this.model.settings.betaRecommendations) { this.moveToRecommendations() } else { this.exitToURL(this.model.manga.url) } } else { if (!skipGapCheck && this.model.settings.gapWarning && id === this.model.chapter.nextChapterId) { if (this.model.chapter.isSequentialWith(id) === false) { this.moveToGapCheck(id, this.model.chapter.id, useHistory) return } } try { await this.model.moveToChapter(id, pg) if (this.model.chapter && !this.model.chapter.error && this.model.isStateReading) { return this.moveToPage(pg, useHistory) } else if (useHistory) { this.pushHistory() } } catch (e) { } } } moveToGapCheck(chapterId, prevChapterId, useHistory = true) { const gotoLastPage = (this.model.isDoublePage && this.renderedPages === 2 && this.model.currentPage === this.model.totalPages - 1) this.gapChapterId = chapterId this.gapPrevChapterId = prevChapterId if (gotoLastPage) { this.model.setCurrentPage(this.model.totalPages) } this.model.state = ReaderModel.readerState.GAP if (useHistory) { this.pushHistory(chapterId, 'gap') } } async moveToRecommendations(useHistory = true) { await this.model.moveToRecommendations() this.model.setCurrentPage(this.model.totalPages + 1) if (useHistory) { this.pushHistory(undefined, 'recs') } } exitToURL(url) { if (!this.model.exiting) { this.model.exitReader() window.location = url } } pageURL(id, pg) { if (id != null && id > 0 || typeof id === 'string') { if (pg != null && (!this.model.chapter.isExternal || typeof pg === 'string')) { if (pg === 0) { return this.pageURL(this.model.chapter.prevChapterId, -1) } else if (pg > this.model.totalPages) { return this.pageURL(this.model.chapter.nextChapterId) } return `/chapter/${id}/${pg}` } return `/chapter/${id}` } return this.model.manga.url } pageLeftURL(pages = this.model.isDoublePage ? 2 : 1) { return this.pageURL(this.model.chapter.id, Math.min(this.model.currentPage + (this.model.isDirectionLTR ? -pages : pages)), 0) } pageRightURL(pages = this.model.isDoublePage ? 2 : 1) { return this.pageURL(this.model.chapter.id, Math.min(this.model.currentPage + (this.model.isDirectionLTR ? pages : -pages)), 0) } addListener(action, el, type, handler, useCapture = false) { if (typeof el === 'string') { const elStr = el el = this.el.querySelector(elStr) if (!el) { throw new Error(`Element "${elStr}" not found`) } } this._listeners[action] = { el, type, handler, useCapture, active: false } this.toggleListener(action, true) } toggleListener(action, on) { const ln = this._listeners[action] if (ln && ln.active && !on) { ln.el.removeEventListener(ln.type, ln.handler, ln.useCapture) ln.active = false } else if (ln && !ln.active && on) { ln.el.addEventListener(ln.type, ln.handler, ln.useCapture) ln.active = true } } listenSettingEvents() { const settingInputs = [ ['#modal-settings input[type="checkbox"][data-setting]', 'change'], ['#modal-settings input[data-setting]', 'keyup'], ['#modal-settings input[data-setting]', 'change'], ['#modal-settings select[data-setting]', 'change'], ['#modal-settings button[data-setting]', 'click'], ] const saveSettingValue = (evt) => { if (evt.target.type === 'checkbox') { this.model.saveSetting(evt.target.dataset.setting, evt.target.checked ? 1 : 0) } else { const value = (evt.target.dataset.value != null ? evt.target.dataset.value : evt.target.value) this.model.saveSetting(evt.target.dataset.setting, value) } } for (let [input, evt] of settingInputs) { Array.from(this.el.querySelectorAll(input)).forEach(c => c.addEventListener(evt, saveSettingValue)) } } listenButtonEvents() { // various buttons this.el.querySelector('.reader-controls-mode-display-fit').addEventListener('click', () => { this.model.saveSetting('displayFit', this.model.displayFit % 4 + 1) }) this.el.querySelector('.reader-controls-mode-rendering').addEventListener('click', () => { this.model.saveSetting('renderingMode', this.model.renderingMode % 3 + 1) }) this.el.querySelector('.reader-controls-mode-direction').addEventListener('click', () => { this.model.saveSetting('direction', this.model.direction % 2 + 1) }) this.el.querySelector('#hide-header-button').addEventListener('click', () => { this.model.saveSetting('hideHeader', !this.model.settings.hideHeader ? 1 : 0) }) this.el.querySelector('#recommendations-button').addEventListener('click', () => { this.moveToRecommendations() }) this.el.querySelectorAll('.reader-controls-collapser').forEach(n => n.addEventListener('click', () => { this.model.saveSetting('hideSidebar', !this.model.settings.hideSidebar ? 1 : 0) })) // action links this.el.addEventListener('click', (evt) => { if (!evt.ctrlKey && !evt.metaKey) { let target = evt.target while (target && !(target.nodeName === 'A' && target.dataset.action)) { target = target.parentElement } if (target) { const data = target.dataset evt.preventDefault() switch (data.action) { case 'page': if (data.direction && data.direction === 'left') { this.turnPageLeft(parseInt(data.by)) } else if (data.direction && data.direction === 'right') { this.turnPageRight(parseInt(data.by)) } else if (data.to) { this.moveToPage(parseInt(data.to)) } break case 'chapter': return this.moveToChapter(parseInt(data.chapter)) case 'url': return this.exitToURL(target.href) } } } })//) // preload all this.addListener('preload all button', '#preload-all', 'click', (evt) => { evt.target.disabled = true evt.target.textContent = 'Preloading...' this.model.on('pageload', () => { const loaded = this.model.getLoadedPages().length evt.target.textContent = `Preloading... ${Math.round(loaded / this.model.totalPages * 100)}%` if (loaded === this.model.totalPages) { evt.target.textContent = `Preloading done` return true // unbinds handler } }) this.model.preloadEverything() }) // report form submit this.addListener('report submit', '#chapter-report-form', 'submit', (evt) => { evt.preventDefault() const btn = evt.target.querySelector('button[type=submit]') btn.classList.add('is-loading') const alert = evt.target.querySelector('.alert-container') Utils.emptyNode(alert) fetch(`/ajax/actions.ajax.php?function=chapter_report&id=${this.model.chapter.id}&server=${encodeURIComponent(this.model.chapter.server)}`, { method: 'POST', body: new FormData(evt.target), credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }).then(res => { if (res.ok) { return res.text() } else { throw new Error(res.statusText) } }).then((res) => { alert.innerHTML = res ? res : Renderer.Alert.container("This chapter has been reported.", 'success').outerHTML return Promise.resolve() }).catch((err) => { alert.innerHTML = Renderer.Alert.container("Something weird went wrong. Details in the Console (F12), hopefully.", 'danger').outerHTML console.error(err) return Promise.resolve() }).then(() => { btn.classList.remove('is-loading') }) }) // pagebar const notchDisplay = this.el.querySelector('.reader-page-bar .notch-display') this.addListener('page bar hover', '.reader-page-bar .notches', 'mouseover', (evt) => { if (evt.target.dataset.page) { notchDisplay.textContent = `${evt.target.dataset.page} / ${this.model.totalPages}` } else { notchDisplay.textContent = '' } }) } listenInputs() { // history if (Modernizr.history) { window.onpopstate = (evt) => { if (evt.state != null) { if (evt.state.state !== this.model.state) { this.model.state = evt.state.state } if (evt.state.chapter === this.model.chapter.id && evt.state.page !== null) { this.moveToPage(evt.state.page, false) } else { this.moveToChapter(evt.state.chapter, evt.state.page, false, true) } } } } // track tap this.addListener('track tap', '.reader-page-bar .track', 'click', (evt) => { evt.stopPropagation() const page = parseInt(evt.target.dataset.page || evt.currentTarget.dataset.page) if (page) { return this.moveToPage(this.isDoublePage ? Math.max(page, 1) : page) } }) // window resize let resizeDebounce = null this.addListener('window resize', window, 'resize', (evt) => { clearTimeout(resizeDebounce) resizeDebounce = setTimeout(() => this.forceImageLayoutRefresh(), 100) }) // // fullscreen // this.addListener('fullscreen', '#fullscreen-button', 'click', (evt) => { // const requestFullscreen = Modernizr.prefixed('requestFullscreen', this.el) || Modernizr.prefixed('requestFullScreen', this.el) // requestFullscreen.call(this.el) // this.el.style.overflowY = 'scroll' // }) // page tap const getImageContainerWidth = () => { try { return this.imageContainer.scrollWidth - parseFloat(window.getComputedStyle(this.imageContainer).paddingRight) } catch (err) { return this.imageContainer.scrollWidth } } let swiped = false this.addListener('page tap', this.imageContainer, 'click', (evt) => { evt.preventDefault() if (this.model.isLongStrip && this.model.settings.pageTurnLongStrip === 0) { return } if (swiped || (this.model.settings.tapTargetArea !== 1 && evt.target.nodeName.toLowerCase() !== 'img')) { swiped = false return } switch (this.model.settings.pageTapTurn) { case 1: evt.stopPropagation() const imgCW = getImageContainerWidth() const elW = document.body.clientWidth - document.body.scrollWidth + imgCW const isLeft = (evt.clientX < (elW / 2)) return isLeft ? this.turnPageLeft() : this.turnPageRight() case 2: evt.stopPropagation() return this.turnPageForward() } }) // page swipe if (jQuery) { jQuery(this.imageContainer).swipe({ swipeRight: (evt) => { if (this.model.settings.swipeSensitivity > 0) { const inverted = this.model.settings.swipeDirection === 1 inverted ? this.turnPageRight() : this.turnPageLeft() swiped = true } }, swipeLeft: (evt) => { if (this.model.settings.swipeSensitivity > 0) { const inverted = this.model.settings.swipeDirection === 1 inverted ? this.turnPageLeft() : this.turnPageRight() swiped = true } }, preventDefaultEvents: false, cancelThreshold: 10, threshold: this.swipeThreshold }) } // page wheel let wheelTicks = 0 const tickThreshold = 6 this.addListener('page wheel', this.imageContainer, 'wheel', (evt) => { if (this.model.settings.pageWheelTurn !== 0) { if ((evt.deltaY > 0 && ReaderView.isScrolledToBottom) || (evt.deltaY < 0 && ReaderView.isScrolledToTop)) { evt.preventDefault() wheelTicks++ if (!this.model.isLongStrip || (wheelTicks >= tickThreshold)) { wheelTicks = 0 this.turnPage(evt.deltaY > 0) } } else { wheelTicks = 0 } } }) // single page wheel this.addListener('single page wheel', '.reader-controls-pages', 'wheel', (evt) => { if (this.model.settings.pageWheelTurn !== 0) { if (evt.deltaY > 0) { this.turnPageForward(1) } else { this.turnPageBackward(1) } } }) // chapter dropdown this.addListener('jump chapter', '#jump-chapter', 'change', (evt) => { const newChapterId = parseInt(evt.target.value) if (!this.model.chapter || this.model.chapter.id !== newChapterId) { this.moveToChapter(newChapterId, 1) evt.target.blur() } }) // page dropdown this.addListener('jump page', '#jump-page', 'change', (evt) => { this.moveToPage(parseInt(evt.target.value)) evt.target.blur() }) // page prompt this.addListener('page prompt', '.reader-controls-page-text', 'click', (evt) => { const pg = parseInt(prompt('Move to page number:')) this.moveToPage(pg) }) // go to top const isOverThreshold = (turn, threshold) => (Math.abs(window.scrollY - turn) > threshold) this.addListener('go to top scroll', window, 'scroll', (evt) => { const gotoTop = this.el.querySelector('.reader-goto-top') const wasScrollDown = (gotoTop.dataset.scroll < window.scrollY) if (wasScrollDown === (gotoTop.dataset.turn > window.scrollY)) { gotoTop.dataset.turn = gotoTop.dataset.scroll } if (!wasScrollDown && !gotoTop.classList.contains('show') && isOverThreshold(gotoTop.dataset.turn, gotoTop.dataset.threshold)) { gotoTop.classList.add('show') } else if (wasScrollDown && gotoTop.classList.contains('show')) { gotoTop.classList.remove('show') } gotoTop.dataset.scroll = window.scrollY }) this.resetGoToTop() this.addListener('go to top click', '.reader-goto-top', 'click', () => { window.scrollTo(0, 0) this.resetGoToTop() }) // hide cursor over images this._cursorHideDebounce = null this.addListener('hide cursor', '.reader-images', 'mousemove', (evt) => { clearTimeout(this._cursorHideDebounce) this.el.classList.remove('hide-cursor') this._cursorHideDebounce = setTimeout(() => this.el.classList.add('hide-cursor'), 2000) }) this.toggleListener('hide cursor', this.model.settings.hideCursor) // kbd shortcuts KeyboardShortcuts.registerDefaults() document.addEventListener('keydown', (evt) => { KeyboardShortcuts.keydownHandler(evt, this) }) } resetGoToTop() { this.toggleListener('go to top scroll', this.model.isLongStrip) const gotoTop = this.el.querySelector('.reader-goto-top') gotoTop.dataset.scroll = 0 gotoTop.dataset.turn = 0 gotoTop.dataset.threshold = 100 gotoTop.classList.remove('show') } static scroll(left = 50, top = 50, behavior = 'auto') { Utils.scrollBy({ behavior, left, top }) } static get isTestReader() { try { return window.location.href.includes('chapter_test') } catch (err) { return false } } static getRendererClass(mode) { const STATE = ReaderModel.renderingModeState switch (mode) { case STATE.ALERT: return Renderer.Alert case STATE.RECS: return Renderer.Recommendations case STATE.LONG: return Renderer.LongStrip case STATE.DOUBLE: return Renderer.DoublePage case STATE.SINGLE: default: return Renderer.SinglePage } } static getDisplayFitString(fit) { const STATE = ReaderModel.displayFitState switch (fit) { case STATE.FIT_BOTH: return 'fit-both' case STATE.FIT_WIDTH: return 'fit-width' case STATE.FIT_HEIGHT: return 'fit-height' case STATE.NO_RESIZE: return 'no-resize' default: return 'fit-unknown' } } static get isScrolledToLeft() { return window.pageXOffset === 0 } static get isScrolledToRight() { return (window.innerWidth + Math.ceil(window.pageXOffset + 1)) >= document.body.scrollWidth } static get isScrolledToTop() { return window.pageYOffset === 0 } static get isScrolledToBottom() { return (window.innerHeight + Math.ceil(window.pageYOffset + 1)) >= document.body.scrollHeight } }