您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Universal comic reader
当前为
此脚本不应直接安装,它是供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/417893/1277482/vim%20comic%20viewer.js
// ==UserScript== // @name vim comic viewer // @name:ko vim comic viewer // @description Universal comic reader // @description:ko 만화 뷰어 라이브러리 // @version 12.1.0 // @namespace https://gf.qytechs.cn/en/users/713014-nanikit // @exclude * // @match http://unused-field.space/ // @author nanikit // @license MIT // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @resource link:@headlessui/react https://cdn.jsdelivr.net/npm/@headlessui/[email protected]/dist/headlessui.prod.cjs // @resource link:@stitches/react https://cdn.jsdelivr.net/npm/@stitches/[email protected]/dist/index.cjs // @resource link:clsx https://cdn.jsdelivr.net/npm/[email protected]/dist/clsx.js // @resource link:fflate https://cdn.jsdelivr.net/npm/[email protected]/lib/browser.cjs // @resource link:jotai https://cdn.jsdelivr.net/npm/[email protected]/index.js // @resource link:jotai/react https://cdn.jsdelivr.net/npm/[email protected]/react.js // @resource link:jotai/react/utils https://cdn.jsdelivr.net/npm/[email protected]/react/utils.js // @resource link:jotai/utils https://cdn.jsdelivr.net/npm/[email protected]/utils.js // @resource link:jotai/vanilla https://cdn.jsdelivr.net/npm/[email protected]/vanilla.js // @resource link:jotai/vanilla/utils https://cdn.jsdelivr.net/npm/[email protected]/vanilla/utils.js // @resource link:react https://cdn.jsdelivr.net/npm/[email protected]/cjs/react.production.min.js // @resource link:react-dom https://cdn.jsdelivr.net/npm/[email protected]/cjs/react-dom.production.min.js // @resource link:react-toastify https://cdn.jsdelivr.net/npm/[email protected]/dist/react-toastify.js // @resource link:scheduler https://cdn.jsdelivr.net/npm/[email protected]/cjs/scheduler.production.min.js // @resource link:vcv-inject-node-env data:,unsafeWindow.process=%7Benv:%7BNODE_ENV:%22production%22%7D%7D // @resource react-toastify-css https://cdn.jsdelivr.net/npm/[email protected]/dist/ReactToastify.css // ==/UserScript== "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var mod_exports = {}; __export(mod_exports, { Viewer: () => Viewer, download: () => download, initialize: () => initialize, types: () => types_exports, utils: () => utils_exports }); module.exports = __toCommonJS(mod_exports); var React = __toESM(require("react")); var import_vcv_inject_node_env = require("vcv-inject-node-env"); var deps_exports = {}; __export(deps_exports, { Dialog: () => import_react2.Dialog, Fragment: () => import_react3.Fragment, Provider: () => import_jotai.Provider, RESET: () => import_utils2.RESET, Tab: () => import_react2.Tab, ToastContainer: () => import_react_toastify.ToastContainer, atom: () => import_jotai.atom, atomWithStorage: () => import_utils2.atomWithStorage, createContext: () => import_react3.createContext, createJSONStorage: () => import_utils2.createJSONStorage, createRef: () => import_react3.createRef, createRoot: () => import_react_dom.createRoot, createStitches: () => import_react.createStitches, createStore: () => import_jotai.createStore, deferred: () => deferred, forwardRef: () => import_react3.forwardRef, selectAtom: () => import_utils2.selectAtom, toast: () => import_react_toastify.toast, useAtom: () => import_jotai.useAtom, useAtomValue: () => import_jotai.useAtomValue, useCallback: () => import_react3.useCallback, useEffect: () => import_react3.useEffect, useId: () => import_react3.useId, useImperativeHandle: () => import_react3.useImperativeHandle, useLayoutEffect: () => import_react3.useLayoutEffect, useMemo: () => import_react3.useMemo, useReducer: () => import_react3.useReducer, useRef: () => import_react3.useRef, useSetAtom: () => import_jotai.useSetAtom, useState: () => import_react3.useState, useStore: () => import_jotai.useStore }); var import_react = require("@stitches/react"); __reExport(deps_exports, require("fflate")); function deferred() { let methods; let state = "pending"; const promise = new Promise((resolve, reject) => { methods = { async resolve(value) { await value; state = "fulfilled"; resolve(value); }, reject(reason) { state = "rejected"; reject(reason); } }; }); Object.defineProperty(promise, "state", { get: () => state }); return Object.assign(promise, methods); } var import_jotai = require("jotai"); var import_utils2 = require("jotai/utils"); var import_react_toastify = require("react-toastify"); var utils_exports = {}; __export(utils_exports, { getSafeFileName: () => getSafeFileName, insertCss: () => insertCss, isTyping: () => isTyping, save: () => save, saveAs: () => saveAs, timeout: () => timeout, waitDomContent: () => waitDomContent }); var timeout = (millisecond) => new Promise((resolve) => setTimeout(resolve, millisecond)); var waitDomContent = (document2) => document2.readyState === "loading" ? new Promise((r) => document2.addEventListener("readystatechange", r, { once: true })) : true; var insertCss = (css2) => { const style = document.createElement("style"); style.innerHTML = css2; document.head.append(style); }; var isTyping = (event) => event.target?.tagName?.match?.(/INPUT|TEXTAREA/) || event.target?.isContentEditable; var saveAs = async (blob, name) => { const a = document.createElement("a"); a.download = name; a.rel = "noopener"; a.href = URL.createObjectURL(blob); a.click(); await timeout(4e4); URL.revokeObjectURL(a.href); }; var getSafeFileName = (str) => { return str.replace(/[<>:"/\\|?*\x00-\x1f]+/gi, "").trim() || "download"; }; var save = (blob) => { return saveAs(blob, `${getSafeFileName(document.title)}.zip`); }; insertCss(GM_getResourceText("react-toastify-css")); var import_react2 = require("@headlessui/react"); var import_react3 = require("react"); var import_react_dom = require("react-dom"); function imageSourceToIterable(source) { if (typeof source === "string") { return async function* () { yield source; }(); } else if (Array.isArray(source)) { return async function* () { for (const url of source) { yield url; } }(); } else { return source(); } } var globalCss = document.createElement("style"); globalCss.innerHTML = `html, body { overflow: hidden; }`; function hideBodyScrollBar(doHide) { if (doHide) { document.head.append(globalCss); } else { globalCss.remove(); } } async function setFullscreenElement(element) { if (element) { await element.requestFullscreen?.(); } else { await document.exitFullscreen?.(); } } function focusWithoutScroll(element) { element.focus({ preventScroll: true }); } var emptyScroll = { page: null, ratio: 0, fullyVisiblePages: [] }; function getCurrentViewerScroll(container) { const children = [...container?.children ?? []]; if (!container || !children.length) { return emptyScroll; } return getCurrentScroll(children); } function getUrlImgs(urls) { const pages = []; const imgs = document.querySelectorAll("img[src]"); for (const img of imgs) { if (urls.includes(img.src)) { pages.push(img); } } return pages; } function getCurrentScroll(elements) { if (!elements.length) { return emptyScroll; } const pages = elements.map((page) => ({ page, rect: page.getBoundingClientRect() })); const fullyVisiblePages = pages.filter( ({ rect }) => rect.y >= 0 && rect.y + rect.height <= innerHeight ); if (fullyVisiblePages.length) { return { page: fullyVisiblePages[0].page, ratio: 0.5, fullyVisiblePages: fullyVisiblePages.map((x) => x.page) }; } const scrollCenter = innerHeight / 2; const centerCrossingPage = pages.find( ({ rect }) => rect.top <= scrollCenter && rect.bottom >= scrollCenter ); if (centerCrossingPage) { const centerCrossingRect = centerCrossingPage.rect; const ratio = 1 - (centerCrossingRect.bottom - scrollCenter) / centerCrossingRect.height; return { page: centerCrossingPage.page, ratio, fullyVisiblePages: [] }; } const firstPage = pages[0]; const lastPage = pages[pages.length - 1]; if (scrollCenter < pages[0].rect.top) { return { page: firstPage.page, ratio: 0, fullyVisiblePages: [] }; } return { page: lastPage.page, ratio: 1, fullyVisiblePages: [] }; } function isUserGesturePermissionError(error) { return error?.message === "Permissions check failed"; } var scrollElementStateAtom = (0, import_jotai.atom)(null); var scrollElementSizeAtom = (0, import_jotai.atom)({ width: 0, height: 0 }); var pageScrollStateAtom = (0, import_jotai.atom)(getCurrentViewerScroll()); var previousSizeAtom = (0, import_jotai.atom)({ width: 0, height: 0 }); var synchronizeScrollAtom = (0, import_jotai.atom)(null, (get, set) => { const scrollElement = get(scrollElementAtom); const current = getCurrentViewerScroll(scrollElement); if (!current.page) { return; } const height = scrollElement?.clientHeight ?? 0; const width = scrollElement?.clientWidth ?? 0; const previous = get(previousSizeAtom); const isResizing = height !== previous.height || width !== previous.width; if (isResizing) { set(restoreScrollAtom); set(previousSizeAtom, { width, height }); } else { set(pageScrollStateAtom, current); } }); var restoreScrollAtom = (0, import_jotai.atom)(null, (get) => { const { page, ratio } = get(pageScrollStateAtom); const scrollable = get(scrollElementAtom); if (!scrollable || !page) { return; } const { offsetTop, clientHeight } = page; const restoredY = Math.floor(offsetTop + clientHeight * ratio - scrollable.clientHeight / 2); scrollable.scroll({ top: restoredY }); }); var scrollElementAtom = (0, import_jotai.atom)( (get) => get(scrollElementStateAtom)?.div ?? null, (_get, set, div) => { set(scrollElementStateAtom, (previous) => { if (previous?.div === div) { return previous; } previous?.resizeObserver.disconnect(); if (div === null) { return null; } set(scrollElementSizeAtom, { width: div.clientWidth, height: div.clientHeight }); const resizeObserver = new ResizeObserver(() => { set(scrollElementSizeAtom, { width: div.clientWidth, height: div.clientHeight }); set(synchronizeScrollAtom); }); resizeObserver.observe(div); return { div, resizeObserver }; }); } ); scrollElementAtom.onMount = (set) => () => set(null); var goNextAtom = (0, import_jotai.atom)(null, (get) => { const scrollElement = get(scrollElementAtom); const { page } = getCurrentViewerScroll(scrollElement); if (!page) { return; } const viewerHeight = scrollElement.clientHeight; const ignorableHeight = viewerHeight * 0.05; const scrollBottom = scrollElement.scrollTop + viewerHeight; const remainingHeight = page.offsetTop + page.clientHeight - Math.ceil(scrollBottom) - 1; if (remainingHeight > ignorableHeight) { const divisor = Math.ceil(remainingHeight / viewerHeight); const delta = Math.ceil(remainingHeight / divisor); scrollElement.scroll({ top: Math.floor(scrollElement.scrollTop + delta) }); } else { scrollToNextPageTopOrEnd(page); } }); var goPreviousAtom = (0, import_jotai.atom)(null, (get) => { const scrollElement = get(scrollElementAtom); const { page } = getCurrentViewerScroll(scrollElement); if (!page) { return; } const viewerHeight = scrollElement.clientHeight; const ignorableHeight = viewerHeight * 0.05; const remainingHeight = scrollElement.scrollTop - Math.ceil(page.offsetTop) - 1; if (remainingHeight > ignorableHeight) { const divisor = Math.ceil(remainingHeight / viewerHeight); const delta = -Math.ceil(remainingHeight / divisor); scrollElement.scroll({ top: Math.floor(scrollElement.scrollTop + delta) }); } else { scrollToPreviousPageBottomOrStart(page); } }); var navigateAtom = (0, import_jotai.atom)(null, (get, set, event) => { const height = get(scrollElementAtom)?.clientHeight; if (!height || event.button !== 0) { return; } event.preventDefault(); const isTop = event.clientY < height / 2; if (isTop) { set(goPreviousAtom); } else { set(goNextAtom); } }); function scrollToNextPageTopOrEnd(page) { const scrollable = page.offsetParent; if (!scrollable) { return; } const pageBottom = page.offsetTop + page.clientHeight; let cursor = page; while (cursor.nextElementSibling) { const next = cursor.nextElementSibling; if (pageBottom <= next.offsetTop) { scrollable.scroll({ top: next.offsetTop }); return; } cursor = next; } scrollable.scroll({ top: cursor.offsetTop + cursor.clientHeight }); } function scrollToPreviousPageBottomOrStart(page) { const scrollable = page.offsetParent; if (!scrollable) { return; } const pageTop = page.offsetTop; let cursor = page; while (cursor.previousElementSibling) { const previous = cursor.previousElementSibling; const previousBottom = previous.offsetTop + previous.clientHeight; if (previousBottom <= pageTop) { scrollable.scroll({ top: previous.offsetTop + previous.clientHeight - scrollable.clientHeight }); return; } cursor = previous; } scrollable.scroll({ top: cursor.offsetTop }); } var gmStorage = { getItem: GM_getValue, setItem: GM_setValue, removeItem: (key) => GM_deleteValue(key) }; function atomWithGmValue(key, defaultValue) { return (0, import_utils2.atomWithStorage)(key, GM_getValue(key, defaultValue), gmStorage); } var jsonSessionStorage = (0, import_utils2.createJSONStorage)(() => sessionStorage); function atomWithSession(key, defaultValue) { const atom2 = (0, import_utils2.atomWithStorage)( key, jsonSessionStorage.getItem(key, defaultValue), jsonSessionStorage ); return atom2; } var backgroundColorAtom = atomWithGmValue("vim_comic_viewer.background_color", "#eeeeee"); var singlePageCountAtom = atomWithGmValue("vim_comic_viewer.single_page_count", 1); var maxZoomOutExponentAtom = atomWithGmValue("vim_comic_viewer.max_zoom_out_exponent", 3); var maxZoomInExponentAtom = atomWithGmValue("vim_comic_viewer.max_zoom_in_exponent", 3); var pageDirectionAtom = atomWithGmValue( "vim_comic_viewer.page_direction", "rightToLeft" ); var isFullscreenPreferredAtom = atomWithGmValue("vim_comic_viewer.use_full_screen", false); var fullscreenNoticeCountAtom = atomWithGmValue( "vim_comic_viewer.full_screen_notice_count", 0 ); var wasImmersiveAtom = atomWithSession("vim_comic_viewer.was_immersive", false); function createPageAtom({ index, source }) { let imageLoad = deferred(); let div = null; const stateAtom = (0, import_jotai.atom)({ status: "loading" }); const loadAtom = (0, import_jotai.atom)(null, async (_get, set) => { const urls = []; for await (const url of imageSourceToIterable(source)) { urls.push(url); imageLoad = deferred(); set(stateAtom, { src: url, status: "loading" }); const result = await imageLoad; switch (result) { case false: continue; case null: return; default: { const img = result; set(stateAtom, { src: url, naturalHeight: img.naturalHeight, status: "complete" }); return; } } } set(stateAtom, { urls, status: "error" }); }); loadAtom.onMount = (set) => { set(); }; const reloadAtom = (0, import_jotai.atom)(null, async (_get, set) => { imageLoad.resolve(null); await set(loadAtom); }); const imageToViewerSizeRatioAtom = (0, import_jotai.atom)((get) => { const viewerSize = get(scrollElementSizeAtom); if (!viewerSize) { return 1; } const state = get(stateAtom); if (state.status !== "complete") { return 1; } return state.naturalHeight / viewerSize.height; }); const shouldBeOriginalSizeAtom = (0, import_jotai.atom)((get) => { const maxZoomInExponent = get(maxZoomInExponentAtom); const maxZoomOutExponent = get(maxZoomOutExponentAtom); const imageRatio = get(imageToViewerSizeRatioAtom); const minZoomRatio = Math.sqrt(2) ** maxZoomOutExponent; const maxZoomRatio = Math.sqrt(2) ** maxZoomInExponent; const isOver = minZoomRatio < imageRatio || imageRatio < 1 / maxZoomRatio; return isOver; }); const aggregateAtom = (0, import_jotai.atom)((get) => { get(loadAtom); const state = get(stateAtom); const compactWidthIndex = get(singlePageCountAtom); const shouldBeOriginalSize = get(shouldBeOriginalSizeAtom); const ratio = get(imageToViewerSizeRatioAtom); const isLarge = ratio > 1; const canMessUpRow = shouldBeOriginalSize && isLarge; return { state, div, setDiv: (newDiv) => { div = newDiv; }, reloadAtom, fullWidth: index < compactWidthIndex || canMessUpRow, shouldBeOriginalSize, imageProps: { ..."src" in state ? { src: state.src } : {}, onError: () => imageLoad.resolve(false), onLoad: (event) => imageLoad.resolve(event.currentTarget) } }; }); return aggregateAtom; } var fullscreenElementAtom = (0, import_jotai.atom)(null); var viewerElementAtom = (0, import_jotai.atom)(null); var isViewerFullscreenAtom = (0, import_jotai.atom)( (get) => get(fullscreenElementAtom) === get(viewerElementAtom) ); var scrollBarStyleFactorAtom = (0, import_jotai.atom)( (get) => ({ fullscreenElement: get(fullscreenElementAtom), viewerElement: get(viewerElementAtom), isImmersive: get(wasImmersiveAtom) }), (get, set, factors) => { const { fullscreenElement, viewerElement, isImmersive } = factors; if (fullscreenElement !== void 0) { set(fullscreenElementAtom, fullscreenElement); } if (viewerElement !== void 0) { set(viewerElementAtom, viewerElement); } if (isImmersive !== void 0) { set(wasImmersiveAtom, isImmersive); } const canScrollBarDuplicate = !get(isViewerFullscreenAtom) && get(wasImmersiveAtom); hideBodyScrollBar(canScrollBarDuplicate); } ); scrollBarStyleFactorAtom.onMount = (set) => set({}); var viewerFullscreenAtom = (0, import_jotai.atom)((get) => { return get(isViewerFullscreenAtom); }, async (get, _set, value) => { const element = value ? get(viewerElementAtom) : null; const { fullscreenElement } = get(scrollBarStyleFactorAtom); if (element === fullscreenElement) { return; } await setFullscreenElement(element); }); var isFullscreenPreferredSettingsAtom = (0, import_jotai.atom)( (get) => get(isFullscreenPreferredAtom), async (get, set, value) => { set(isFullscreenPreferredAtom, value); const isImmersive = get(wasImmersiveAtom); const shouldEnterFullscreen = value && isImmersive; await set(viewerFullscreenAtom, shouldEnterFullscreen); } ); var en_default = { "@@locale": "en", settings: "Settings", help: "Help", maxZoomOut: "Maximum zoom out", maxZoomIn: "Maximum zoom in", singlePageCount: "single page count", backgroundColor: "Background color", leftToRight: "Left to right", errorIsOccurred: "Error is occurred.", failedToLoadImage: "Failed to load image.", loading: "Loading...", fullScreenRestorationGuide: "Enter full screen yourself if you want to keep the viewer open in full screen.", useFullScreen: "Use full screen", downloading: "Downloading...", cancel: "CANCEL", downloadComplete: "Download complete.", errorOccurredWhileDownloading: "Error occurred while downloading.", keyBindings: "Key bindings", toggleViewer: "Toggle viewer", toggleViewerWithTogglingFullscreenSetting: "Toggle viewer with toggling fullscreen setting", nextPage: "Next page", previousPage: "Previous page", download: "Download", refresh: "Refresh", increaseSinglePageCount: "Increase single page count", decreaseSinglePageCount: "Decrease single page count" }; var ko_default = { "@@locale": "ko", settings: "설정", help: "도움말", maxZoomOut: "최대 축소", maxZoomIn: "최대 확대", singlePageCount: "한쪽 페이지 수", backgroundColor: "배경색", leftToRight: "왼쪽부터 보기", errorIsOccurred: "에러가 발생했습니다.", failedToLoadImage: "이미지를 불러오지 못했습니다.", loading: "로딩 중...", fullScreenRestorationGuide: "뷰어 전체 화면을 유지하려면 직접 전체 화면을 켜 주세요 (F11).", useFullScreen: "전체 화면", downloading: "다운로드 중...", cancel: "취소", downloadComplete: "다운로드 완료", errorOccurredWhileDownloading: "다운로드 도중 오류가 발생했습니다", keyBindings: "단축키", toggleViewer: "뷰어 전환", toggleViewerWithTogglingFullscreenSetting: "전체화면 설정을 바꾸면서 전환", nextPage: "다음 페이지", previousPage: "이전 페이지", download: "다운로드", refresh: "새로고침", increaseSinglePageCount: "한쪽 페이지 수 늘리기", decreaseSinglePageCount: "한쪽 페이지 수 줄이기" }; var translations = { en: en_default, ko: ko_default }; var i18nStringsAtom = (0, import_jotai.atom)(getLanguage()); var i18nAtom = (0, import_jotai.atom)((get) => get(i18nStringsAtom), (_get, set) => { set(i18nStringsAtom, getLanguage()); }); i18nAtom.onMount = (set) => { addEventListener("languagechange", set); return () => { removeEventListener("languagechange", set); }; }; function getLanguage() { for (const language of navigator.languages) { const locale = language.split("-")[0]; const translation = translations[locale]; if (translation) { return translation; } } return en_default; } var viewerStateAtom = (0, import_jotai.atom)({ options: {}, status: "loading" }); var pagesAtom = (0, import_utils2.selectAtom)( viewerStateAtom, (state) => state.pages ); var rootAtom = (0, import_jotai.atom)(null); var transferViewerScrollToWindowAtom = (0, import_jotai.atom)(null, (get) => { const { page, ratio } = get(pageScrollStateAtom); const src = page?.querySelector("img")?.src; if (!src) { return; } const fileName = src.split("/").pop()?.split("?")[0]; const candidates = document.querySelectorAll(`img[src*="${fileName}"]`); const original = [...candidates].find((img) => img.src === src); const isViewerImage = original?.parentElement === page; if (!original || isViewerImage) { return; } const rect = original.getBoundingClientRect(); const top = window.scrollY + rect.y + rect.height * ratio - window.innerHeight / 2; scroll({ behavior: "instant", top }); }); var transferWindowScrollToViewerAtom = (0, import_jotai.atom)(null, (get, set) => { const viewerPages = get(pagesAtom)?.map(get); if (!viewerPages || !viewerPages.length) { return; } const urlToViewerPages = new Map(); for (const viewerPage2 of viewerPages) { if (viewerPage2.imageProps.src) { urlToViewerPages.set(viewerPage2.imageProps.src, viewerPage2); } } const urls = [...urlToViewerPages.keys()]; const imgs = getUrlImgs(urls); const viewerImgs = new Set(viewerPages.flatMap((page2) => page2.div?.querySelector("img") ?? [])); const originalImgs = imgs.filter((img) => !viewerImgs.has(img)); const { page, ratio, fullyVisiblePages: fullyVisibleWindowPages } = getCurrentScroll( originalImgs ); if (!page) { return; } const viewerPage = urlToViewerPages.get(page.src); if (!viewerPage) { return; } const fullyVisiblePages = fullyVisibleWindowPages.flatMap((img) => { return urlToViewerPages.get(img.src)?.div ?? []; }); const snappedRatio = Math.abs(ratio - 0.5) < 0.1 ? 0.5 : ratio; set(pageScrollStateAtom, { page: viewerPage.div, ratio: snappedRatio, fullyVisiblePages }); }); var isViewerImmersiveAtom = (0, import_jotai.atom)( (get) => get(scrollBarStyleFactorAtom).isImmersive, async (get, set, value) => { if (value !== import_utils2.RESET) { if (!get(viewerStateAtom).options.noSyncScroll && value) { set(transferWindowScrollToViewerAtom); } set(scrollBarStyleFactorAtom, { isImmersive: value }); } const scrollable = get(scrollElementAtom); if (!scrollable) { return; } if (value !== false) { focusWithoutScroll(scrollable); } if (value === import_utils2.RESET) { return; } try { if (get(isFullscreenPreferredAtom)) { await set(viewerFullscreenAtom, value); } } finally { if (value) { await timeout(1); set(restoreScrollAtom); } else if (!get(viewerStateAtom).options.noSyncScroll) { set(transferViewerScrollToWindowAtom); } } } ); isViewerImmersiveAtom.onMount = (set) => void set(import_utils2.RESET); var isBeforeUnloadAtom = (0, import_jotai.atom)(false); var beforeUnloadAtom = (0, import_jotai.atom)(null, async (_get, set) => { set(isBeforeUnloadAtom, true); for (let i = 0; i < 5; i++) { await timeout(100); } set(isBeforeUnloadAtom, false); }); beforeUnloadAtom.onMount = (set) => { addEventListener("beforeunload", set); return () => removeEventListener("beforeunload", set); }; var fullscreenSynchronizationAtom = (0, import_jotai.atom)( (get) => { get(beforeUnloadAtom); return get(scrollBarStyleFactorAtom).fullscreenElement; }, (get, set, element) => { const isFullscreenPreferred = get(isFullscreenPreferredAtom); const isFullscreen = element === get(scrollBarStyleFactorAtom).viewerElement; const wasImmersive = get(isViewerImmersiveAtom); const isViewerFullscreenExit = wasImmersive && !isFullscreen; const isNavigationExit = get(isBeforeUnloadAtom); const shouldExitImmersive = isFullscreenPreferred && isViewerFullscreenExit && !isNavigationExit; set(scrollBarStyleFactorAtom, { fullscreenElement: element, isImmersive: shouldExitImmersive ? false : void 0 }); } ); var fullscreenSyncWithWindowScrollAtom = (0, import_jotai.atom)( (get) => get(fullscreenSynchronizationAtom), (_get, set, element) => { set(fullscreenSynchronizationAtom, element); } ); fullscreenSyncWithWindowScrollAtom.onMount = (set) => { const notify = () => set(document.fullscreenElement ?? null); document.addEventListener("fullscreenchange", notify); return () => document.removeEventListener("fullscreenchange", notify); }; var setViewerElementAtom = (0, import_jotai.atom)( null, async (get, set, element) => { set(scrollBarStyleFactorAtom, { viewerElement: element }); const isViewerFullscreen = get(viewerFullscreenAtom); const isFullscreenPreferred = get(isFullscreenPreferredAtom); const isImmersive = get(isViewerImmersiveAtom); const shouldEnterFullscreen = isFullscreenPreferred && isImmersive; if (isViewerFullscreen === shouldEnterFullscreen || !element) { return; } const isUserFullscreen = window.innerHeight === screen.height || window.innerWidth === screen.width; if (isUserFullscreen) { return; } try { if (shouldEnterFullscreen) { await set(viewerFullscreenAtom, true); } } catch (error) { if (isUserGesturePermissionError(error)) { if (get(fullscreenNoticeCountAtom) >= 3) { return; } (0, import_react_toastify.toast)(get(i18nAtom).fullScreenRestorationGuide, { type: "info" }); await timeout(5e3); set(fullscreenNoticeCountAtom, (count) => count + 1); return; } throw error; } } ); var viewerModeAtom = (0, import_jotai.atom)((get) => { const isFullscreen = get(viewerFullscreenAtom); const isImmersive = get(isViewerImmersiveAtom); return isFullscreen ? "fullscreen" : isImmersive ? "window" : "normal"; }); var setViewerOptionsAtom = (0, import_jotai.atom)( null, async (get, set, options) => { try { const { source } = options; if (source === get(viewerStateAtom).options.source) { return; } if (!source) { set(viewerStateAtom, (state) => ({ ...state, status: "complete", images: [], pages: [] })); return; } set(viewerStateAtom, (state) => ({ ...state, status: "loading" })); const images = await source(); if (!Array.isArray(images)) { throw new Error(`Invalid comic source type: ${typeof images}`); } set(viewerStateAtom, (state) => ({ ...state, status: "complete", images, pages: images.map((source2, index) => createPageAtom({ source: source2, index })) })); } catch (error) { set(viewerStateAtom, (state) => ({ ...state, status: "error" })); console.error(error); throw error; } } ); var reloadErroredAtom = (0, import_jotai.atom)(null, (get, set) => { window.stop(); const pages = get(pagesAtom); for (const atom2 of pages ?? []) { const page = get(atom2); if (page.state.status !== "complete") { set(page.reloadAtom); } } }); var toggleImmersiveAtom = (0, import_jotai.atom)(null, async (get, set) => { if (get(viewerModeAtom) === "window" && get(isFullscreenPreferredAtom)) { await set(viewerFullscreenAtom, true); return; } await set(isViewerImmersiveAtom, !get(isViewerImmersiveAtom)); }); var blockSelectionAtom = (0, import_jotai.atom)(null, (_get, set, event) => { if (event.detail >= 2) { event.preventDefault(); } if (event.buttons === 3) { set(toggleImmersiveAtom); event.preventDefault(); } }); var { styled, css, keyframes } = (0, import_react.createStitches)({}); var Svg = styled("svg", { opacity: "50%", filter: "drop-shadow(0 0 1px white) drop-shadow(0 0 1px white)", color: "black", cursor: "pointer", "&:hover": { opacity: "100%", transform: "scale(1.1)" } }); var downloadCss = { width: "40px" }; var fullscreenCss = { position: "absolute", right: "1%", bottom: "1%", width: "40px" }; var DownloadIcon = (props) => React.createElement( Svg, { version: "1.1", xmlns: "http://www.w3.org/2000/svg", x: "0px", y: "0px", viewBox: "0 -34.51 122.88 122.87", css: downloadCss, ...props }, React.createElement("g", null, React.createElement("path", { d: "M58.29,42.08V3.12C58.29,1.4,59.7,0,61.44,0s3.15,1.4,3.15,3.12v38.96L79.1,29.4c1.3-1.14,3.28-1.02,4.43,0.27 s1.03,3.25-0.27,4.39L63.52,51.3c-1.21,1.06-3.01,1.03-4.18-0.02L39.62,34.06c-1.3-1.14-1.42-3.1-0.27-4.39 c1.15-1.28,3.13-1.4,4.43-0.27L58.29,42.08L58.29,42.08L58.29,42.08z M0.09,47.43c-0.43-1.77,0.66-3.55,2.43-3.98 c1.77-0.43,3.55,0.66,3.98,2.43c1.03,4.26,1.76,7.93,2.43,11.3c3.17,15.99,4.87,24.57,27.15,24.57h52.55 c20.82,0,22.51-9.07,25.32-24.09c0.67-3.6,1.4-7.5,2.44-11.78c0.43-1.77,2.21-2.86,3.98-2.43c1.77,0.43,2.85,2.21,2.43,3.98 c-0.98,4.02-1.7,7.88-2.36,11.45c-3.44,18.38-5.51,29.48-31.8,29.48H36.07C8.37,88.36,6.3,77.92,2.44,58.45 C1.71,54.77,0.98,51.08,0.09,47.43L0.09,47.43z" })) ); var FullscreenIcon = (props) => React.createElement( Svg, { version: "1.1", xmlns: "http://www.w3.org/2000/svg", x: "0px", y: "0px", viewBox: "0 0 122.88 122.87", css: fullscreenCss, ...props }, React.createElement("g", null, React.createElement("path", { d: "M122.88,77.63v41.12c0,2.28-1.85,4.12-4.12,4.12H77.33v-9.62h35.95c0-12.34,0-23.27,0-35.62H122.88L122.88,77.63z M77.39,9.53V0h41.37c2.28,0,4.12,1.85,4.12,4.12v41.18h-9.63V9.53H77.39L77.39,9.53z M9.63,45.24H0V4.12C0,1.85,1.85,0,4.12,0h41 v9.64H9.63V45.24L9.63,45.24z M45.07,113.27v9.6H4.12c-2.28,0-4.12-1.85-4.12-4.13V77.57h9.63v35.71H45.07L45.07,113.27z" })) ); var ErrorIcon = styled("svg", { width: "10vmin", height: "10vmin", fill: "hsl(0, 50%, 20%)", margin: "2rem" }); var CircledX = (props) => { return React.createElement( ErrorIcon, { x: "0px", y: "0px", viewBox: "0 0 122.881 122.88", "enable-background": "new 0 0 122.881 122.88", ...props }, React.createElement("g", null, React.createElement("path", { d: "M61.44,0c16.966,0,32.326,6.877,43.445,17.996c11.119,11.118,17.996,26.479,17.996,43.444 c0,16.967-6.877,32.326-17.996,43.444C93.766,116.003,78.406,122.88,61.44,122.88c-16.966,0-32.326-6.877-43.444-17.996 C6.877,93.766,0,78.406,0,61.439c0-16.965,6.877-32.326,17.996-43.444C29.114,6.877,44.474,0,61.44,0L61.44,0z M80.16,37.369 c1.301-1.302,3.412-1.302,4.713,0c1.301,1.301,1.301,3.411,0,4.713L65.512,61.444l19.361,19.362c1.301,1.301,1.301,3.411,0,4.713 c-1.301,1.301-3.412,1.301-4.713,0L60.798,66.157L41.436,85.52c-1.301,1.301-3.412,1.301-4.713,0c-1.301-1.302-1.301-3.412,0-4.713 l19.363-19.362L36.723,42.082c-1.301-1.302-1.301-3.412,0-4.713c1.301-1.302,3.412-1.302,4.713,0l19.363,19.362L80.16,37.369 L80.16,37.369z M100.172,22.708C90.26,12.796,76.566,6.666,61.44,6.666c-15.126,0-28.819,6.13-38.731,16.042 C12.797,32.62,6.666,46.314,6.666,61.439c0,15.126,6.131,28.82,16.042,38.732c9.912,9.911,23.605,16.042,38.731,16.042 c15.126,0,28.82-6.131,38.732-16.042c9.912-9.912,16.043-23.606,16.043-38.732C116.215,46.314,110.084,32.62,100.172,22.708 L100.172,22.708z" })) ); }; var IconSettings = (props) => { return React.createElement( Svg, { fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, viewBox: "0 0 24 24", height: "40px", width: "40px", ...props }, React.createElement("path", { d: "M15 12 A3 3 0 0 1 12 15 A3 3 0 0 1 9 12 A3 3 0 0 1 15 12 z" }), React.createElement("path", { d: "M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" }) ); }; var defaultScrollbar = { "scrollbarWidth": "initial", "scrollbarColor": "initial", "&::-webkit-scrollbar": { all: "initial" }, "&::-webkit-scrollbar-thumb": { all: "initial", background: "#00000088" }, "&::-webkit-scrollbar-track": { all: "initial" } }; var Container = styled("div", { position: "relative", height: "100%", overflow: "hidden", userSelect: "none", fontFamily: "Pretendard, NanumGothic, sans-serif", fontSize: "16px", color: "black", "&:focus-visible": { outline: "none" }, variants: { immersive: { true: { position: "fixed", top: 0, bottom: 0, left: 0, right: 0, zIndex: 9999999 } } } }); var ScrollableLayout = styled("div", { outline: 0, position: "relative", width: "100%", height: "100%", display: "flex", justifyContent: "center", alignItems: "center", flexFlow: "row-reverse wrap", overflowY: "auto", ...defaultScrollbar, variants: { fullscreen: { true: { position: "fixed", top: 0, bottom: 0, overflow: "auto" } }, ltr: { true: { flexFlow: "row wrap" } }, dark: { true: { "&::-webkit-scrollbar-thumb": { all: "initial", background: "#ffffff88" } } } } }); function useDefault({ enable, controller }) { const defaultKeyHandler = async (event) => { if (maybeNotHotkey(event)) { return; } switch (event.key) { case "j": case "ArrowDown": controller.goNext(); event.preventDefault(); break; case "k": case "ArrowUp": controller.goPrevious(); event.preventDefault(); break; case ";": await controller.downloader?.downloadAndSave(); break; case "/": controller.compactWidthIndex++; break; case "?": controller.compactWidthIndex--; break; case "'": controller.reloadErrored(); break; default: return; } event.stopPropagation(); }; const defaultGlobalKeyHandler = (event) => { if (maybeNotHotkey(event)) { return; } if (["KeyI", "Numpad0", "Enter"].includes(event.code)) { if (event.shiftKey) { controller.toggleWithFullscreenPreferred(); } else { controller.toggleFullscreen(); } } }; (0, import_react3.useEffect)(() => { if (!controller || !enable) { return; } addEventListener("keydown", defaultGlobalKeyHandler); controller.container?.addEventListener("keydown", defaultKeyHandler); return () => { controller.container?.removeEventListener("keydown", defaultKeyHandler); removeEventListener("keydown", defaultGlobalKeyHandler); }; }, [controller, enable]); } function maybeNotHotkey(event) { const { ctrlKey, altKey, metaKey } = event; return ctrlKey || altKey || metaKey || isTyping(event); } function DownloadCancel({ onClick }) { const strings = (0, import_jotai.useAtomValue)(i18nAtom); return React.createElement(SpaceBetween, null, React.createElement("p", null, strings.downloading), React.createElement("button", { onClick }, strings.cancel)); } var SpaceBetween = styled("div", { display: "flex", flexFlow: "row nowrap", justifyContent: "space-between" }); async function fetchBlob(url, init) { try { const response = await fetch(url, init); return await response.blob(); } catch (error) { if (init?.signal?.aborted) { throw error; } const isOriginDifferent = new URL(url).origin !== location.origin; if (isOriginDifferent) { return await gmFetch(url, init).blob(); } else { throw new Error("CORS blocked and cannot use GM_xmlhttpRequest", { cause: error }); } } } function gmFetch(resource, init) { const method = init?.body ? "POST" : "GET"; const xhr = (type) => { return new Promise((resolve, reject) => { const request = GM_xmlhttpRequest({ method, url: resource, headers: init?.headers, responseType: type === "text" ? void 0 : type, data: init?.body, onload: (response) => { if (type === "text") { resolve(response.responseText); } else { resolve(response.response); } }, onerror: reject, onabort: reject }); init?.signal?.addEventListener( "abort", () => { request.abort(); }, { once: true } ); }); }; return { blob: () => xhr("blob"), json: () => xhr("json"), text: () => xhr("text") }; } var isGmCancelled = (error) => { return error instanceof Function; }; async function* downloadImage({ source, signal }) { for await (const url of imageSourceToIterable(source)) { if (signal?.aborted) { break; } try { const blob = await fetchBlob(url, { signal }); yield { url, blob }; } catch (error) { if (isGmCancelled(error)) { yield { error: new Error("download aborted") }; } else { yield { error }; } } } } var getExtension = (url) => { if (!url) { return ".txt"; } const extension = url.match(/\.[^/?#]{3,4}?(?=[?#]|$)/); return extension?.[0] || ".jpg"; }; var guessExtension = (array) => { const { 0: a, 1: b, 2: c, 3: d } = array; if (a === 255 && b === 216 && c === 255) { return ".jpg"; } if (a === 137 && b === 80 && c === 78 && d === 71) { return ".png"; } if (a === 82 && b === 73 && c === 70 && d === 70) { return ".webp"; } if (a === 71 && b === 73 && c === 70 && d === 56) { return ".gif"; } }; var download = (images, options) => { const { onError, onProgress, signal } = options || {}; let startedCount = 0; let resolvedCount = 0; let rejectedCount = 0; let status = "ongoing"; const reportProgress = ({ transition } = {}) => { if (status !== "ongoing") { return; } if (transition) { status = transition; } onProgress?.({ total: images.length, started: startedCount, settled: resolvedCount + rejectedCount, rejected: rejectedCount, status }); }; const downloadWithReport = async (source) => { const errors = []; startedCount++; reportProgress(); for await (const event of downloadImage({ source, signal })) { if ("error" in event) { errors.push(event.error); onError?.(event.error); continue; } if (event.url) { resolvedCount++; } else { rejectedCount++; } reportProgress(); return event; } return { url: "", blob: new Blob([errors.map((x) => `${x}`).join("\n\n")]) }; }; const cipher = Math.floor(Math.log10(images.length)) + 1; const toPair = async ({ url, blob }, index) => { const array = new Uint8Array(await blob.arrayBuffer()); const pad = `${index}`.padStart(cipher, "0"); const name = `${pad}${guessExtension(array) ?? getExtension(url)}`; return { [name]: array }; }; const archiveWithReport = async (sources) => { const result = await Promise.all(sources.map(downloadWithReport)); if (signal?.aborted) { reportProgress({ transition: "cancelled" }); signal.throwIfAborted(); } const pairs = await Promise.all(result.map(toPair)); const data = Object.assign({}, ...pairs); const value = deferred(); const abort = (0, deps_exports.zip)(data, { level: 0 }, (error, array) => { if (error) { reportProgress({ transition: "error" }); value.reject(error); } else { reportProgress({ transition: "complete" }); value.resolve(array); } }); signal?.addEventListener("abort", abort, { once: true }); return value; }; return archiveWithReport(images); }; var aborterAtom = (0, import_jotai.atom)(null); var cancelDownloadAtom = (0, import_jotai.atom)(null, (get) => { get(aborterAtom)?.abort(); }); var startDownloadAtom = (0, import_jotai.atom)(null, async (get, set, options) => { const viewerState = get(viewerStateAtom); if (viewerState.status !== "complete") { return; } const aborter = new AbortController(); set(aborterAtom, (previous) => { previous?.abort(); return aborter; }); let toastId = null; addEventListener("beforeunload", confirmDownloadAbort); try { toastId = (0, import_react_toastify.toast)( React.createElement(DownloadCancel, { onClick: aborter.abort }), { autoClose: false, progress: 0 }); return await download(options?.images ?? viewerState.images, { onProgress: reportProgress, onError: logIfNotAborted, signal: aborter.signal }); } finally { removeEventListener("beforeunload", confirmDownloadAbort); } async function reportProgress(event) { if (!toastId) { return; } const { total, started, settled, rejected, status } = event; const value = started / total * 0.1 + settled / total * 0.89; switch (status) { case "ongoing": import_react_toastify.toast.update(toastId, { type: rejected > 0 ? "warning" : "default", progress: value }); break; case "complete": import_react_toastify.toast.update(toastId, { type: "success", render: get(i18nAtom).downloadComplete, progress: 0.9999 }); await timeout(1e3); import_react_toastify.toast.done(toastId); break; case "error": import_react_toastify.toast.update(toastId, { type: "error", render: get(i18nAtom).errorOccurredWhileDownloading, progress: 0 }); break; case "cancelled": import_react_toastify.toast.done(toastId); break; } } }); var downloadAndSaveAtom = (0, import_jotai.atom)(null, async (_get, set, options) => { const zip2 = await set(startDownloadAtom, options); if (zip2) { await save(new Blob([zip2])); } }); function logIfNotAborted(error) { if (isNotAbort(error)) { console.error(error); } } function isNotAbort(error) { return !/aborted/i.test(`${error}`); } function confirmDownloadAbort(event) { event.preventDefault(); event.returnValue = ""; } function useViewerController() { const store = (0, import_jotai.useStore)(); return (0, import_react3.useMemo)(() => createViewerController(store), [store]); } function createViewerController(store) { const downloader = { download: (options) => store.set(startDownloadAtom, options), downloadAndSave: (options) => store.set(downloadAndSaveAtom, options), cancel: () => store.set(cancelDownloadAtom) }; return { get options() { return store.get(viewerStateAtom).options; }, get status() { return store.get(viewerStateAtom).status; }, get container() { return store.get(scrollBarStyleFactorAtom).viewerElement; }, get compactWidthIndex() { return store.get(singlePageCountAtom); }, downloader, get pages() { return store.get(pagesAtom); }, set compactWidthIndex(value) { store.set(singlePageCountAtom, Math.max(0, value)); }, setOptions: (value) => store.set(setViewerOptionsAtom, value), goPrevious: () => store.set(goPreviousAtom), goNext: () => store.set(goNextAtom), toggleFullscreen: () => store.set(toggleImmersiveAtom), toggleWithFullscreenPreferred: async () => { if (store.get(isViewerImmersiveAtom)) { await store.set(toggleImmersiveAtom); store.set(isFullscreenPreferredAtom, !store.get(isFullscreenPreferredAtom)); } else { store.set(isFullscreenPreferredAtom, !store.get(isFullscreenPreferredAtom)); await store.set(toggleImmersiveAtom); } }, reloadErrored: () => store.set(reloadErroredAtom), unmount: () => store.get(rootAtom)?.unmount() }; } var Backdrop = styled("div", { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0, 0, 0, 0.5)", transition: "0.2s", variants: { isOpen: { true: { opacity: 1, pointerEvents: "auto" }, false: { opacity: 0, pointerEvents: "none" } } } }); var CenterDialog = styled("div", { minWidth: "20em", minHeight: "20em", transition: "0.2s", background: "white", padding: "20px", borderRadius: "10px", boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.2)" }); function BackdropDialog({ onClose, ...props }) { const [isOpen, setIsOpen] = (0, import_react3.useState)(false); const close = async () => { setIsOpen(false); await timeout(200); onClose(); }; const closeIfEnter = (event) => { if (event.key === "Enter") { close(); event.stopPropagation(); } }; (0, import_react3.useEffect)(() => { setIsOpen(true); }, []); return React.createElement(Backdrop, { isOpen, onClick: close, onKeyDown: closeIfEnter }, React.createElement( CenterDialog, { onClick: (event) => event.stopPropagation(), ...props } )); } function HelpTab() { const keyBindings = (0, import_jotai.useAtomValue)(keyBindingsAtom); const strings = (0, import_jotai.useAtomValue)(i18nAtom); return React.createElement(React.Fragment, null, React.createElement("p", null, strings.keyBindings), React.createElement("table", null, keyBindings.map(([action, keyBinding]) => React.createElement("tr", null, React.createElement(ActionName, null, action), React.createElement("td", null, keyBinding))))); } var keyBindingsAtom = (0, import_jotai.atom)((get) => { const strings = get(i18nAtom); return [ [ strings.toggleViewer, React.createElement(React.Fragment, null, React.createElement("kbd", null, "i"), ", ", React.createElement("kbd", null, "Enter⏎"), ", ", React.createElement("kbd", null, "NumPad0")) ], [ strings.toggleViewerWithTogglingFullscreenSetting, React.createElement(React.Fragment, null, React.createElement("kbd", null, "⇧Shift"), "+", React.createElement("kbd", null, "i")) ], [strings.nextPage, React.createElement("kbd", null, "j")], [strings.previousPage, React.createElement("kbd", null, "k")], [strings.download, React.createElement("kbd", null, ";")], [strings.refresh, React.createElement("kbd", null, "'")], [strings.increaseSinglePageCount, React.createElement("kbd", null, "/")], [strings.decreaseSinglePageCount, React.createElement("kbd", null, "?")] ]; }); var ActionName = styled("td", { width: "50%" }); var import_jotai2 = require("jotai"); function SettingsTab() { const [maxZoomOutExponent, setMaxZoomOutExponent] = (0, import_jotai.useAtom)(maxZoomOutExponentAtom); const [maxZoomInExponent, setMaxZoomInExponent] = (0, import_jotai.useAtom)(maxZoomInExponentAtom); const [singlePageCount, setSinglePageCount] = (0, import_jotai.useAtom)(singlePageCountAtom); const [backgroundColor, setBackgroundColor] = (0, import_jotai.useAtom)(backgroundColorAtom); const [pageDirection, setPageDirection] = (0, import_jotai.useAtom)(pageDirectionAtom); const [isFullscreenPreferred, setIsFullscreenPreferred] = (0, import_jotai.useAtom)( isFullscreenPreferredSettingsAtom ); const zoomOutExponentInputId = (0, import_react3.useId)(); const zoomInExponentInputId = (0, import_react3.useId)(); const singlePageCountInputId = (0, import_react3.useId)(); const colorInputId = (0, import_react3.useId)(); const pageDirectionInputId = (0, import_react3.useId)(); const fullscreenInputId = (0, import_react3.useId)(); const strings = (0, import_jotai2.useAtomValue)(i18nAtom); const maxZoomOut = formatMultiplier(maxZoomOutExponent); const maxZoomIn = formatMultiplier(maxZoomInExponent); return React.createElement(ConfigSheet, null, React.createElement(ConfigRow, null, React.createElement("label", { htmlFor: zoomOutExponentInputId }, strings.maxZoomOut, ": ", maxZoomOut), React.createElement( "input", { type: "number", min: 0, step: 0.1, id: zoomOutExponentInputId, value: maxZoomOutExponent, onChange: (event) => { setMaxZoomOutExponent(event.currentTarget.valueAsNumber || 0); } } )), React.createElement(ConfigRow, null, React.createElement("label", { htmlFor: zoomInExponentInputId }, strings.maxZoomIn, ": ", maxZoomIn), React.createElement( "input", { type: "number", min: 0, step: 0.1, id: zoomInExponentInputId, value: maxZoomInExponent, onChange: (event) => { setMaxZoomInExponent(event.currentTarget.valueAsNumber || 0); } } )), React.createElement(ConfigRow, null, React.createElement("label", { htmlFor: singlePageCountInputId }, strings.singlePageCount), React.createElement( "input", { type: "number", min: 0, step: 1, id: singlePageCountInputId, value: singlePageCount, onChange: (event) => { setSinglePageCount(event.currentTarget.valueAsNumber || 0); } } )), React.createElement(ConfigRow, null, React.createElement("label", { htmlFor: colorInputId }, strings.backgroundColor), React.createElement( ColorInput, { type: "color", id: colorInputId, value: backgroundColor, onChange: (event) => { setBackgroundColor(event.currentTarget.value); } } )), React.createElement(ConfigRow, null, React.createElement("p", null, strings.useFullScreen), React.createElement(Toggle, null, React.createElement( HiddenInput, { type: "checkbox", id: fullscreenInputId, checked: isFullscreenPreferred, onChange: (event) => { setIsFullscreenPreferred(event.currentTarget.checked); } } ), React.createElement("label", { htmlFor: fullscreenInputId }, strings.useFullScreen))), React.createElement(ConfigRow, null, React.createElement("p", null, strings.leftToRight), React.createElement(Toggle, null, React.createElement( HiddenInput, { type: "checkbox", id: pageDirectionInputId, checked: pageDirection === "leftToRight", onChange: (event) => { setPageDirection(event.currentTarget.checked ? "leftToRight" : "rightToLeft"); } } ), React.createElement("label", { htmlFor: pageDirectionInputId }, strings.leftToRight)))); } function formatMultiplier(maxZoomOutExponent) { return Math.sqrt(2) ** maxZoomOutExponent === Infinity ? "∞" : `${(Math.sqrt(2) ** maxZoomOutExponent).toPrecision(2)}x`; } var ColorInput = styled("input", { height: "1.5em" }); var ConfigRow = styled("div", { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10%", "&& > *": { fontSize: "1em", fontWeight: "medium", minWidth: "0", margin: 0, padding: 0 }, "& > input": { appearance: "meter", border: "gray 1px solid", borderRadius: "0.2em", textAlign: "center" }, ":first-child": { flex: "2 1 0" }, ":nth-child(2)": { flex: "1 1 0" } }); var HiddenInput = styled("input", { opacity: 0, width: 0, height: 0 }); var Toggle = styled("span", { "--width": "60px", "label": { position: "relative", display: "inline-flex", margin: 0, width: "var(--width)", height: "calc(var(--width) / 2)", borderRadius: "calc(var(--width) / 2)", cursor: "pointer", textIndent: "-9999px", background: "grey" }, "label:after": { position: "absolute", top: "calc(var(--width) * 0.025)", left: "calc(var(--width) * 0.025)", width: "calc(var(--width) * 0.45)", height: "calc(var(--width) * 0.45)", borderRadius: "calc(var(--width) * 0.45)", content: "", background: "#fff", transition: "0.3s" }, "input:checked + label": { background: "#bada55" }, "input:checked + label:after": { left: "calc(var(--width) * 0.975)", transform: "translateX(-100%)" }, "label:active:after": { width: "calc(var(--width) * 0.65)" } }); var ConfigSheet = styled("div", { display: "flex", flexFlow: "column nowrap", gap: "0.8em" }); function ViewerDialog({ onClose }) { const strings = (0, import_jotai.useAtomValue)(i18nAtom); return React.createElement(BackdropDialog, { onClose }, React.createElement(import_react2.Tab.Group, null, React.createElement(import_react2.Tab.List, { as: TabList }, React.createElement(import_react2.Tab, { as: PlainTab }, strings.settings), React.createElement(import_react2.Tab, { as: PlainTab }, strings.help)), React.createElement(import_react2.Tab.Panels, { as: TabPanels }, React.createElement(import_react2.Tab.Panel, null, React.createElement(SettingsTab, null)), React.createElement(import_react2.Tab.Panel, null, React.createElement(HelpTab, null))))); } var PlainTab = styled("button", { flex: 1, padding: "0.5em 1em", background: "transparent", border: "none", borderRadius: "0.5em", color: "#888", cursor: "pointer", fontSize: "1.2em", fontWeight: "bold", textAlign: "center", '&[data-headlessui-state="selected"]': { border: "1px solid black", color: "black" }, "&:hover": { color: "black" } }); var TabList = styled("div", { display: "flex", flexFlow: "row nowrap", gap: "0.5em" }); var TabPanels = styled("div", { marginTop: "1em" }); var LeftBottomFloat = styled("div", { position: "absolute", bottom: "1%", left: "1%", display: "flex", flexFlow: "column" }); var MenuActions = styled("div", { display: "flex", flexFlow: "column nowrap", alignItems: "center", gap: "16px" }); function LeftBottomControl() { const downloadAndSave = (0, import_jotai.useSetAtom)(downloadAndSaveAtom); const [isOpen, setIsOpen] = (0, import_react3.useState)(false); return React.createElement(React.Fragment, null, React.createElement(LeftBottomFloat, null, React.createElement(MenuActions, null, React.createElement( IconSettings, { onClick: () => { setIsOpen((value) => !value); } } ), React.createElement(DownloadIcon, { onClick: () => downloadAndSave() }))), isOpen && React.createElement(ViewerDialog, { onClose: () => setIsOpen(false) })); } var stretch = keyframes({ "0%": { top: "8px", height: "64px" }, "50%": { top: "24px", height: "32px" }, "100%": { top: "24px", height: "32px" } }); var SpinnerContainer = styled("div", { position: "absolute", left: "0", top: "0", right: "0", bottom: "0", margin: "auto", display: "flex", justifyContent: "center", alignItems: "center", div: { display: "inline-block", width: "16px", margin: "0 4px", background: "#fff", animation: `${stretch} 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite` }, "div:nth-child(1)": { "animation-delay": "-0.24s" }, "div:nth-child(2)": { "animation-delay": "-0.12s" }, "div:nth-child(3)": { "animation-delay": "0" } }); var Spinner = () => React.createElement(SpinnerContainer, null, React.createElement("div", null), React.createElement("div", null), React.createElement("div", null)); var Overlay = styled("div", { position: "relative", maxWidth: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", "@media print": { margin: 0 }, variants: { placeholder: { true: { width: "45%", height: "100%" } }, fullWidth: { true: { width: "100%" } }, originalSize: { true: { minHeight: "100%", height: "auto" } } } }); var LinkColumn = styled("div", { display: "flex", flexFlow: "column nowrap", alignItems: "center", justifyContent: "center", cursor: "pointer", boxShadow: "1px 1px 3px", padding: "1rem 1.5rem", transition: "box-shadow 1s easeOutExpo", "&:hover": { boxShadow: "2px 2px 5px" }, "&:active": { boxShadow: "0 0 2px" } }); var Image = styled("img", { position: "relative", height: "100%", maxWidth: "100%", objectFit: "contain", variants: { originalSize: { true: { height: "auto" } } } }); var Page = ({ atom: atom2, ...props }) => { const { imageProps, fullWidth, reloadAtom, shouldBeOriginalSize, state: pageState, setDiv } = (0, import_jotai.useAtomValue)(atom2); const strings = (0, import_jotai.useAtomValue)(i18nAtom); const reload = (0, import_jotai.useSetAtom)(reloadAtom); const { status } = pageState; const reloadErrored = async (event) => { event.stopPropagation(); await reload(); }; return React.createElement( Overlay, { ref: setDiv, placeholder: status !== "complete", originalSize: shouldBeOriginalSize, fullWidth }, status === "loading" && React.createElement(Spinner, null), status === "error" && React.createElement(LinkColumn, { onClick: reloadErrored }, React.createElement(CircledX, null), React.createElement("p", null, strings.failedToLoadImage), React.createElement("p", null, pageState.urls?.join("\n"))), React.createElement(Image, { ...imageProps, originalSize: shouldBeOriginalSize, ...props }) ); }; function InnerViewer(props) { const { useDefault: enableDefault, options: viewerOptions, onInitialized, ...otherProps } = props; const setViewerElement = (0, import_jotai.useSetAtom)(setViewerElementAtom); const setScrollElement = (0, import_jotai.useSetAtom)(scrollElementAtom); const isFullscreen = (0, import_jotai.useAtomValue)(viewerFullscreenAtom); const backgroundColor = (0, import_jotai.useAtomValue)(backgroundColorAtom); const viewer = (0, import_jotai.useAtomValue)(viewerStateAtom); const setViewerOptions = (0, import_jotai.useSetAtom)(setViewerOptionsAtom); const navigate = (0, import_jotai.useSetAtom)(navigateAtom); const blockSelection = (0, import_jotai.useSetAtom)(blockSelectionAtom); const synchronizeScroll = (0, import_jotai.useSetAtom)(synchronizeScrollAtom); const pageDirection = (0, import_jotai.useAtomValue)(pageDirectionAtom); const strings = (0, import_jotai.useAtomValue)(i18nAtom); const mode = (0, import_jotai.useAtomValue)(viewerModeAtom); (0, import_jotai.useAtomValue)(fullscreenSyncWithWindowScrollAtom); const { status } = viewer; const controller = useViewerController(); const { options, toggleFullscreen } = controller; useDefault({ enable: props.useDefault, controller }); (0, import_react3.useEffect)(() => { onInitialized?.(controller); }, [controller]); (0, import_react3.useEffect)(() => { setViewerOptions(viewerOptions); }, [viewerOptions]); return React.createElement( Container, { ref: setViewerElement, css: { backgroundColor }, immersive: mode === "window" }, React.createElement( ScrollableLayout, { tabIndex: 0, ref: setScrollElement, dark: isDarkColor(backgroundColor), fullscreen: isFullscreen, ltr: pageDirection === "leftToRight", onScroll: synchronizeScroll, onClick: navigate, onMouseDown: blockSelection, children: status === "complete" ? viewer.pages.map((atom2) => React.createElement( Page, { key: `${atom2}`, atom: atom2, ...options?.imageProps } )) : React.createElement("p", null, status === "error" ? strings.errorIsOccurred : strings.loading), ...otherProps } ), React.createElement(FullscreenIcon, { onClick: toggleFullscreen }), status === "complete" ? React.createElement(LeftBottomControl, null) : false, React.createElement(import_react_toastify.ToastContainer, null) ); } function isDarkColor(rgbColor) { const match = rgbColor.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); if (!match) { return false; } const [_, r, g, b] = match.map((x) => parseInt(x, 16)); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance < 0.5; } var types_exports = {}; function initialize(options) { const store = (0, import_jotai.createStore)(); const root = (0, import_react_dom.createRoot)(getDefaultRoot()); const deferredController = deferred(); root.render( React.createElement(import_jotai.Provider, { store }, React.createElement(InnerViewer, { onInitialized: deferredController.resolve, options, useDefault: true })) ); store.set(rootAtom, root); return deferredController; } var Viewer = (0, import_react3.forwardRef)(({ options, useDefault: useDefault2, onInitialized }) => { const store = (0, import_react3.useMemo)(import_jotai.createStore, []); return React.createElement(import_jotai.Provider, { store }, React.createElement(InnerViewer, { ...{ options, onInitialized, useDefault: useDefault2 } })); }); function getDefaultRoot() { const div = document.createElement("div"); div.setAttribute("style", "width: 0; height: 0;"); document.body.append(div); return div; }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址