diff --git a/public/css/lrr.css b/public/css/lrr.css index b5921ce69..ade042793 100644 --- a/public/css/lrr.css +++ b/public/css/lrr.css @@ -250,6 +250,7 @@ p#nb { font-size: 15em; margin: -3.9375rem auto 0; text-align: center; + z-index: 999 } #overlay-shade { diff --git a/public/js/mod/reader_archive_overlay.js b/public/js/mod/reader_archive_overlay.js index 4ffb14fca..de2775347 100644 --- a/public/js/mod/reader_archive_overlay.js +++ b/public/js/mod/reader_archive_overlay.js @@ -7,7 +7,6 @@ import I18N from "i18n"; import { state, goToPage, loadContentData, stopAutoNextPage, toggleOverlay, getCurrentChapter, getArchiveForPage } from "./reader_common.js"; export function initializeArchiveOverlay() { - $(document).on("click.toggle-archive-overlay", "#toggle-archive-overlay", toggleArchiveOverlay); $(document).on("click.edit-metadata", "#edit-archive", () => LRR.openInNewTab(new LRR.ApiURL(`/edit?id=${state.id}`))); $(document).on("click.delete-archive", "#delete-archive", () => { const isTank = state.id.startsWith("TANK_"); @@ -189,15 +188,15 @@ export function updateArchiveOverlay(forceUpdate = false) { // Otherwise, update chapter and overlay -- If there are no chapters defined, just show all pages state.currentChapter = getCurrentChapter(); let firstPage = state.currentChapter ? state.currentChapter.startPage : 1; - let lastPage = state.currentChapter ? state.currentChapter.endPage : state.pages.length; + let lastPage = state.currentChapter ? state.currentChapter.endPage : state.pages.value.length; $("#overlay-section").text(state.currentChapter ? state.currentChapter.name : I18N.ReaderPages); if (state.currentChapter !== null) { // Create `; - if (state.content.chapters) { - state.content.chapters.forEach((chapter) => { + if (state.content.value.chapters) { + state.content.value.chapters.forEach((chapter) => { const selected = (state.currentChapter && chapter.startPage === state.currentChapter.startPage) ? "selected" : ""; chapterOptions += ``; diff --git a/public/js/mod/reader_common.js b/public/js/mod/reader_common.js index f7be1e9d2..62fcd43aa 100644 --- a/public/js/mod/reader_common.js +++ b/public/js/mod/reader_common.js @@ -5,18 +5,30 @@ import * as Server from "./server.js"; import * as LRR from "./common.js"; import I18N from "i18n"; import fscreen from "fscreen"; -import { signal, effect } from "@preact/signals"; +import { signal, effect, computed, batch } from "@preact/signals"; import { initializeStamps, updateStamps, renderMarkers, clearMarkers } from "./reader_stamps.js"; import { initializeArchiveOverlay, toggleArchiveOverlay, updateArchiveOverlay, addCategoryBadge, removeCategoryBadge } from "./reader_archive_overlay.js"; import { initializeSettings, toggleSettingsOverlay } from "./reader_options.js"; +import { initializeHeader } from "./reader_header.js"; +import { initializeFooter } from "./reader_footer.js"; + +/** + * @typedef Content + * @property {string} id + * @property {string} title + * @property {number} pages + * @property {Array} chapters + * @property {string} tags + * @property {string} summary + */ export let state = { id: "", force: false, previousPage: -1, - currentPage: -1, + currentPage: signal(0), currentChapter: null, - showingSinglePage: true, + showingSinglePage: signal(true), // TODO: compute pageThumbnails: [], preloadedImg: {}, preloadedSizes: {}, @@ -31,41 +43,79 @@ export let state = { holdDelay: 350, // Delay time in ms before continuous scroll starts on keydown scrollSpeed: 22 // Speed % to scroll when spacebar is held }, - autoNextPage: false, + autoNextPage: signal(false), autoNextPageCountdownTaskId: undefined, - autoNextPageCountdown: 0, + autoNextPageCountdown: signal(0), trackProgressLocally: null, authenticateProgress: null, containerWidth: signal(localStorage.containerWidth || null), - content: undefined, - pages: [], - maxPage: -1, + content: signal(/** @type {Content} */{ + id: "", + title: "", + pages: 0, + chapters: [], + tags: "", + summary: "", + }), + pages: signal(/** @type {Array} */ []), + maxPage: computed(() => { + return state.pages.value.length - 1; + }), mangaMode: signal(localStorage.mangaMode === "true" || false), doublePageMode: signal(localStorage.doublePageMode === "true" || false), ignoreProgress: signal(localStorage.ignoreProgress === "true" || false), infiniteScroll: signal(localStorage.infiniteScroll === "true" || false), fitMode: signal(localStorage.fitMode || "fit-container"), hideHeader: signal(localStorage.hideHeader === "true" || false), - currentPageLoaded: false, + currentPageLoaded: signal(false), + showLoader: signal(true), progress: undefined, showOverlayByDefault: signal(localStorage.showOverlayByDefault === "true" || false), preloadCount: signal((localStorage.preloadCount === "" ? null : localStorage.preloadCount) ?? 2), - AutoNextPageInterval: signal(+localStorage.AutoNextPageInterval || 10), + AutoNextPageInterval: signal(/** @type {number} */+localStorage.AutoNextPageInterval || 10), markerMode: false, markersVisible: signal(localStorage.markersVisible === "true" || false), markers: [], overlayFiltered: false, pageNaviState: true, wakeLock: null, + isBookmarked: signal(/** @type boolean|null */null), + filename: signal(""), // TODO: compute from bigger object + filenameDoublePage: signal(""), // TODO: compute from bigger object + width: signal(0), + height: signal(0), + size: signal(0), + // Ugly but eh + width2: signal(0), + height2: signal(0), + size2: signal(0), + artist: computed(() => { + const res = state.content.value.tags.match(/artist:([^,]+)(?:,|$)/i); + if (res) { + return res[1]; + } + return ""; + }), + /** + * @param {number} newCount + */ + setCurrentPage: function (newCount) { + state.currentPage.value = Math.max(+newCount, 0); + }, + multiArchiveNavigation: signal(false), }; let infiniteScrollObserver = null; +let loadingDebounceTimer = null; +let refreshGeneration = 0; export async function initializeAll(trackProgressLocally, authenticateProgress) { state.trackProgressLocally = trackProgressLocally; state.authenticateProgress = authenticateProgress; initializeSettings(); + initializeHeader(); + initializeFooter(); initFullscreen(); applyContainerWidth(); initializeStamps(); @@ -78,18 +128,7 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) $(document).on("keydown", (e) => { if (e.which === 32) handleShortcuts(e); }); $(document).on("wheel", handleWheel); - $(document).on("click.toggle-manga-mode", ".reading-direction", toggleMangaMode); - $(document).on("click.pagination-change-pages", ".page-link", handlePaginator); - $(document).on("click.close-overlay", "#overlay-shade", LRR.closeOverlay); - $(document).on("click.toggle-full-screen", "#toggle-full-screen", (e) => { - e.preventDefault(); - e.stopPropagation(); - toggleFullScreen(); - }); - $(document).on("click.toggle-auto-next-page", ".toggle-auto-next-page", toggleAutoNextPage); - $(document).on("click.toggle-help", "#toggle-help", toggleHelp); - $(document).on("click.toggle-bookmark", ".toggle-bookmark", toggleBookmark); $(document).on("click.regenerate-archive-cache", "#regenerate-cache", () => { window.location.href = new LRR.ApiURL(`/reader?id=${state.id}&force_reload`); }); @@ -99,20 +138,11 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) returnToIndex(); }); - // Apply full-screen utility - // F11 Fullscreen is totally another "Fullscreen", so its support is beyong consideration. - // Small override function, always returns boolean - fscreen.inFullscreen = () => !!fscreen.fullscreenElement; - if (!fscreen.fullscreenEnabled) { - // Fullscreen mode is unsupported; use attribute selector to hide all instances - $("[id='toggle-full-screen']").hide(); - } - // Infer initial information from the URL const params = new URLSearchParams(window.location.search); state.id = params.get("id"); state.force = params.get("force_reload") !== null; - state.currentPage = (+params.get("p") || 1) - 1; + state.setCurrentPage((+params.get("p") || 1) - 1); // Set up archive navigation state from the entry source (datatables vs carousel vs direct nav) await setupArchiveNavigation(); @@ -123,12 +153,9 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) // Load metadata for the requested ID and populate the page loadContentData().then(() => { - - document.title = state.content.title; - $(".max-page").text(state.content.pages); // Regex look in tags for artist - const artist = state.content.tags.match(/artist:([^,]+)(?:,|$)/i); + const artist = state.content.value.tags.match(/artist:([^,]+)(?:,|$)/i); if (artist) { const artistName = artist[1]; const artistSearchUrl = `/?sort=0&q=artist%3A${encodeURIComponent(artistName)}%24&`; @@ -136,20 +163,18 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) .attr("href", artistSearchUrl) .text(artistName); const titleContainer = $("") - .text(`${state.content.title} by `) + .text(`${state.content.value.title} by `) .append(link); - $("#archive-title").empty().append(titleContainer); $("#archive-title-overlay").empty().append(titleContainer.clone()); } else { - $("#archive-title").text(state.content.title); - $("#archive-title-overlay").text(state.content.title); + $("#archive-title-overlay").text(state.content.value.title); } - $("#tagContainer").append(LRR.buildTagsDiv(state.content.tags)); + $("#tagContainer").append(LRR.buildTagsDiv(state.content.value.tags)); const ratyEl = document.querySelector(`[data-raty]`); if (ratyEl) { - const rating = LRR.splitTagsByNamespace(state.content.tags).rating?.at(0).length; + const rating = LRR.splitTagsByNamespace(state.content.value.tags).rating?.at(0).length; new Raty(ratyEl, { starType: `i`, cancelButton: true, @@ -159,7 +184,7 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) score: rating, click: function(score, element, evt) { - let tags = LRR.splitTagsByNamespace(state.content.tags); + let tags = LRR.splitTagsByNamespace(state.content.value.tags); let selectedRating = score; if (selectedRating === null) @@ -181,7 +206,7 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) } $("#tagContainer").append(`
`); - $(".archive-summary").text(state.content.summary); + $(".archive-summary").text(state.content.value.summary); // Get the chapter for the current page (if any) state.currentChapter = getCurrentChapter(); @@ -196,6 +221,10 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) enterStandardView(); } }); + effect(() => { + if (state.infiniteScroll.value) { return; } + refreshCurrentPage(); + }); }); }); @@ -207,21 +236,13 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) effect(() => { if (state.infiniteScroll.value) { return; } localStorage.mangaMode = state.mangaMode.value; - - if (state.mangaMode.value) { - - $(".reading-direction").addClass("fa-arrow-left").removeClass("fa-arrow-right"); - } else { - $(".reading-direction").removeClass("fa-arrow-left").addClass("fa-arrow-right"); - } - - goToPage(state.currentPage); + // This can cause a spurious refresh, but is required to update the fileinfo bar + refreshCurrentPage(); }); effect(() => { if (state.infiniteScroll.value) { return; } localStorage.doublePageMode = state.doublePageMode.value; - goToPage(state.currentPage); }); effect(() => { @@ -242,12 +263,6 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) effect(() => { if (state.infiniteScroll.value) { return; } localStorage.hideHeader = state.hideHeader.value; - const i2 = document.getElementById("i2"); - if (state.hideHeader.value) { - i2.setAttribute("hidden", "hidden"); - } else { - i2.removeAttribute("hidden"); - } applyContainerWidth(); }); @@ -263,13 +278,33 @@ export async function initializeAll(trackProgressLocally, authenticateProgress) effect(() => { localStorage.preloadCount = state.preloadCount.value; }); + + effect(() => { + document.tile = state.content.value.title; + }); + + effect(() => { + if (state.currentPageLoaded.value) { + state.showLoader.value = false; + clearTimeout(loadingDebounceTimer); + loadingDebounceTimer = null; + } + }); + + effect(() => { + if (state.showLoader.value) { + $("#i3").addClass("loading"); + } else { + $("#i3").removeClass("loading"); + } + }); } export function loadContentData() { // Initialize content object to hold metadata -- This is a recursive object that will be used to build the page overlay. // (For tanks, content.chapters will hold archive chapters that can themselves contain nested chapters from ToCs) - state.content = { + let content = { id: state.id, title: "", pages: 0, @@ -294,11 +329,13 @@ export function loadContentData() { .then(r => r.ok ? r.json() : Promise.reject(new Error(I18N.ServerInfoError))) .then(data => { const tank = data.result; - state.content.title = tank.name; - state.content.tags = tank.tags || ""; - state.content.summary = tank.summary || ""; - - state.content.chapters = []; + content = { + ...content, + title: tank.name, + tags: tank.tags || "", + summary: tank.summary || "", + chapters: [], + }; // full_data contains pre-fetched metadata for every archive in order const fullData = tank.full_data || []; @@ -310,12 +347,18 @@ export function loadContentData() { // Create archive chapter (with nested ToC chapters if present) const archiveChapters = LRR.buildTankChapters(meta, pageOffset); - state.content.chapters.push(...archiveChapters); + content = { + ...content, + chapters: content.chapters.push(...archiveChapters) + }; pageOffset += meta.pagecount || 0; }); - state.content.pages = pageOffset; + state.content.value = { + ...content, + pages: pageOffset, + }; updateProgress(tank, state.id); }) .catch(err => LRR.showErrorToast(I18N.ServerInfoError, err)); @@ -323,16 +366,25 @@ export function loadContentData() { return Server.callAPI(`/api/archives/${state.id}/metadata`, "GET", null, I18N.ServerInfoError, (data) => { - state.content.title = data.title; - state.content.pages = data.pagecount; - state.content.tags = data.tags; - state.content.summary = data.summary; - - updateProgress(data, state.id); + batch(() => + { + state.content.value = { + ...state.content.value, + title: data.title, + pages: data.pagecount, + tags: data.tags, + summary: data.summary, + }; - if (data.toc) - state.content.chapters = LRR.buildArchiveChapters(data.toc, state.id, data.pagecount); + updateProgress(data, state.id); + if (data.toc) { + state.content.value = { + ...state.content.value, + chapters: LRR.buildArchiveChapters(data.toc, state.id, data.pagecount), + }; + } + }); // Check and display warnings for unsupported filetypes checkFiletypeSupport(data.extension); } @@ -346,7 +398,7 @@ export function loadContentData() { */ export function getArchiveForPage(globalPage) { if (state.id.startsWith("TANK_")) { - const arc = state.content.chapters.find(a => globalPage >= a.startPage && globalPage <= a.endPage); + const arc = state.content.value.chapters.find(a => globalPage >= a.startPage && globalPage <= a.endPage); if (arc) return { arcId: arc.id, localPage: globalPage - arc.startPage + 1 }; } @@ -356,21 +408,20 @@ export function getArchiveForPage(globalPage) { function loadImages() { const onLoad = (data) => { - state.pages = data; - state.maxPage = state.pages.length - 1; - $(".max-page").html(state.pages.length); - - // Choices in order for page picking: - // * p is in parameters and is not the first page - // * progress is tracked and is not the last page - // * first page - // This allows for bookmarks to trump progress - // when there's no parameter, null is coerced to 0 so it becomes -1 - state.currentPage = state.currentPage || ( - !state.ignoreProgress.value && state.progress < state.maxPage - ? state.progress - : 0 - ); + batch(() => { + state.pages.value = data; + // Choices in order for page picking: + // * p is in parameters and is not the first page + // * progress is tracked and is not the last page + // * first page + // This allows for bookmarks to trump progress + // when there's no parameter, null is coerced to 0 so it becomes -1 + state.setCurrentPage(state.currentPage.peek() || ( + !state.ignoreProgress.value && state.progress < state.maxPage.peek() + ? state.progress + : 0 + )); + }); if (state.showOverlayByDefault.value) { toggleArchiveOverlay(); } @@ -382,7 +433,7 @@ function loadImages() { }; const onFinally = () => { - if (state.pages === undefined) { + if (state.pages.value === undefined) { $("#img").attr("src", new LRR.ApiURL("/img/flubbed.gif").toString()); $("#display").append(`

${I18N.ReaderArchiveError}

`); } @@ -392,7 +443,7 @@ function loadImages() { if (state.id.startsWith("TANK_")) { // For tanks: fetch pages for each archive and concatenate them return Promise.all( - state.content.chapters.map(arc => + state.content.value.chapters.map(arc => fetch(new LRR.ApiURL(`/api/archives/${arc.id}/files?force=${state.force}`)) .then(r => r.ok ? r.json() : Promise.reject()) ) @@ -411,16 +462,16 @@ function loadImages() { function initFullscreen() { // Apply full-screen utility // F11 Fullscreen is totally another "Fullscreen", so its support is beyond consideration. - // Small override function, always returns boolean - fscreen.inFullscreen = () => !!fscreen.fullscreenElement; - if (!fscreen.fullscreenEnabled) { - // Fullscreen mode is unsupported; use attribute selector to hide all instances - $("[id='toggle-full-screen']").hide(); - } - fscreen.onfullscreenchange = () => handleFullScreen(fscreen.fullscreenElement !== null); } +/** + * Small override function, always returns boolean + */ +export function inFullscreen() { + return !!fscreen.fullscreenElement; +} + function enterInfiniteScrollView() { if (infiniteScrollObserver !== null) { // We're already in infiniteScrollView @@ -433,10 +484,10 @@ function enterInfiniteScrollView() { // Wait for the pages to load before scrolling to the current page loaded += 1; - if (loaded === state.pages.length) { + if (loaded === state.pages.value.length) { allImagesLoaded = true; if (window.scrollY === 0) { - goToPage(state.currentPage); + goToPage(state.currentPage.value); } } } @@ -448,7 +499,7 @@ function enterInfiniteScrollView() { $img.on("load.infinite-scroll", imgOnLoad); $("body").addClass("infinite-scroll"); - $img.attr("src", state.pages[0]).addClass("infinite-scroll-image"); + $img.attr("src", state.pages.value[0]).addClass("infinite-scroll-image"); // Disable other options that don't work with infinite scroll state.mangaMode.value = false; @@ -463,16 +514,16 @@ function enterInfiniteScrollView() { // Convert to int const page = parseInt(index, 10); // Avoid double progress updates - if (state.currentPage !== page) { - state.currentPage = page; + if (state.currentPage.value !== page) { + state.setCurrentPage(page); updateProgress(); } } }, { threshold: 0.5 }); - state.pages.slice(1).forEach((source) => { + state.pages.value.slice(1).forEach((source) => { const img = new Image(); - img.id = `page-${state.pages.indexOf(source)}`; + img.id = `page-${state.pages.value.indexOf(source)}`; img.height = 800; img.width = 600; const $img = $(img); @@ -485,7 +536,7 @@ function enterInfiniteScrollView() { infiniteScrollObserver.observe(img); }); - $("#i3").removeClass("loading"); + state.showLoader.value = false; $(document).on("click.infinite-scroll-map", "#display .reader-image", (event) => { // is click X position is left on screen or right if (event.pageX < $(window).width() / 2) { @@ -497,7 +548,7 @@ function enterInfiniteScrollView() { applyContainerWidth(); - if (state.content.tags?.includes("webtoon")) { + if (state.content.value.tags?.includes("webtoon")) { $("body").addClass("webtoon-mode"); } } @@ -516,7 +567,7 @@ function enterStandardView() { const images = $("#display .reader-image"); images.off("load.infinite-scroll"); - $("#i3").removeClass("loading"); + state.showLoader.value = false; applyContainerWidth(); // when click left or right img area change page @@ -533,9 +584,6 @@ function enterStandardView() { }); $("#img").off("load.updatemeta").on("load.updatemeta", updateMetadata); - - $(".current-page").each((_i, el) => $(el).html(state.currentPage + 1)); - goToPage(state.currentPage); } /** Process inputs @@ -586,7 +634,7 @@ function handleShortcuts(e) { } break; case 66: // b - toggleBookmark(e); + toggleBookmark(); break; case 68: // d if (e.shiftKey) { @@ -641,7 +689,7 @@ function handleShortcuts(e) { */ function spaceScrollProcessInput(e) { //Break early and go back to browser default behaviour if overlay is open or gallery has webtoon tag and in infiniteScroll - if ($(".page-overlay").is(":visible") || e.repeat || (state.infiniteScroll.value && state.content.tags?.includes("webtoon"))) return; + if ($(".page-overlay").is(":visible") || e.repeat || (state.infiniteScroll.value && state.content.value.tags?.includes("webtoon"))) return; e.preventDefault(); // Capture direction now so we dont lose it if shift state changes while held @@ -731,7 +779,7 @@ function spaceScrollProcessInput(e) { } function handleWheel(e) { - if (fscreen.inFullscreen() && !state.infiniteScroll.value) { + if (inFullscreen() && !state.infiniteScroll.value) { let changePageNum = 1; if (e.originalEvent.deltaY > 0) changePageNum = -1; // In Manga mode, reverse the changePage variable @@ -771,7 +819,7 @@ function checkFiletypeSupport(extension) { } } -function toggleHelp() { +export function toggleHelp() { LRR.toast({ toastId: "readerHelp", heading: I18N.ReaderNavHelp, @@ -784,8 +832,7 @@ function toggleHelp() { // all toggable panes need to return false to avoid scrolling to top } -function toggleBookmark(e) { - e.preventDefault(); +export function toggleBookmark() { if (!localStorage.getItem("bookmarkCategoryId")) { console.error("No bookmark category ID found!"); return; @@ -800,20 +847,20 @@ function toggleBookmark(e) { return; } - if ($(".toggle-bookmark").hasClass("fas fa-bookmark")) { + if (state.isBookmarked.value) { // Remove from category - Server.removeArchiveFromCategory(state.id, localStorage.getItem("bookmarkCategoryId")); + Server.removeArchiveFromCategory(state.id, localStorage.getItem("bookmarkCategoryId")) + .then(() => { + state.isBookmarked.value = false; + }); removeCategoryBadge(localStorage.getItem("bookmarkCategoryId")); - $(".toggle-bookmark") - .removeClass("fas fa-bookmark") - .addClass("far fa-bookmark"); } else { // Add to category - Server.addArchiveToCategory(state.id, localStorage.getItem("bookmarkCategoryId")); + Server.addArchiveToCategory(state.id, localStorage.getItem("bookmarkCategoryId")) + .then(() => { + state.isBookmarked.value = true; + }); addCategoryBadge(localStorage.getItem("bookmarkCategoryId")); - $(".toggle-bookmark") - .removeClass("far fa-bookmark") - .addClass("fas fa-bookmark"); } } @@ -826,20 +873,7 @@ function loadBookmarkStatus() { } fetch(new LRR.ApiURL(`/api/categories/${category_id}`)) .then(response => response.json()).then(categoryData => { - const isBookmarked = categoryData.archives.includes(state.id); - const bookmarkState = isBookmarked ? "fas" : "far"; - const disabledClass = LRR.isUserLogged() ? "" : " disabled"; - const leftOptionsList = document.querySelectorAll(".absolute-options.absolute-left"); - leftOptionsList.forEach(leftOption => { - let bookmark = document.createElement("a"); - bookmark.className = `${bookmarkState} fa-bookmark fa-2x toggle-bookmark${disabledClass}`; - bookmark.href = "#"; - bookmark.title = I18N.ToggleBookmark; - if (!LRR.isUserLogged()) { - bookmark.setAttribute("style", "opacity: 0.5; cursor: not-allowed;"); - } - leftOption.appendChild(bookmark); - }); + state.isBookmarked.value = categoryData.archives.includes(state.id); }); } ); @@ -847,136 +881,193 @@ function loadBookmarkStatus() { function updateMetadata() { const img = $("#img")[0]; - const {filename} = img.dataset; - const imgDoublePage = $("#img_doublepage")[0]; - const filenameDoublePage = imgDoublePage.dataset.filename; - if (!filename && state.showingSinglePage) { - state.currentPageLoaded = true; - $("#i3").removeClass("loading"); + /*if (!state.filename.peek() && state.showingSinglePage.value) { + state.currentPageLoaded.value = true; return; - } + }*/ const width = img.naturalWidth; const height = img.naturalHeight; const widthDoublePage = imgDoublePage.naturalWidth; const heightDoublePage = imgDoublePage.naturalHeight; - const widthView = width + widthDoublePage; - - if (state.showingSinglePage) { - let size = state.preloadedSizes[state.currentPage]; - if (!size) { - size = LRR.getImgSize(state.pages[state.currentPage]); - state.preloadedSizes[state.currentPage] = size; - $(".file-info").text(`${filename} :: ${width} x ${height} :: ${size} KB`); - $(".file-info").attr("title", `${filename} :: ${width} x ${height} :: ${size} KB`); - } else { - $(".file-info").text(`${filename} :: ${width} x ${height} :: ${size} KB`); - $(".file-info").attr("title", `${filename} :: ${width} x ${height} :: ${size} KB`); - } - } else { - let size = state.preloadedSizes[state.currentPage]; - let sizePre = state.preloadedSizes[state.currentPage + 1]; - - if (!size || !sizePre) { - size = LRR.getImgSize(state.pages[state.currentPage]); - sizePre = LRR.getImgSize(state.pages[state.currentPage + 1]); - state.preloadedSizes[state.currentPage] = size; - state.preloadedSizes[state.currentPage + 1] = sizePre; - } - const sizeView = size + sizePre; - $(".file-info").text(`${filename} - ${filenameDoublePage} :: ${widthView} x ${height} :: ${sizeView} KB`); - $(".file-info").attr("title", `${filename} :: ${width} x ${height} :: ${size} KB - ${filenameDoublePage} :: ${widthDoublePage} x ${heightDoublePage} :: ${sizePre} KB`); + let size = state.preloadedSizes[state.currentPage.value]; + if (!size) { + size = LRR.getImgSize(state.pages.value[state.currentPage.value]); + state.preloadedSizes[state.currentPage.value] = size; } + batch(() => { + state.width.value = width; + state.height.value = height; + state.size.value = size; + }); + + if (!state.showingSinglePage.value) { + let sizePre = state.preloadedSizes[state.currentPage.value + 1]; + + if (!sizePre) { + sizePre = LRR.getImgSize(state.pages.value[state.currentPage.value + 1]); + state.preloadedSizes[state.currentPage.value + 1] = sizePre; + } - // Update page numbers in the paginator - const newVal = state.showingSinglePage - ? state.currentPage + 1 - : `${state.currentPage + 1} + ${state.currentPage + 2}`; - $(".current-page").each((_i, el) => $(el).html(newVal)); + batch(() => { + state.width2.value = widthDoublePage; + state.height2.value = heightDoublePage; + state.size2.value = sizePre; + }); + } - state.currentPageLoaded = true; - $("#i3").removeClass("loading"); + state.currentPageLoaded.value = true; } -export async function goToPage(page) { - if (state.maxPage < 0) { +export async function refreshCurrentPage() { + // Since we may be letting multiple page-loads be active (due to quick navigation), + // we need to ensure we don't end up with stale data. + refreshGeneration = refreshGeneration + 1; + const ourGeneration = refreshGeneration; + + if (state.infiniteScroll.value) { + return; + } + if (state.maxPage.value < 0) { // Not yet loaded, NOOP return; } - state.previousPage = state.currentPage; - state.currentPage = Math.min(state.maxPage, Math.max(0, +page)); + let currentPage = state.currentPage.value; - if (state.infiniteScroll.value) { - $("#display img:not(#img_doublepage)").get(state.currentPage).scrollIntoView({ block: "nearest" }); - } else { - if (state.doublePageMode.value && state.currentPage > 0 - && state.currentPage < state.maxPage) { - - // Special case when going backwards and already showing a widespread, - // we need to go back by two pages to show the previous double-page spread - if (state.showingSinglePage && state.previousPage > state.currentPage) - state.currentPage = Math.max(0, state.currentPage - 1); - // Composite an image and use that as the source - const img1 = await loadImage(state.currentPage); - const img1Filename = getFilename(state.currentPage); - const img1Size = await getImageSize(img1); - const img2 = await loadImage(state.currentPage + 1); - const img2Filename = getFilename(state.currentPage + 1); - const img2Size = await getImageSize(img2); - // If w > h on one of the images(widespread), set canvasdata to the first(or second) image only - if (img1Size.width > img1Size.height || img2Size.width > img2Size.height) { - // Depending on whether we were going forward or backward, display img1 or img2 - const wideSrc = state.previousPage > state.currentPage ? img2 : img1; - const wideFilename = state.previousPage > state.currentPage ? img2Filename : img1Filename; - $("#img").attr("src", wideSrc); - $("#img").attr("data-filename", wideFilename); - $("#display").removeClass("double-mode"); - $("#img_doublepage").attr("src", ""); - $("#img_doublepage").attr("data-filename", ""); - state.showingSinglePage = true; - // Adjust currentPage to the page of the image being displayed (don't jump by 2 anymore) - state.currentPage = state.previousPage > state.currentPage ? state.currentPage + 1 : state.currentPage; - } else { - $("#display").addClass("double-mode"); - if (state.mangaMode.value) { - $("#img").attr("src", img2); - $("#img").attr("data-filename", img2Filename); - $("#img_doublepage").attr("src", img1); - $("#img_doublepage").attr("data-filename", img1Filename); - } else { - $("#img").attr("src", img1); - $("#img").attr("data-filename", img1Filename); - $("#img_doublepage").attr("src", img2); - $("#img_doublepage").attr("data-filename", img2Filename); + if (state.doublePageMode.value && currentPage > 0 + && currentPage < state.maxPage.value) { + + // Special case when going backwards and already showing a widespread, + // we need to go back by two pages to show the previous double-page spread + if (state.showingSinglePage.value && state.previousPage > currentPage) { + currentPage = Math.max(0, state.currentPage - 1); + } + + // Composite an image and use that as the source + const [img1, img2] = await Promise.all([ + loadImage(currentPage), + loadImage(currentPage + 1), + ]); + + if (ourGeneration !== refreshGeneration) { + return; + } + const [img1Size, img2Size] = await Promise.all([ + getImageSize(img1), + getImageSize(img2), + ]); + const img1Filename = getFilename(currentPage); + const img2Filename = getFilename(currentPage + 1); + // If w > h on one of the images(widespread), set canvasdata to the first(or second) image only + if (img1Size.width > img1Size.height || img2Size.width > img2Size.height) { + // Depending on whether we were going forward or backward, display img1 or img2 + const wideSrc = state.previousPage > currentPage ? img2 : img1; + const wideFilename = state.previousPage > currentPage ? img2Filename : img1Filename; + $("#img") + .attr("src", wideSrc); + $("#img_doublepage") + .attr("src", ""); + // Adjust currentPage to the page of the image being displayed (don't jump by 2 anymore) + currentPage = state.previousPage > currentPage ? currentPage + 1 : currentPage; + + batch(() => { + state.filename.value = wideFilename; + state.filenameDoublePage.value = ""; + state.showingSinglePage.value = true; + + if (currentPage !== state.currentPage.peek()) { + state.currentPage.value = currentPage; } - state.showingSinglePage = false; - } + }); } else { - const img = await loadImage(state.currentPage); - const imgFilename = getFilename(state.currentPage); - $("#img").attr("src", img); - $("#img").attr("data-filename", imgFilename); - $("#display").removeClass("double-mode"); - $("#img_doublepage").attr("src", ""); - $("#img_doublepage").attr("data-filename", ""); - state.showingSinglePage = true; + if (state.mangaMode.value) { + $("#img") + .attr("src", img2); + $("#img_doublepage") + .attr("src", img1); + batch(() => { + state.filename.value = img2Filename; + state.filenameDoublePage.value = img1Filename; + state.showingSinglePage.value = false; + if (currentPage !== state.currentPage.peek()) { + state.currentPage.value = currentPage; + } + }); + } else { + $("#img") + .attr("src", img1); + $("#img_doublepage") + .attr("src", img2); + batch(() => { + state.filename.value = img1Filename; + state.filenameDoublePage.value = img2Filename; + state.showingSinglePage.value = false; + if (currentPage !== state.currentPage.peek()) { + state.currentPage.value = currentPage; + } + }); + } } + } else { + const img = await loadImage(state.currentPage.value); + if (ourGeneration !== refreshGeneration) { + return; + } + const imgFilename = getFilename(state.currentPage.value); + $("#img") + .attr("src", img); + $("#img_doublepage") + .attr("src", ""); + batch(() => { + state.filename.value = imgFilename; + state.filenameDoublePage.value = ""; + state.showingSinglePage.value = true; + if (currentPage !== state.currentPage.peek()) { + state.currentPage.value = currentPage; + } + }); + } +} + +export async function goToPage(page) { + if (state.maxPage.value < 0) { + // Not yet loaded, NOOP + return; + } + const requestedPage = Math.min(state.maxPage.value, Math.max(0, +page)); + if (requestedPage === state.currentPage.peek()) { + // No change, do nothing + return; + } + + state.previousPage = state.currentPage.value; + state.setCurrentPage(requestedPage); + + if (state.infiniteScroll.value) { + $("#display img").get(state.currentPage.value).scrollIntoView({ block: "nearest" }); + state.showingSinglePage.value = false; + } else { preloadImages(); applyContainerWidth(); - state.currentPageLoaded = false; + state.currentPageLoaded.value = false; // display overlay if it takes too long to load a page - setTimeout(() => { - if (!state.currentPageLoaded) { $("#i3").addClass("loading"); } - }, 500); - + if (loadingDebounceTimer === null) { + loadingDebounceTimer = setTimeout(() => { + if (!state.currentPageLoaded.peek()) { + state.showLoader.value = true; + } + loadingDebounceTimer = null; + }, 500); + } + // update full image link - $("#imgLink").attr("href", state.pages[state.currentPage]); + $("#imgLink").attr("href", state.pages.value[state.currentPage.value]); // scroll to top window.scrollTo(0, 0); @@ -988,7 +1079,7 @@ export async function goToPage(page) { function updateProgress() { // Clear markers - let page = state.currentPage + 1; // progress is 1-indexed + let page = state.currentPage.value + 1; // progress is 1-indexed // Send an API request to update progress on the server if (state.authenticateProgress && LRR.isUserLogged()) { @@ -1009,22 +1100,42 @@ function preloadImages() { if (state.doublePageMode.value) { preloadNext *= 2; preloadPrev *= 2; } for (let i = 1; i <= preloadNext; i++) { - if (state.currentPage + i > state.maxPage) { break; } - loadImage(state.currentPage + i); + if (state.currentPage.value + i > state.maxPage.value) { break; } + loadImage(state.currentPage.value + i); } for (let i = 1; i <= preloadPrev; i++) { - if (state.currentPage - i < 0) { break; } - loadImage(state.currentPage - i); + if (state.currentPage.value - i < 0) { break; } + loadImage(state.currentPage.value - i); } } +const fetchPromiseMap = new Map(); + +/** + * Utility function for preventing multiple simultaneous fetches to a single resource + * @param key + * @param fetcher + * @returns {any} + */ +function fetchOnce(key, fetcher) { + if (!fetchPromiseMap.has(key)) { + const promise = fetcher().finally(() => fetchPromiseMap.delete(key)); + fetchPromiseMap.set(key, promise); + } + return fetchPromiseMap.get(key); +} + async function loadImage(index) { - const src = state.pages[index]; + const src = state.pages.value[index]; if (!state.preloadedImg[src]) { - const res = await fetch(src); - state.preloadedSizes[index] = parseInt(res.headers.get("Content-Length") / 1024, 10); - const blob = await res.blob(); + const { size, blob } = await fetchOnce(src, async () => { + const res = await fetch(src); + const size = parseInt(res.headers.get("Content-Length") / 1024, 10); + const blob = await res.blob(); + return { size, blob }; + }); + state.preloadedSizes[index] = size; state.preloadedImg[src] = URL.createObjectURL(blob); } @@ -1035,7 +1146,7 @@ export function applyContainerWidth() { $(".reader-image, .sni").attr("style", ""); // If we are in fullscreen don't apply anything - if (fscreen.inFullscreen()) + if (inFullscreen()) return; if (state.fitMode.value === "fit-height") { @@ -1052,7 +1163,7 @@ export function applyContainerWidth() { // If the user defined a custom width, then we can fall back to that one $(".sni").attr("style", `max-width: ${state.containerWidth.value}`); $(".reader-image").attr("style", "width: 100%"); - } else if (!state.showingSinglePage) { + } else if (!state.showingSinglePage.value) { // Otherwise, if we are showing two pages we can override the default width $(".sni").attr("style", "max-width: 90%"); } else { @@ -1063,15 +1174,15 @@ export function applyContainerWidth() { renderMarkers(); } -function toggleMangaMode() { +export function toggleMangaMode() { if (state.infiniteScroll.value) { return false; } state.mangaMode.value = !state.mangaMode.value; return false; } function startAutoNextPage() { - state.autoNextPageCountdown = Math.trunc(state.AutoNextPageInterval.value); - if (state.autoNextPageCountdown <= 0) { + state.autoNextPageCountdown.value = Math.trunc(state.AutoNextPageInterval.value); + if (state.autoNextPageCountdown.value <= 0) { LRR.toast({ heading: I18N.AutoNextPageFailHeader, text: I18N.AutoNextPageFailBody, @@ -1081,17 +1192,13 @@ function startAutoNextPage() { return; } - state.autoNextPage = true; - - const aEls = $(".toggle-auto-next-page"); - aEls.removeClass("fa-stopwatch"); - aEls.text(state.autoNextPageCountdown); + state.autoNextPage.value = true; state.autoNextPageCountdownTaskId = setInterval(() => { - if (state.autoNextPageCountdown <= 0) { + if (state.autoNextPageCountdown.value <= 0) { clearInterval(state.autoNextPageCountdownTaskId); - const atLastPage = state.mangaMode.value ? state.currentPage === 0 : state.currentPage === state.maxPage; + const atLastPage = state.mangaMode.value ? state.currentPage.value === 0 : state.currentPage.value === state.maxPage.value; if (atLastPage) { // At archive boundary: attempt cross-archive navigation. @@ -1113,29 +1220,27 @@ function startAutoNextPage() { } return; } - state.autoNextPageCountdown -= 1; - aEls.text(state.autoNextPageCountdown); + state.autoNextPageCountdown.value -= 1; }, 1000); requestWakeLock(); } export function stopAutoNextPage() { - state.autoNextPage = false; + state.autoNextPage.value = false; clearInterval(state.autoNextPageCountdownTaskId); - $(".toggle-auto-next-page").addClass("fa-stopwatch"); - $(".toggle-auto-next-page").text(""); + state.autoNextPageCountdown.value = 0; releaseWakeLock(); } -function toggleAutoNextPage() { - state.autoNextPage ? stopAutoNextPage() : startAutoNextPage(); +export function toggleAutoNextPage() { + state.autoNextPage.value ? stopAutoNextPage() : startAutoNextPage(); return false; // prevent scrolling to top } -function toggleFullScreen() { - if (fscreen.inFullscreen()) { +export function toggleFullScreen() { + if (inFullscreen()) { // if already full screen; exit fscreen.exitFullscreen(); } else { @@ -1146,7 +1251,7 @@ function toggleFullScreen() { } function handleFullScreen(enableFullscreen = false) { - if (fscreen.inFullscreen() || enableFullscreen === true) { + if (inFullscreen() || enableFullscreen === true) { if (state.markersVisible.value) { clearMarkers(); } @@ -1167,7 +1272,7 @@ function handleFullScreen(enableFullscreen = false) { } export function getCurrentChapter() { - return findChapterForPage(state.currentPage + 1, state.content.chapters); + return findChapterForPage(state.currentPage.value + 1, state.content.value.chapters); } // Find the current chapter (or nested sub-chapter) for the given page. @@ -1198,7 +1303,7 @@ function generateThumbnails() { if (Object.hasOwn(notes, i) && notes[i] === "processed") { const startPage = state.id.startsWith("TANK_") ? - state.content.chapters.find(ch => ch.id === notes.id).startPage : + state.content.value.chapters.find(ch => ch.id === notes.id).startPage : 1; const index = startPage + i - 2; // 0-based global @@ -1243,27 +1348,26 @@ function generateThumbnails() { }; if (state.id.startsWith("TANK_")) - state.content.chapters.forEach(arc => fetchThumbsForArc(arc)); // Generate thumbnails per archive + state.content.value.chapters.forEach(arc => fetchThumbsForArc(arc)); // Generate thumbnails per archive else fetchThumbsForArc({ id: state.id, startPage: 1, - endPage: state.content.pages, + endPage: state.content.value.pages, }); // Queue a single minion job for thumbnails } /** * Change current page in reader. * - * @param {(-1|1|"first"|"last")} targetPage One of -1 (previous), 1 (next), "first", or "last" page. + * @param {(-1|1|"first"|"last"|"outermost-left"|"outermost-right")} targetPage One of -1 (previous), 1 (next), "first", or "last" page. * @param {boolean} resetAuto Whether to reset current slideshow counter. */ -function changePage(targetPage, resetAuto = false) { +export function changePage(targetPage, resetAuto = false) { // Reset timer if user manually changes pages during slideshow - if (resetAuto && state.autoNextPage) { - state.autoNextPageCountdown = Math.trunc(state.AutoNextPageInterval.value); - $(".toggle-auto-next-page").text(state.autoNextPageCountdown); + if (resetAuto && state.autoNextPage.value) { + state.autoNextPageCountdown.value = Math.trunc(state.AutoNextPageInterval.value); } // Sync position if in infinite scroll mode @@ -1273,23 +1377,34 @@ function changePage(targetPage, resetAuto = false) { for (let i = 0; i < images.length; i++) { const rect = images[i].getBoundingClientRect(); if (rect.top <= midViewport && rect.bottom >= midViewport) { - state.currentPage = i; + state.setCurrentPage(i); break; } } } + + if (targetPage === "outermost-left") { + return state.mangaMode.value ? readNextArchive() : readPreviousArchive(); + } else if (targetPage === "outermost-right") { + return state.mangaMode.value ? readPreviousArchive() : readNextArchive(); + } + let destination; if (targetPage === "first") { - destination = state.mangaMode.value ? state.maxPage : 0; + destination = state.mangaMode.value ? state.maxPage.value : 0; } else if (targetPage === "last") { - destination = state.mangaMode.value ? 0 : state.maxPage; + destination = state.mangaMode.value ? 0 : state.maxPage.value; } else { let offset = targetPage; // Double the offset to move by 2 pages at once, unless we're currently showing a widespread - if (state.doublePageMode.value && !state.showingSinglePage && state.currentPage > 0) { - offset *= 2; + if (state.doublePageMode.value && !state.showingSinglePage.value && state.currentPage.value > 0) { + // Avoid skipping page 2. First page is always displayed solo, so double-jumping back to + // the first page would skip page 2. + if (offset !== -1 || state.currentPage.value > 2) { + offset *= 2; + } } - destination = state.currentPage + (state.mangaMode.value ? -offset : offset); + destination = state.currentPage.peek() + (state.mangaMode.value ? -offset : offset); } if (destination < 0) { // Clamp if we're not at the first page, to avoid doublepage mode accidentally yeeting us to previous archive @@ -1309,33 +1424,8 @@ function changePage(targetPage, resetAuto = false) { return goToPage(destination); } -function handlePaginator() { - switch (this.getAttribute("value")) { - case "outermost-left": - readPreviousArchive(); - break; - case "outer-left": - changePage("first", true); - break; - case "left": - changePage(-1, true); - break; - case "right": - changePage(1, true); - break; - case "outer-right": - changePage("last", true); - break; - case "outermost-right": - readNextArchive(); - break; - default: - break; - } -} - function getFilename(index) { - return new URLSearchParams(state.pages[index].split("?")[1]).get("path"); + return new URLSearchParams(state.pages.value[index].split("?")[1]).get("path"); } function getImageSize(url) { @@ -1379,7 +1469,7 @@ async function setupArchiveNavigation() { state.archiveIds = ids; state.archiveIndex = ids.indexOf(state.id); if (state.archiveIndex !== -1) { - $(".archive-nav-link").show(); + state.multiArchiveNavigation.value = true; if (state.archiveIndex === 0) { const previousArchives = await loadPreviousDatatablesArchives(); if (previousArchives) { @@ -1419,7 +1509,7 @@ async function loadNextDatatablesArchives() { } function readPreviousArchive() { - if (fscreen.inFullscreen()) { + if (inFullscreen()) { console.warn("[previous] Archive navigation not supported in fullscreen mode."); return; } @@ -1443,7 +1533,7 @@ function readPreviousArchive() { } else { previousArchiveId = state.archiveIds[state.archiveIndex - 1]; } - if (state.autoNextPage) { + if (state.autoNextPage.value) { sessionStorage.setItem("autoNextPage", "true"); } const newUrl = new LRR.ApiURL(`/reader?id=${previousArchiveId}`).toString(); @@ -1454,7 +1544,7 @@ function readPreviousArchive() { } function readNextArchive() { - if (fscreen.inFullscreen()) { + if (inFullscreen()) { console.warn("[next] Archive navigation not supported in fullscreen mode."); return; } @@ -1478,7 +1568,7 @@ function readNextArchive() { } else { nextArchiveId = state.archiveIds[state.archiveIndex + 1]; } - if (state.autoNextPage) { + if (state.autoNextPage.value) { sessionStorage.setItem("autoNextPage", "true"); } const newUrl = new LRR.ApiURL(`/reader?id=${nextArchiveId}`).toString(); diff --git a/public/js/mod/reader_components.js b/public/js/mod/reader_components.js new file mode 100644 index 000000000..33ce74e0e --- /dev/null +++ b/public/js/mod/reader_components.js @@ -0,0 +1,60 @@ +import { h } from "preact"; +import htm from "htm"; +import fscreen from "fscreen"; +import { computed } from "@preact/signals"; + +import { state, toggleBookmark, toggleHelp, toggleMangaMode, toggleAutoNextPage, toggleFullScreen } from "./reader_common.js"; +import { toggleSettingsOverlay } from "./reader_options.js"; +import { toggleArchiveOverlay } from "./reader_archive_overlay.js"; + +import I18N from "i18n"; + +const html = htm.bind(h); + +export function ToggleButton({ id, active, onClick, label }) { + return html``; +} + +export function BookmarkButton() { + return html` + {e.preventDefault();toggleBookmark();}}> + `; +} + +export function FileInfo() +{ + const title = computed(() => { + if (state.showingSinglePage.value) { + return `${state.filename.value} :: ${state.width.value} x ${state.height.value} :: ${state.size.value} KB`; + } else { + return `${state.filename.value} - ${state.filenameDoublePage.value} :: ${state.width.value + state.width2.value} x ${state.height.value} :: ${state.size.value + state.size2.value} KB`; + } + }); + + return html` +
${title}
+ `; +} + +export function LeftToolbar() { + return html` + + `; +} +export function RightToolbar() { + return html` + + `; +} \ No newline at end of file diff --git a/public/js/mod/reader_footer.js b/public/js/mod/reader_footer.js new file mode 100644 index 000000000..26bc723de --- /dev/null +++ b/public/js/mod/reader_footer.js @@ -0,0 +1,24 @@ +import { h, render } from "preact"; +import htm from "htm"; + +import { FileInfo, LeftToolbar, RightToolbar } from "./reader_components.js"; +import { ReaderPaginator } from "./reader_paginator.js"; + +const html = htm.bind(h); + +export function ReaderFooter() { + return html` +
+ <${FileInfo} /> + + <${LeftToolbar} /> + <${RightToolbar} /> + <${ReaderPaginator} /> +
+ `; +} + +export function initializeFooter() { + render( + html`<${ReaderFooter} />`, document.getElementById("reader-footer")); +} diff --git a/public/js/mod/reader_header.js b/public/js/mod/reader_header.js new file mode 100644 index 000000000..9baa1f0f1 --- /dev/null +++ b/public/js/mod/reader_header.js @@ -0,0 +1,45 @@ +import { h, render } from "preact"; +import htm from "htm"; + +import { state } from "./reader_common.js"; +import { computed } from "@preact/signals"; +import { FileInfo, LeftToolbar, RightToolbar } from "./reader_components.js"; +import { ReaderPaginator } from "./reader_paginator.js"; +import { ApiURL } from "./common.js"; + +const html = htm.bind(h); + +export function ReaderHeader() { + const hideHeader = computed(() => { + return state.hideHeader.value || state.infiniteScroll.value; + }); + + const defaultTitle = computed(() => { + return state.content.value?.title || "..."; + }); + + const artistUrl = computed(() => { + return new ApiURL(`/?sort=0&q=artist%3A${encodeURIComponent(state.artist.value)}%24&`); + }); + + return html` +
+

+ ${!state.artist.value + ? html`${defaultTitle.value}` + : html`${defaultTitle.value} by ${state.artist.value}` + } +

+ + <${LeftToolbar} /> + <${RightToolbar} /> + <${ReaderPaginator} /> + <${FileInfo} /> +
+ `; +} + +export function initializeHeader() { + render( + html`<${ReaderHeader} />`, document.getElementById("header")); +} diff --git a/public/js/mod/reader_options.js b/public/js/mod/reader_options.js index 3f4730de5..f26fb9246 100644 --- a/public/js/mod/reader_options.js +++ b/public/js/mod/reader_options.js @@ -9,15 +9,11 @@ import { Show } from "@preact/signals/utils"; import htm from "htm"; import { state, stopAutoNextPage, toggleOverlay } from "./reader_common.js"; +import { ToggleButton } from "./reader_components.js"; import I18N from "i18n"; const html = htm.bind(h); -function ToggleButton({ id, active, onClick, label }) { - return html``; -} - function SettingsPanel() { const [autoNextPageInterval, setAutoNextPageInterval] = useState(state.AutoNextPageInterval.value); const [containerWidth, setContainerWidth] = useState(state.containerWidth.value); @@ -147,8 +143,6 @@ function SettingsPanel() { export function initializeSettings() { render( html`<${SettingsPanel} />`, document.getElementById("settingsOverlay")); - - $(document).on("click.toggle-settings-overlay", "#toggle-settings-overlay", toggleSettingsOverlay); } export function toggleSettingsOverlay() { diff --git a/public/js/mod/reader_paginator.js b/public/js/mod/reader_paginator.js new file mode 100644 index 000000000..7a659afd7 --- /dev/null +++ b/public/js/mod/reader_paginator.js @@ -0,0 +1,47 @@ +import { h } from "preact"; +import htm from "htm"; + +import { state, changePage } from "./reader_common.js"; +import { computed } from "@preact/signals"; + +const html = htm.bind(h); + +export function ReaderPaginator() { + const currentPage = computed(() => { + if (state.currentPage.value === -1) { + return "..."; + } + return state.showingSinglePage.value ? state.currentPage.value + 1 : `${state.currentPage.value + 1} + ${state.currentPage.value + 2}`; + }); + + const maxPage = computed(() => { + if (state.maxPage.value === -1) { + return "..."; + } + return state.maxPage.value + 1; + }); + + function multiNavStyle() { + if (!state.multiArchiveNavigation.value) { + return "display:none;"; + } + return ""; + } + + return html` + + `; +} diff --git a/public/js/mod/reader_stamps.js b/public/js/mod/reader_stamps.js index 14d0cd8ff..d0cd64306 100644 --- a/public/js/mod/reader_stamps.js +++ b/public/js/mod/reader_stamps.js @@ -4,10 +4,8 @@ import * as Server from "./server.js"; import * as LRR from "./common.js"; import I18N from "i18n"; -import fscreen from "fscreen"; - -import { state, getArchiveForPage } from "./reader_common.js"; +import { state, getArchiveForPage, inFullscreen } from "./reader_common.js"; import { checkStampedPages } from "./reader_archive_overlay.js"; import { effect } from "@preact/signals"; @@ -49,10 +47,10 @@ export function initializeStamps() { left: true, }; - let page = state.currentPage + 1; + let page = state.currentPage.value + 1; - if (state.doublePageMode.value && state.currentPage > 0 - && state.currentPage < state.maxPage) { + if (state.doublePageMode.value && state.currentPage.value > 0 + && state.currentPage.value < state.maxPage.value) { if (img.id == "img_doublepage") { page += 1; markerData.left = false; @@ -242,7 +240,7 @@ function createMarkerElement(markerData, index) { } export function renderMarkers() { - if (state.infiniteScroll.value || fscreen.inFullscreen()) return; + if (state.infiniteScroll.value || inFullscreen()) return; // Clean markers const existing = document.querySelectorAll(".marker"); existing.forEach(el => el.remove()); @@ -280,7 +278,7 @@ function loadStamps(currentPage) { } if (state.doublePageMode.value && currentPage > 0 - && currentPage < state.maxPage) { + && currentPage < state.maxPage.value) { const { arcId: id2, localPage: p2 } = getArchiveForPage(currentPage + 1); // Call for the second page (may be in a different archive for tanks) diff --git a/templates/i18n.html.tt2 b/templates/i18n.html.tt2 index ef76598a9..cc78438f5 100644 --- a/templates/i18n.html.tt2 +++ b/templates/i18n.html.tt2 @@ -286,4 +286,6 @@ I18N.TheDefaultIs10Seconds = "[% c.lh('The default is 10 seconds.') %]"; I18N.ToggleStamps = "[% c.lh('Toggle Stamps') %]"; +I18N.ToggleBookmark = "[% c.lh('Toggle Bookmark') %]"; + export default I18N; \ No newline at end of file diff --git a/templates/reader.html.tt2 b/templates/reader.html.tt2 index 81b81b72a..75a0d2ec4 100644 --- a/templates/reader.html.tt2 +++ b/templates/reader.html.tt2 @@ -39,11 +39,7 @@
-
-

...

- - [% INCLUDE pagesel %] [% INCLUDE arrows %] [% INCLUDE fileinfo %] -
+
@@ -60,9 +56,7 @@
-
- [% INCLUDE fileinfo %] [% INCLUDE pagesel %] [% INCLUDE arrows %] -
+
@@ -196,45 +190,3 @@ - - - -[% #compute those blocks before getting to the main html %] - - -[% BLOCK arrows %] -
- - - - -
- ... / - ... -
- - - - -
-[% END %] - -[% BLOCK pagesel %] -
- - -
- -
- - - - -
-[% END %] - -[% BLOCK fileinfo %] -
- -
-[% END %] \ No newline at end of file