manatoki comic viewer

Universal comic reader

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/500538/1411307/manatoki%20comic%20viewer.js

  1. // ==UserScript==
  2. // @name vim comic viewer
  3. // @name:ko vim comic viewer
  4. // @description Universal comic reader
  5. // @description:ko 만화 뷰어 라이브러리
  6. // @version 15.0.0
  7. // @namespace https://gf.qytechs.cn/en/users/713014-nanikit
  8. // @exclude *
  9. // @match http://unused-field.space/
  10. // @author nanikit
  11. // @license MIT
  12. // @grant GM_addValueChangeListener
  13. // @grant GM_getValue
  14. // @grant GM_removeValueChangeListener
  15. // @grant GM_setValue
  16. // @grant GM_xmlhttpRequest
  17. // @grant unsafeWindow
  18. // @resource link:@headlessui/react https://cdn.jsdelivr.net/npm/@headlessui/react@1.7.17/dist/headlessui.prod.cjs
  19. // @resource link:@stitches/react https://cdn.jsdelivr.net/npm/@stitches/react@1.3.1-1/dist/index.cjs
  20. // @resource link:clsx https://cdn.jsdelivr.net/npm/clsx@2.0.0/dist/clsx.js
  21. // @resource link:fflate https://cdn.jsdelivr.net/npm/fflate@0.8.1/lib/browser.cjs
  22. // @resource link:jotai https://cdn.jsdelivr.net/npm/jotai@2.4.2/index.js
  23. // @resource link:jotai/react https://cdn.jsdelivr.net/npm/jotai@2.4.2/react.js
  24. // @resource link:jotai/react/utils https://cdn.jsdelivr.net/npm/jotai@2.4.2/react/utils.js
  25. // @resource link:jotai/utils https://cdn.jsdelivr.net/npm/jotai@2.4.2/utils.js
  26. // @resource link:jotai/vanilla https://cdn.jsdelivr.net/npm/jotai@2.4.2/vanilla.js
  27. // @resource link:jotai/vanilla/utils https://cdn.jsdelivr.net/npm/jotai@2.4.2/vanilla/utils.js
  28. // @resource link:react https://cdn.jsdelivr.net/npm/react@18.2.0/cjs/react.production.min.js
  29. // @resource link:react-dom https://cdn.jsdelivr.net/npm/react-dom@18.2.0/cjs/react-dom.production.min.js
  30. // @resource link:react-toastify https://cdn.jsdelivr.net/npm/react-toastify@9.1.3/dist/react-toastify.js
  31. // @resource link:scheduler https://cdn.jsdelivr.net/npm/scheduler@0.23.0/cjs/scheduler.production.min.js
  32. // @resource link:vcv-inject-node-env data:,unsafeWindow.process=%7Benv:%7BNODE_ENV:%22production%22%7D%7D
  33. // @resource react-toastify-css https://cdn.jsdelivr.net/npm/react-toastify@9.1.3/dist/ReactToastify.css
  34. // ==/UserScript==
  35. "use strict";
  36.  
  37. var __create = Object.create;
  38. var __defProp = Object.defineProperty;
  39. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  40. var __getOwnPropNames = Object.getOwnPropertyNames;
  41. var __getProtoOf = Object.getPrototypeOf;
  42. var __hasOwnProp = Object.prototype.hasOwnProperty;
  43. var __export = (target, all) => {
  44. for (var name in all)
  45. __defProp(target, name, { get: all[name], enumerable: true });
  46. };
  47. var __copyProps = (to, from, except, desc) => {
  48. if (from && typeof from === "object" || typeof from === "function") {
  49. for (let key of __getOwnPropNames(from))
  50. if (!__hasOwnProp.call(to, key) && key !== except)
  51. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  52. }
  53. return to;
  54. };
  55. var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
  56. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  57. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  58. mod
  59. ));
  60. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  61. var mod_exports = {};
  62. __export(mod_exports, {
  63. Viewer: () => Viewer,
  64. download: () => download,
  65. initialize: () => initialize,
  66. types: () => types_exports,
  67. utils: () => utils_exports
  68. });
  69. module.exports = __toCommonJS(mod_exports);
  70. var React = __toESM(require("react"));
  71. var import_vcv_inject_node_env = require("vcv-inject-node-env");
  72. var deps_exports = {};
  73. __export(deps_exports, {
  74. Dialog: () => import_react2.Dialog,
  75. Fragment: () => import_react3.Fragment,
  76. Provider: () => import_jotai.Provider,
  77. RESET: () => import_utils2.RESET,
  78. Tab: () => import_react2.Tab,
  79. ToastContainer: () => import_react_toastify.ToastContainer,
  80. atom: () => import_jotai.atom,
  81. atomWithStorage: () => import_utils2.atomWithStorage,
  82. createContext: () => import_react3.createContext,
  83. createJSONStorage: () => import_utils2.createJSONStorage,
  84. createRef: () => import_react3.createRef,
  85. createRoot: () => import_react_dom.createRoot,
  86. createStitches: () => import_react.createStitches,
  87. createStore: () => import_jotai.createStore,
  88. deferred: () => deferred,
  89. forwardRef: () => import_react3.forwardRef,
  90. selectAtom: () => import_utils2.selectAtom,
  91. toast: () => import_react_toastify.toast,
  92. useAtom: () => import_jotai.useAtom,
  93. useAtomValue: () => import_jotai.useAtomValue,
  94. useCallback: () => import_react3.useCallback,
  95. useEffect: () => import_react3.useEffect,
  96. useId: () => import_react3.useId,
  97. useImperativeHandle: () => import_react3.useImperativeHandle,
  98. useLayoutEffect: () => import_react3.useLayoutEffect,
  99. useMemo: () => import_react3.useMemo,
  100. useReducer: () => import_react3.useReducer,
  101. useRef: () => import_react3.useRef,
  102. useSetAtom: () => import_jotai.useSetAtom,
  103. useState: () => import_react3.useState,
  104. useStore: () => import_jotai.useStore
  105. });
  106. var import_react = require("@stitches/react");
  107. __reExport(deps_exports, require("fflate"));
  108. function deferred() {
  109. let methods;
  110. let state = "pending";
  111. const promise = new Promise((resolve, reject) => {
  112. methods = {
  113. async resolve(value) {
  114. await value;
  115. state = "fulfilled";
  116. resolve(value);
  117. },
  118. reject(reason) {
  119. state = "rejected";
  120. reject(reason);
  121. }
  122. };
  123. });
  124. Object.defineProperty(promise, "state", { get: () => state });
  125. return Object.assign(promise, methods);
  126. }
  127. var import_jotai = require("jotai");
  128. var import_utils2 = require("jotai/utils");
  129. var import_react_toastify = require("react-toastify");
  130. var utils_exports = {};
  131. __export(utils_exports, {
  132. getSafeFileName: () => getSafeFileName,
  133. insertCss: () => insertCss,
  134. isTyping: () => isTyping,
  135. save: () => save,
  136. saveAs: () => saveAs,
  137. timeout: () => timeout,
  138. waitDomContent: () => waitDomContent
  139. });
  140. var timeout = (millisecond) => new Promise((resolve) => setTimeout(resolve, millisecond));
  141. var waitDomContent = (document2) => document2.readyState === "loading" ? new Promise((r) => document2.addEventListener("readystatechange", r, { once: true })) : true;
  142. var insertCss = (css2) => {
  143. const style = document.createElement("style");
  144. style.innerHTML = css2;
  145. document.head.append(style);
  146. };
  147. var isTyping = (event) => event.target?.tagName?.match?.(/INPUT|TEXTAREA/) || event.target?.isContentEditable;
  148. var saveAs = async (blob, name) => {
  149. const a = document.createElement("a");
  150. a.download = name;
  151. a.rel = "noopener";
  152. a.href = URL.createObjectURL(blob);
  153. a.click();
  154. await timeout(4e4);
  155. URL.revokeObjectURL(a.href);
  156. };
  157. var getSafeFileName = (str) => {
  158. return str.replace(/[<>:"/\\|?*\x00-\x1f]+/gi, "").trim() || "download";
  159. };
  160. var save = (blob) => {
  161. return saveAs(blob, `${getSafeFileName(document.title)}.zip`);
  162. };
  163. insertCss(GM_getResourceText("react-toastify-css"));
  164. var import_react2 = require("@headlessui/react");
  165. var import_react3 = require("react");
  166. var import_react_dom = require("react-dom");
  167. var import_jotai2 = require("jotai");
  168. var gmStorage = {
  169. getItem: GM_getValue,
  170. setItem: GM_setValue,
  171. removeItem: (key) => GM_deleteValue(key),
  172. subscribe: (key, callback) => {
  173. const id = GM_addValueChangeListener(key, (_key, _oldValue, newValue) => callback(newValue));
  174. return () => GM_removeValueChangeListener(id);
  175. }
  176. };
  177. function atomWithGmValue(key, defaultValue) {
  178. return (0, import_utils2.atomWithStorage)(key, defaultValue, gmStorage, { unstable_getOnInit: true });
  179. }
  180. var jsonSessionStorage = (0, import_utils2.createJSONStorage)(() => sessionStorage);
  181. function atomWithSession(key, defaultValue) {
  182. return (0, import_utils2.atomWithStorage)(
  183. key,
  184. defaultValue,
  185. jsonSessionStorage,
  186. { unstable_getOnInit: true }
  187. );
  188. }
  189. var defaultPreferences = {
  190. backgroundColor: "#eeeeee",
  191. singlePageCount: 1,
  192. maxZoomOutExponent: 3,
  193. maxZoomInExponent: 3,
  194. pageDirection: "rightToLeft",
  195. isFullscreenPreferred: false,
  196. fullscreenNoticeCount: 0
  197. };
  198. function getEffectivePreferences(scriptPreferences, manualPreferences) {
  199. return { ...defaultPreferences, ...scriptPreferences, ...manualPreferences };
  200. }
  201. var scriptPreferencesAtom = (0, import_jotai2.atom)({});
  202. var preferencesPresetAtom = (0, import_jotai2.atom)("default");
  203. var manualPreferencesAtomAtom = (0, import_jotai2.atom)((get) => {
  204. const preset = get(preferencesPresetAtom);
  205. const key = `vim_comic_viewer.preferences.${preset}`;
  206. return atomWithGmValue(key, {});
  207. });
  208. var manualPreferencesAtom = (0, import_jotai2.atom)(
  209. (get) => get(get(manualPreferencesAtomAtom)),
  210. (get, set, update) => {
  211. set(get(manualPreferencesAtomAtom), update);
  212. }
  213. );
  214. var preferencesAtom = (0, import_jotai2.atom)((get) => {
  215. return getEffectivePreferences(get(scriptPreferencesAtom), get(manualPreferencesAtom));
  216. });
  217. var backgroundColorAtom = atomWithPreferences("backgroundColor");
  218. var singlePageCountAtom = atomWithPreferences("singlePageCount");
  219. var maxZoomOutExponentAtom = atomWithPreferences("maxZoomOutExponent");
  220. var maxZoomInExponentAtom = atomWithPreferences("maxZoomInExponent");
  221. var pageDirectionAtom = atomWithPreferences("pageDirection");
  222. var isFullscreenPreferredAtom = atomWithPreferences("isFullscreenPreferred");
  223. var fullscreenNoticeCountAtom = atomWithPreferences("fullscreenNoticeCount");
  224. var wasImmersiveAtom = atomWithSession("vim_comic_viewer.was_immersive", false);
  225. function atomWithPreferences(key) {
  226. return (0, import_jotai2.atom)(
  227. (get) => get(preferencesAtom)[key],
  228. (get, set, update) => {
  229. const effective = typeof update === "function" ? update(get(preferencesAtom)[key]) : update;
  230. set(manualPreferencesAtom, (preferences) => ({ ...preferences, [key]: effective }));
  231. }
  232. );
  233. }
  234. function imageSourceToIterable(source) {
  235. if (typeof source === "string") {
  236. return async function* () {
  237. yield source;
  238. }();
  239. } else if (Array.isArray(source)) {
  240. return async function* () {
  241. for (const url of source) {
  242. yield url;
  243. }
  244. }();
  245. } else {
  246. return source();
  247. }
  248. }
  249. var globalCss = document.createElement("style");
  250. globalCss.innerHTML = `html, body {
  251. overflow: hidden;
  252. }`;
  253. function hideBodyScrollBar(doHide) {
  254. if (doHide) {
  255. document.head.append(globalCss);
  256. } else {
  257. globalCss.remove();
  258. }
  259. }
  260. async function setFullscreenElement(element) {
  261. if (element) {
  262. await element.requestFullscreen?.();
  263. } else {
  264. await document.exitFullscreen?.();
  265. }
  266. }
  267. function focusWithoutScroll(element) {
  268. element?.focus({ preventScroll: true });
  269. }
  270. var emptyScroll = { page: null, ratio: 0, fullyVisiblePages: [] };
  271. function getCurrentViewerScroll(container) {
  272. const children = [...container?.children ?? []];
  273. if (!container || !children.length) {
  274. return emptyScroll;
  275. }
  276. return getCurrentScroll(children);
  277. }
  278. function getUrlImgs(urls) {
  279. const pages = [];
  280. const imgs = document.querySelectorAll("img[src]");
  281. for (const img of imgs) {
  282. if (urls.includes(img.src)) {
  283. pages.push(img);
  284. }
  285. }
  286. return pages;
  287. }
  288. function getCurrentScroll(elements) {
  289. if (!elements.length) {
  290. return emptyScroll;
  291. }
  292. const pages = elements.map((page) => ({ page, rect: page.getBoundingClientRect() }));
  293. const fullyVisiblePages = pages.filter(
  294. ({ rect }) => rect.y >= 0 && rect.y + rect.height <= innerHeight
  295. );
  296. if (fullyVisiblePages.length) {
  297. return {
  298. page: fullyVisiblePages[0].page,
  299. ratio: 0.5,
  300. fullyVisiblePages: fullyVisiblePages.map((x) => x.page)
  301. };
  302. }
  303. const scrollCenter = innerHeight / 2;
  304. const centerCrossingPage = pages.find(
  305. ({ rect }) => rect.top <= scrollCenter && rect.bottom >= scrollCenter
  306. );
  307. if (centerCrossingPage) {
  308. const centerCrossingRect = centerCrossingPage.rect;
  309. const ratio = 1 - (centerCrossingRect.bottom - scrollCenter) / centerCrossingRect.height;
  310. return { page: centerCrossingPage.page, ratio, fullyVisiblePages: [] };
  311. }
  312. const firstPage = pages[0];
  313. const lastPage = pages[pages.length - 1];
  314. if (scrollCenter < pages[0].rect.top) {
  315. return { page: firstPage.page, ratio: 0, fullyVisiblePages: [] };
  316. }
  317. return { page: lastPage.page, ratio: 1, fullyVisiblePages: [] };
  318. }
  319. function isUserGesturePermissionError(error) {
  320. return error?.message === "Permissions check failed";
  321. }
  322. var scrollElementStateAtom = (0, import_jotai.atom)(null);
  323. var scrollElementAtom = (0, import_jotai.atom)((get) => get(scrollElementStateAtom)?.div ?? null);
  324. var scrollElementSizeAtom = (0, import_jotai.atom)({ width: 0, height: 0 });
  325. var pageScrollStateAtom = (0, import_jotai.atom)(getCurrentViewerScroll());
  326. var transferViewerScrollToWindowAtom = (0, import_jotai.atom)(null, (get) => {
  327. const { page, ratio } = get(pageScrollStateAtom);
  328. const src = page?.querySelector("img")?.src;
  329. if (!src) {
  330. return false;
  331. }
  332. const fileName = src.split("/").pop()?.split("?")[0];
  333. const candidates = document.querySelectorAll(`img[src*="${fileName}"]`);
  334. const original = [...candidates].find((img) => img.src === src);
  335. const isViewerImage = original?.parentElement === page;
  336. if (!original || isViewerImage) {
  337. return false;
  338. }
  339. const rect = original.getBoundingClientRect();
  340. const top = scrollY + rect.y + rect.height * ratio - innerHeight / 2;
  341. scroll({ behavior: "instant", top });
  342. return true;
  343. });
  344. var previousSizeAtom = (0, import_jotai.atom)({ width: 0, height: 0 });
  345. var synchronizeScrollAtom = (0, import_jotai.atom)(null, (get, set) => {
  346. const scrollElement = get(scrollElementAtom);
  347. const current = getCurrentViewerScroll(scrollElement);
  348. if (!current.page) {
  349. return;
  350. }
  351. const height = scrollElement?.clientHeight ?? 0;
  352. const width = scrollElement?.clientWidth ?? 0;
  353. const previous = get(previousSizeAtom);
  354. const isResizing = width === 0 || height === 0 || height !== previous.height || width !== previous.width;
  355. if (isResizing) {
  356. set(restoreScrollAtom);
  357. set(previousSizeAtom, { width, height });
  358. } else {
  359. set(pageScrollStateAtom, current);
  360. set(transferViewerScrollToWindowAtom);
  361. }
  362. });
  363. var viewerScrollAtom = (0, import_jotai.atom)(
  364. (get) => get(scrollElementAtom)?.scrollTop,
  365. (get, _set, top) => {
  366. get(scrollElementAtom)?.scroll({ top });
  367. }
  368. );
  369. var restoreScrollAtom = (0, import_jotai.atom)(null, (get, set) => {
  370. const { page, ratio } = get(pageScrollStateAtom);
  371. const scrollable = get(scrollElementAtom);
  372. if (!scrollable || !page) {
  373. return;
  374. }
  375. const { offsetTop, clientHeight } = page;
  376. const restoredY = Math.floor(offsetTop + clientHeight * ratio - scrollable.clientHeight / 2);
  377. set(viewerScrollAtom, restoredY);
  378. });
  379. var setScrollElementAtom = (0, import_jotai.atom)(
  380. null,
  381. (_get, set, div) => {
  382. set(scrollElementStateAtom, (previous) => {
  383. if (previous?.div === div) {
  384. return previous;
  385. }
  386. previous?.resizeObserver.disconnect();
  387. if (div === null) {
  388. return null;
  389. }
  390. set(scrollElementSizeAtom, { width: div.clientWidth, height: div.clientHeight });
  391. const resizeObserver = new ResizeObserver(() => {
  392. set(scrollElementSizeAtom, { width: div.clientWidth, height: div.clientHeight });
  393. set(restoreScrollAtom);
  394. });
  395. div.addEventListener("wheel", (event)=>event.preventDefault());
  396. resizeObserver.observe(div);
  397. return { div, resizeObserver };
  398. });
  399. }
  400. );
  401. var goNextAtom = (0, import_jotai.atom)(null, (get, set) => {
  402. const top = getNextScroll(get(scrollElementAtom));
  403. if (top != null) {
  404. set(viewerScrollAtom, top);
  405. }
  406. });
  407. var goPreviousAtom = (0, import_jotai.atom)(null, (get, set) => {
  408. const top = getPreviousScroll(get(scrollElementAtom));
  409. if (top != null) {
  410. set(viewerScrollAtom, top);
  411. }
  412. });
  413. var navigateAtom = (0, import_jotai.atom)(null, (get, set, event) => {
  414. const height = get(scrollElementAtom)?.clientHeight;
  415. if (!height || event.button !== 0) {
  416. return;
  417. }
  418. event.preventDefault();
  419. const isTop = event.clientY < height / 2;
  420. if (isTop) {
  421. set(goPreviousAtom);
  422. } else {
  423. set(goNextAtom);
  424. }
  425. });
  426. var wheelAtom = (0, import_jotai.atom)(null, (get, set, event) => {
  427. event.preventDefault();
  428. if(event.deltaY > 0) {
  429. set(goNextAtom);
  430. }else{
  431. set(goPreviousAtom);
  432. }
  433. });
  434. function getPreviousScroll(scrollElement) {
  435. const { page } = getCurrentViewerScroll(scrollElement);
  436. if (!page || !scrollElement) {
  437. return;
  438. }
  439. const viewerHeight = scrollElement.clientHeight;
  440. const ignorableHeight = viewerHeight * 0.05;
  441. const remainingHeight = scrollElement.scrollTop - Math.ceil(page.offsetTop) - 1;
  442. if (remainingHeight > ignorableHeight) {
  443. const divisor = Math.ceil(remainingHeight / viewerHeight);
  444. const delta = -Math.ceil(remainingHeight / divisor);
  445. return Math.floor(scrollElement.scrollTop + delta);
  446. } else {
  447. return getPreviousPageBottomOrStart(page);
  448. }
  449. }
  450. function getNextScroll(scrollElement) {
  451. const { page } = getCurrentViewerScroll(scrollElement);
  452. if (!page || !scrollElement) {
  453. return;
  454. }
  455. const viewerHeight = scrollElement.clientHeight;
  456. const ignorableHeight = viewerHeight * 0.05;
  457. const scrollBottom = scrollElement.scrollTop + viewerHeight;
  458. const remainingHeight = page.offsetTop + page.clientHeight - Math.ceil(scrollBottom) - 1;
  459. if (remainingHeight > ignorableHeight) {
  460. const divisor = Math.ceil(remainingHeight / viewerHeight);
  461. const delta = Math.ceil(remainingHeight / divisor);
  462. return Math.floor(scrollElement.scrollTop + delta);
  463. } else {
  464. return getNextPageTopOrEnd(page);
  465. }
  466. }
  467. function getNextPageTopOrEnd(page) {
  468. const scrollable = page.offsetParent;
  469. if (!scrollable) {
  470. return;
  471. }
  472. const pageBottom = page.offsetTop + page.clientHeight;
  473. let cursor = page;
  474. while (cursor.nextElementSibling) {
  475. const next = cursor.nextElementSibling;
  476. if (pageBottom <= next.offsetTop) {
  477. return next.offsetTop;
  478. }
  479. cursor = next;
  480. }
  481. return cursor.offsetTop + cursor.clientHeight;
  482. }
  483. function getPreviousPageBottomOrStart(page) {
  484. const scrollable = page.offsetParent;
  485. if (!scrollable) {
  486. return;
  487. }
  488. const pageTop = page.offsetTop;
  489. let cursor = page;
  490. while (cursor.previousElementSibling) {
  491. const previous = cursor.previousElementSibling;
  492. const previousBottom = previous.offsetTop + previous.clientHeight;
  493. if (previousBottom <= pageTop) {
  494. return previous.offsetTop + previous.clientHeight - scrollable.clientHeight;
  495. }
  496. cursor = previous;
  497. }
  498. return cursor.offsetTop;
  499. }
  500. function createPageAtom({ index, source }) {
  501. let imageLoad = deferred();
  502. let div = null;
  503. const stateAtom = (0, import_jotai.atom)({ status: "loading" });
  504. const loadAtom = (0, import_jotai.atom)(null, async (_get, set) => {
  505. imageLoad.resolve(null);
  506. const urls = [];
  507. for await (const url of imageSourceToIterable(source)) {
  508. urls.push(url);
  509. imageLoad = deferred();
  510. set(stateAtom, { src: url, status: "loading" });
  511. const result = await imageLoad;
  512. switch (result) {
  513. case false:
  514. continue;
  515. case null:
  516. return;
  517. default: {
  518. const img = result;
  519. set(stateAtom, { src: url, naturalHeight: img.naturalHeight, status: "complete" });
  520. return;
  521. }
  522. }
  523. }
  524. set(stateAtom, { urls, status: "error" });
  525. });
  526. loadAtom.onMount = (set) => {
  527. set();
  528. };
  529. const aggregateAtom = (0, import_jotai.atom)((get) => {
  530. get(loadAtom);
  531. const state = get(stateAtom);
  532. const compactWidthIndex = get(singlePageCountAtom);
  533. const ratio = getImageToViewerSizeRatio({ viewerSize: get(scrollElementSizeAtom), state });
  534. const shouldBeOriginalSize = shouldPageBeOriginalSize({
  535. maxZoomInExponent: get(maxZoomInExponentAtom),
  536. maxZoomOutExponent: get(maxZoomOutExponentAtom),
  537. imageRatio: ratio
  538. });
  539. const isLarge = ratio > 1;
  540. const canMessUpRow = shouldBeOriginalSize && isLarge;
  541. return {
  542. state,
  543. div,
  544. setDiv: (newDiv) => {
  545. div = newDiv;
  546. },
  547. reloadAtom: loadAtom,
  548. fullWidth: index < compactWidthIndex || canMessUpRow,
  549. shouldBeOriginalSize,
  550. imageProps: {
  551. ..."src" in state ? { src: state.src } : {},
  552. onError: () => imageLoad.resolve(false),
  553. onLoad: (event) => imageLoad.resolve(event.currentTarget)
  554. }
  555. };
  556. });
  557. return aggregateAtom;
  558. }
  559. function getImageToViewerSizeRatio({ viewerSize, state }) {
  560. if (!viewerSize) {
  561. return 1;
  562. }
  563. if (state.status !== "complete") {
  564. return 1;
  565. }
  566. return state.naturalHeight / viewerSize.height;
  567. }
  568. function shouldPageBeOriginalSize({ maxZoomOutExponent, maxZoomInExponent, imageRatio }) {
  569. const minZoomRatio = Math.sqrt(2) ** maxZoomOutExponent;
  570. const maxZoomRatio = Math.sqrt(2) ** maxZoomInExponent;
  571. const isOver = minZoomRatio < imageRatio || imageRatio < 1 / maxZoomRatio;
  572. return isOver;
  573. }
  574. var fullscreenElementAtom = (0, import_jotai.atom)(null);
  575. var viewerElementAtom = (0, import_jotai.atom)(null);
  576. var isViewerFullscreenAtom = (0, import_jotai.atom)((get) => {
  577. const viewerElement = get(viewerElementAtom);
  578. return !!viewerElement && viewerElement === get(fullscreenElementAtom);
  579. });
  580. var isImmersiveAtom = (0, import_jotai.atom)(false);
  581. var isViewerImmersiveAtom = (0, import_jotai.atom)((get) => get(isImmersiveAtom));
  582. var scrollBarStyleFactorAtom = (0, import_jotai.atom)(
  583. (get) => ({
  584. fullscreenElement: get(fullscreenElementAtom),
  585. viewerElement: get(viewerElementAtom)
  586. }),
  587. (get, set, factors) => {
  588. const { fullscreenElement, viewerElement, isImmersive } = factors;
  589. if (fullscreenElement !== void 0) {
  590. set(fullscreenElementAtom, fullscreenElement);
  591. }
  592. if (viewerElement !== void 0) {
  593. set(viewerElementAtom, viewerElement);
  594. }
  595. if (isImmersive !== void 0) {
  596. set(wasImmersiveAtom, isImmersive);
  597. set(isImmersiveAtom, isImmersive);
  598. }
  599. const canScrollBarDuplicate = !get(isViewerFullscreenAtom) && get(wasImmersiveAtom);
  600. hideBodyScrollBar(canScrollBarDuplicate);
  601. }
  602. );
  603. scrollBarStyleFactorAtom.onMount = (set) => set({});
  604. var viewerFullscreenAtom = (0, import_jotai.atom)((get) => {
  605. get(isFullscreenPreferredAtom);
  606. return get(isViewerFullscreenAtom);
  607. }, async (get, _set, value) => {
  608. const element = value ? get(viewerElementAtom) : null;
  609. const { fullscreenElement } = get(scrollBarStyleFactorAtom);
  610. if (element === fullscreenElement) {
  611. return;
  612. }
  613. const fullscreenChange = new Promise((resolve) => {
  614. addEventListener("fullscreenchange", resolve, { once: true });
  615. });
  616. await setFullscreenElement(element);
  617. await fullscreenChange;
  618. });
  619. var transitionDeferredAtom = (0, import_jotai.atom)({});
  620. var transitionLockAtom = (0, import_jotai.atom)(null, async (get, set) => {
  621. const { deferred: previousLock } = get(transitionDeferredAtom);
  622. const lock = deferred();
  623. set(transitionDeferredAtom, { deferred: lock });
  624. await previousLock;
  625. return { deferred: lock };
  626. });
  627. var isFullscreenPreferredSettingsAtom = (0, import_jotai.atom)(
  628. (get) => get(isFullscreenPreferredAtom),
  629. async (get, set, value) => {
  630. set(isFullscreenPreferredAtom, value);
  631. const lock = await set(transitionLockAtom);
  632. try {
  633. const wasImmersive = get(wasImmersiveAtom);
  634. const shouldEnterFullscreen = value && wasImmersive;
  635. await set(viewerFullscreenAtom, shouldEnterFullscreen);
  636. } finally {
  637. lock.deferred.resolve();
  638. }
  639. }
  640. );
  641. var en_default = {
  642. "@@locale": "en",
  643. settings: "Settings",
  644. help: "Help",
  645. maxZoomOut: "Maximum zoom out",
  646. maxZoomIn: "Maximum zoom in",
  647. singlePageCount: "single page count",
  648. backgroundColor: "Background color",
  649. leftToRight: "Left to right",
  650. reset: "Reset",
  651. doYouReallyWantToReset: "Do you really want to reset?",
  652. errorIsOccurred: "Error is occurred.",
  653. failedToLoadImage: "Failed to load image.",
  654. loading: "Loading...",
  655. fullScreenRestorationGuide: "Enter full screen yourself if you want to keep the viewer open in full screen.",
  656. useFullScreen: "Use full screen",
  657. downloading: "Downloading...",
  658. cancel: "CANCEL",
  659. downloadComplete: "Download complete.",
  660. errorOccurredWhileDownloading: "Error occurred while downloading.",
  661. keyBindings: "Key bindings",
  662. toggleViewer: "Toggle viewer",
  663. toggleFullscreenSetting: "Toggle fullscreen setting",
  664. nextPage: "Next page",
  665. previousPage: "Previous page",
  666. download: "Download",
  667. refresh: "Refresh",
  668. increaseSinglePageCount: "Increase single page count",
  669. decreaseSinglePageCount: "Decrease single page count"
  670. };
  671. var ko_default = {
  672. "@@locale": "ko",
  673. settings: "설정",
  674. help: "도움말",
  675. maxZoomOut: "최대 축소",
  676. maxZoomIn: "최대 확대",
  677. singlePageCount: "한쪽 페이지 수",
  678. backgroundColor: "배경색",
  679. leftToRight: "왼쪽부터 보기",
  680. reset: "초기화",
  681. doYouReallyWantToReset: "정말 초기화하시겠어요?",
  682. errorIsOccurred: "에러가 발생했습니다.",
  683. failedToLoadImage: "이미지를 불러오지 못했습니다.",
  684. loading: "로딩 중...",
  685. fullScreenRestorationGuide: "뷰어 전체 화면을 유지하려면 직접 전체 화면을 켜 주세요 (F11).",
  686. useFullScreen: "전체 화면",
  687. downloading: "다운로드 중...",
  688. cancel: "취소",
  689. downloadComplete: "다운로드 완료",
  690. errorOccurredWhileDownloading: "다운로드 도중 오류가 발생했습니다",
  691. keyBindings: "단축키",
  692. toggleViewer: "뷰어 전환",
  693. toggleFullscreenSetting: "전체화면 설정 전환",
  694. nextPage: "다음 페이지",
  695. previousPage: "이전 페이지",
  696. download: "다운로드",
  697. refresh: "새로고침",
  698. increaseSinglePageCount: "한쪽 페이지 수 늘리기",
  699. decreaseSinglePageCount: "한쪽 페이지 수 줄이기"
  700. };
  701. var translations = { en: en_default, ko: ko_default };
  702. var i18nStringsAtom = (0, import_jotai.atom)(getLanguage());
  703. var i18nAtom = (0, import_jotai.atom)((get) => get(i18nStringsAtom), (_get, set) => {
  704. set(i18nStringsAtom, getLanguage());
  705. });
  706. i18nAtom.onMount = (set) => {
  707. addEventListener("languagechange", set);
  708. return () => {
  709. removeEventListener("languagechange", set);
  710. };
  711. };
  712. function getLanguage() {
  713. for (const language of navigator.languages) {
  714. const locale = language.split("-")[0];
  715. const translation = translations[locale];
  716. if (translation) {
  717. return translation;
  718. }
  719. }
  720. return en_default;
  721. }
  722. var viewerStateAtom = (0, import_jotai.atom)({
  723. options: {},
  724. status: "loading"
  725. });
  726. var pagesAtom = (0, import_utils2.selectAtom)(
  727. viewerStateAtom,
  728. (state) => state.pages
  729. );
  730. var rootAtom = (0, import_jotai.atom)(null);
  731. var transferWindowScrollToViewerAtom = (0, import_jotai.atom)(null, async (get, set) => {
  732. const urlToViewerPages = new Map();
  733. let viewerPages = get(pagesAtom)?.map(get);
  734. if (!viewerPages || viewerPages?.some((page2) => !page2.imageProps.src)) {
  735. await timeout(1);
  736. viewerPages = get(pagesAtom)?.map(get);
  737. (async () => {
  738. await timeout(1);
  739. set(restoreScrollAtom);
  740. })();
  741. }
  742. if (!viewerPages || !viewerPages.length) {
  743. return;
  744. }
  745. for (const viewerPage2 of viewerPages) {
  746. if (viewerPage2.imageProps.src) {
  747. urlToViewerPages.set(viewerPage2.imageProps.src, viewerPage2);
  748. }
  749. }
  750. const urls = [...urlToViewerPages.keys()];
  751. const imgs = getUrlImgs(urls);
  752. const viewerImgs = new Set(viewerPages.flatMap((page2) => page2.div?.querySelector("img") ?? []));
  753. const originalImgs = imgs.filter((img) => !viewerImgs.has(img));
  754. const { page, ratio, fullyVisiblePages: fullyVisibleWindowPages } = getCurrentScroll(
  755. originalImgs
  756. );
  757. if (!page) {
  758. return;
  759. }
  760. const viewerPage = urlToViewerPages.get(page.src);
  761. if (!viewerPage) {
  762. return;
  763. }
  764. const fullyVisiblePages = fullyVisibleWindowPages.flatMap((img) => {
  765. return urlToViewerPages.get(img.src)?.div ?? [];
  766. });
  767. const snappedRatio = Math.abs(ratio - 0.5) < 0.1 ? 0.5 : ratio;
  768. set(pageScrollStateAtom, {
  769. page: viewerPage.div,
  770. ratio: snappedRatio,
  771. fullyVisiblePages
  772. });
  773. });
  774. var externalFocusElementAtom = (0, import_jotai.atom)(null);
  775. var setViewerImmersiveAtom = (0, import_jotai.atom)(
  776. null,
  777. async (get, set, value) => {
  778. const lock = await set(transitionLockAtom);
  779. try {
  780. await transactImmersive(get, set, value);
  781. } finally {
  782. lock.deferred.resolve();
  783. }
  784. }
  785. );
  786. async function transactImmersive(get, set, value) {
  787. if (get(isViewerImmersiveAtom) === value) {
  788. return;
  789. }
  790. if (value) {
  791. set(externalFocusElementAtom, (previous) => previous ? previous : document.activeElement);
  792. if (!get(viewerStateAtom).options.noSyncScroll) {
  793. set(transferWindowScrollToViewerAtom);
  794. }
  795. }
  796. const scrollable = get(scrollElementAtom);
  797. if (!scrollable) {
  798. return;
  799. }
  800. try {
  801. if (get(isFullscreenPreferredAtom)) {
  802. await set(viewerFullscreenAtom, value);
  803. }
  804. } catch (error) {
  805. if (isUserGesturePermissionError(error)) {
  806. showF11GuideGently();
  807. return;
  808. }
  809. throw error;
  810. } finally {
  811. set(scrollBarStyleFactorAtom, { isImmersive: value });
  812. if (value) {
  813. focusWithoutScroll(scrollable);
  814. } else {
  815. if (!get(viewerStateAtom).options.noSyncScroll) {
  816. set(transferViewerScrollToWindowAtom);
  817. }
  818. const externalFocusElement = get(externalFocusElementAtom);
  819. focusWithoutScroll(externalFocusElement);
  820. }
  821. }
  822. async function showF11GuideGently() {
  823. if (get(fullscreenNoticeCountAtom) >= 3) {
  824. return;
  825. }
  826. const isUserFullscreen = innerHeight === screen.height || innerWidth === screen.width;
  827. if (isUserFullscreen) {
  828. return;
  829. }
  830. (0, import_react_toastify.toast)(get(i18nAtom).fullScreenRestorationGuide, { type: "info" });
  831. await timeout(5e3);
  832. set(fullscreenNoticeCountAtom, (count) => count + 1);
  833. }
  834. }
  835. var isBeforeUnloadAtom = (0, import_jotai.atom)(false);
  836. var beforeUnloadAtom = (0, import_jotai.atom)(null, async (_get, set) => {
  837. set(isBeforeUnloadAtom, true);
  838. for (let i = 0; i < 5; i++) {
  839. await timeout(100);
  840. }
  841. set(isBeforeUnloadAtom, false);
  842. });
  843. beforeUnloadAtom.onMount = (set) => {
  844. addEventListener("beforeunload", set);
  845. return () => removeEventListener("beforeunload", set);
  846. };
  847. var fullscreenSynchronizationAtom = (0, import_jotai.atom)(
  848. (get) => {
  849. get(beforeUnloadAtom);
  850. return get(scrollBarStyleFactorAtom).fullscreenElement;
  851. },
  852. (get, set, element) => {
  853. const isFullscreenPreferred = get(isFullscreenPreferredAtom);
  854. const isFullscreen = element === get(scrollBarStyleFactorAtom).viewerElement;
  855. const wasImmersive = get(isViewerImmersiveAtom);
  856. const isViewerFullscreenExit = wasImmersive && !isFullscreen;
  857. const isNavigationExit = get(isBeforeUnloadAtom);
  858. const shouldExitImmersive = isFullscreenPreferred && isViewerFullscreenExit && !isNavigationExit;
  859. set(scrollBarStyleFactorAtom, {
  860. fullscreenElement: element,
  861. isImmersive: shouldExitImmersive ? false : void 0
  862. });
  863. }
  864. );
  865. fullscreenSynchronizationAtom.onMount = (set) => {
  866. const notify = () => set(document.fullscreenElement ?? null);
  867. document.addEventListener("fullscreenchange", notify);
  868. return () => document.removeEventListener("fullscreenchange", notify);
  869. };
  870. var setViewerElementAtom = (0, import_jotai.atom)(
  871. null,
  872. async (get, set, element) => {
  873. set(scrollBarStyleFactorAtom, { viewerElement: element });
  874. await set(setViewerImmersiveAtom, get(wasImmersiveAtom));
  875. }
  876. );
  877. var viewerModeAtom = (0, import_jotai.atom)((get) => {
  878. const isFullscreen = get(viewerFullscreenAtom);
  879. const isImmersive = get(isViewerImmersiveAtom);
  880. return isFullscreen ? "fullscreen" : isImmersive ? "window" : "normal";
  881. });
  882. var setViewerOptionsAtom = (0, import_jotai.atom)(
  883. null,
  884. async (get, set, options) => {
  885. try {
  886. const { source } = options;
  887. const previousOptions = get(viewerStateAtom).options;
  888. set(viewerStateAtom, (state) => ({ ...state, options }));
  889. if (!source || source === previousOptions.source) {
  890. return;
  891. }
  892. set(viewerStateAtom, (state) => ({ ...state, status: "loading" }));
  893. const images = await source();
  894. if (!Array.isArray(images)) {
  895. throw new Error(`Invalid comic source type: ${typeof images}`);
  896. }
  897. set(viewerStateAtom, (state) => ({
  898. ...state,
  899. status: "complete",
  900. images,
  901. pages: images.map((source2, index) => createPageAtom({ source: source2, index }))
  902. }));
  903. } catch (error) {
  904. set(viewerStateAtom, (state) => ({ ...state, status: "error" }));
  905. console.error(error);
  906. throw error;
  907. }
  908. }
  909. );
  910. var reloadErroredAtom = (0, import_jotai.atom)(null, (get, set) => {
  911. stop();
  912. const pages = get(pagesAtom);
  913. for (const atom3 of pages ?? []) {
  914. const page = get(atom3);
  915. if (page.state.status !== "complete") {
  916. set(page.reloadAtom);
  917. }
  918. }
  919. });
  920. var toggleImmersiveAtom = (0, import_jotai.atom)(null, async (get, set) => {
  921. const hasPermissionIssue = get(viewerModeAtom) === "window" && get(isFullscreenPreferredAtom);
  922. if (hasPermissionIssue) {
  923. await set(viewerFullscreenAtom, true);
  924. return;
  925. }
  926. await set(setViewerImmersiveAtom, !get(isViewerImmersiveAtom));
  927. });
  928. var toggleFullscreenAtom = (0, import_jotai.atom)(null, async (get, set) => {
  929. set(isFullscreenPreferredSettingsAtom, !get(isFullscreenPreferredSettingsAtom));
  930. if (get(viewerModeAtom) === "normal") {
  931. await set(setViewerImmersiveAtom, true);
  932. }
  933. });
  934. var blockSelectionAtom = (0, import_jotai.atom)(null, (_get, set, event) => {
  935. if (event.detail >= 2) {
  936. event.preventDefault();
  937. }
  938. if (event.buttons === 3) {
  939. set(toggleImmersiveAtom);
  940. event.preventDefault();
  941. }
  942. });
  943. var { styled, css, keyframes } = (0, import_react.createStitches)({});
  944. function DownloadCancel({ onClick }) {
  945. const strings = (0, import_jotai.useAtomValue)(i18nAtom);
  946. return React.createElement(SpaceBetween, null, React.createElement("p", null, strings.downloading), React.createElement("button", { onClick }, strings.cancel));
  947. }
  948. var SpaceBetween = styled("div", {
  949. display: "flex",
  950. flexFlow: "row nowrap",
  951. justifyContent: "space-between"
  952. });
  953. var isGmFetchAvailable = typeof GM_xmlhttpRequest === "function";
  954. function gmFetch(resource, init) {
  955. const method = init?.body ? "POST" : "GET";
  956. const xhr = (type) => {
  957. return new Promise((resolve, reject) => {
  958. const request = GM_xmlhttpRequest({
  959. method,
  960. url: resource,
  961. headers: {
  962. referer: `${location.origin}/`,
  963. ...init?.headers
  964. },
  965. responseType: type === "text" ? void 0 : type,
  966. data: init?.body,
  967. onload: (response) => {
  968. if (type === "text") {
  969. resolve(response.responseText);
  970. } else {
  971. resolve(response.response);
  972. }
  973. },
  974. onerror: reject,
  975. onabort: reject
  976. });
  977. init?.signal?.addEventListener(
  978. "abort",
  979. () => {
  980. request.abort();
  981. },
  982. { once: true }
  983. );
  984. });
  985. };
  986. return {
  987. blob: () => xhr("blob"),
  988. json: () => xhr("json"),
  989. text: () => xhr("text")
  990. };
  991. }
  992. function download(images, options) {
  993. const { onError, onProgress, signal } = options || {};
  994. let startedCount = 0;
  995. let resolvedCount = 0;
  996. let rejectedCount = 0;
  997. let status = "ongoing";
  998. const reportProgress = ({ transition } = {}) => {
  999. if (status !== "ongoing") {
  1000. return;
  1001. }
  1002. if (transition) {
  1003. status = transition;
  1004. }
  1005. onProgress?.({
  1006. total: images.length,
  1007. started: startedCount,
  1008. settled: resolvedCount + rejectedCount,
  1009. rejected: rejectedCount,
  1010. status
  1011. });
  1012. };
  1013. const downloadWithReport = async (source) => {
  1014. const errors = [];
  1015. startedCount++;
  1016. reportProgress();
  1017. for await (const event of downloadImage({ source, signal })) {
  1018. if ("error" in event) {
  1019. errors.push(event.error);
  1020. onError?.(event.error);
  1021. continue;
  1022. }
  1023. if (event.url) {
  1024. resolvedCount++;
  1025. } else {
  1026. rejectedCount++;
  1027. }
  1028. reportProgress();
  1029. return event;
  1030. }
  1031. return {
  1032. url: "",
  1033. blob: new Blob([errors.map((x) => `${x}`).join("\n\n")])
  1034. };
  1035. };
  1036. const cipher = Math.floor(Math.log10(images.length)) + 1;
  1037. const toPair = async ({ url, blob }, index) => {
  1038. const array = new Uint8Array(await blob.arrayBuffer());
  1039. const pad = `${index}`.padStart(cipher, "0");
  1040. const name = `${pad}${guessExtension(array) ?? getExtension(url)}`;
  1041. return { [name]: array };
  1042. };
  1043. const archiveWithReport = async (sources) => {
  1044. const result = await Promise.all(sources.map(downloadWithReport));
  1045. if (signal?.aborted) {
  1046. reportProgress({ transition: "cancelled" });
  1047. signal.throwIfAborted();
  1048. }
  1049. const pairs = await Promise.all(result.map(toPair));
  1050. const data = Object.assign({}, ...pairs);
  1051. const value = deferred();
  1052. const abort = (0, deps_exports.zip)(data, { level: 0 }, (error, array) => {
  1053. if (error) {
  1054. reportProgress({ transition: "error" });
  1055. value.reject(error);
  1056. } else {
  1057. reportProgress({ transition: "complete" });
  1058. value.resolve(array);
  1059. }
  1060. });
  1061. signal?.addEventListener("abort", abort, { once: true });
  1062. return value;
  1063. };
  1064. return archiveWithReport(images);
  1065. }
  1066. function getExtension(url) {
  1067. if (!url) {
  1068. return ".txt";
  1069. }
  1070. const extension = url.match(/\.[^/?#]{3,4}?(?=[?#]|$)/);
  1071. return extension?.[0] || ".jpg";
  1072. }
  1073. function guessExtension(array) {
  1074. const { 0: a, 1: b, 2: c, 3: d } = array;
  1075. if (a === 255 && b === 216 && c === 255) {
  1076. return ".jpg";
  1077. }
  1078. if (a === 137 && b === 80 && c === 78 && d === 71) {
  1079. return ".png";
  1080. }
  1081. if (a === 82 && b === 73 && c === 70 && d === 70) {
  1082. return ".webp";
  1083. }
  1084. if (a === 71 && b === 73 && c === 70 && d === 56) {
  1085. return ".gif";
  1086. }
  1087. }
  1088. async function* downloadImage({ source, signal }) {
  1089. for await (const url of imageSourceToIterable(source)) {
  1090. if (signal?.aborted) {
  1091. break;
  1092. }
  1093. try {
  1094. const blob = await fetchBlobWithCacheIfPossible(url, signal);
  1095. yield { url, blob };
  1096. } catch (error) {
  1097. yield await fetchBlobIgnoringCors(url, { signal, fetchError: error });
  1098. }
  1099. }
  1100. }
  1101. async function fetchBlobWithCacheIfPossible(url, signal) {
  1102. const response = await fetch(url, { signal });
  1103. return await response.blob();
  1104. }
  1105. async function fetchBlobIgnoringCors(url, { signal, fetchError }) {
  1106. if (isCrossOrigin(url) && !isGmFetchAvailable) {
  1107. return {
  1108. error: new Error(
  1109. "It could be a CORS issue but cannot use GM_xmlhttpRequest",
  1110. { cause: fetchError }
  1111. )
  1112. };
  1113. }
  1114. try {
  1115. const blob = await gmFetch(url, { signal }).blob();
  1116. return { url, blob };
  1117. } catch (error) {
  1118. if (isGmCancelled(error)) {
  1119. return { error: new Error("download aborted") };
  1120. } else {
  1121. return { error: fetchError };
  1122. }
  1123. }
  1124. }
  1125. function isCrossOrigin(url) {
  1126. return new URL(url).origin !== location.origin;
  1127. }
  1128. function isGmCancelled(error) {
  1129. return error instanceof Function;
  1130. }
  1131. var aborterAtom = (0, import_jotai.atom)(null);
  1132. var cancelDownloadAtom = (0, import_jotai.atom)(null, (get) => {
  1133. get(aborterAtom)?.abort();
  1134. });
  1135. var startDownloadAtom = (0, import_jotai.atom)(null, async (get, set, options) => {
  1136. const viewerState = get(viewerStateAtom);
  1137. if (viewerState.status !== "complete") {
  1138. return;
  1139. }
  1140. const aborter = new AbortController();
  1141. set(aborterAtom, (previous) => {
  1142. previous?.abort();
  1143. return aborter;
  1144. });
  1145. let toastId = null;
  1146. addEventListener("beforeunload", confirmDownloadAbort);
  1147. try {
  1148. toastId = (0, import_react_toastify.toast)( React.createElement(DownloadCancel, { onClick: aborter.abort }), { autoClose: false, progress: 0 });
  1149. return await download(options?.images ?? viewerState.images, {
  1150. onProgress: reportProgress,
  1151. onError: logIfNotAborted,
  1152. signal: aborter.signal
  1153. });
  1154. } finally {
  1155. removeEventListener("beforeunload", confirmDownloadAbort);
  1156. }
  1157. async function reportProgress(event) {
  1158. if (!toastId) {
  1159. return;
  1160. }
  1161. const { total, started, settled, rejected, status } = event;
  1162. const value = started / total * 0.1 + settled / total * 0.89;
  1163. switch (status) {
  1164. case "ongoing":
  1165. import_react_toastify.toast.update(toastId, { type: rejected > 0 ? "warning" : "default", progress: value });
  1166. break;
  1167. case "complete":
  1168. import_react_toastify.toast.update(toastId, {
  1169. type: "success",
  1170. render: get(i18nAtom).downloadComplete,
  1171. progress: 0.9999
  1172. });
  1173. await timeout(1e3);
  1174. import_react_toastify.toast.done(toastId);
  1175. break;
  1176. case "error":
  1177. import_react_toastify.toast.update(toastId, {
  1178. type: "error",
  1179. render: get(i18nAtom).errorOccurredWhileDownloading,
  1180. progress: 0
  1181. });
  1182. break;
  1183. case "cancelled":
  1184. import_react_toastify.toast.done(toastId);
  1185. break;
  1186. }
  1187. }
  1188. });
  1189. var downloadAndSaveAtom = (0, import_jotai.atom)(null, async (_get, set, options) => {
  1190. const zip2 = await set(startDownloadAtom, options);
  1191. if (zip2) {
  1192. await save(new Blob([zip2]));
  1193. }
  1194. });
  1195. function logIfNotAborted(error) {
  1196. if (isNotAbort(error)) {
  1197. console.error(error);
  1198. }
  1199. }
  1200. function isNotAbort(error) {
  1201. return !/aborted/i.test(`${error}`);
  1202. }
  1203. function confirmDownloadAbort(event) {
  1204. event.preventDefault();
  1205. event.returnValue = "";
  1206. }
  1207. var controllerAtom = (0, import_jotai.atom)(null);
  1208. var controllerCreationAtom = (0, import_jotai.atom)((get) => get(controllerAtom), (get, set) => {
  1209. if (!get(controllerAtom)) {
  1210. set(controllerAtom, createViewerController(get, set));
  1211. }
  1212. return get(controllerAtom);
  1213. });
  1214. controllerCreationAtom.onMount = (set) => {
  1215. set();
  1216. };
  1217. function createViewerController(get, set) {
  1218. const downloader = {
  1219. download: (options) => set(startDownloadAtom, options),
  1220. downloadAndSave: (options) => set(downloadAndSaveAtom, options),
  1221. cancel: () => set(cancelDownloadAtom)
  1222. };
  1223. const elementKeyHandler = (event) => {
  1224. if (maybeNotHotkey(event)) {
  1225. return false;
  1226. }
  1227. switch (event.key) {
  1228. case "j":
  1229. case "ArrowDown":
  1230. controller.goNext();
  1231. event.preventDefault();
  1232. break;
  1233. case "k":
  1234. case "ArrowUp":
  1235. controller.goPrevious();
  1236. event.preventDefault();
  1237. break;
  1238. case ";":
  1239. controller.downloader?.downloadAndSave();
  1240. break;
  1241. case "/":
  1242. controller.setManualPreferences({
  1243. ...controller.manualPreferences,
  1244. singlePageCount: controller.effectivePreferences.singlePageCount + 1
  1245. });
  1246. break;
  1247. case "?":
  1248. controller.setManualPreferences({
  1249. ...controller.manualPreferences,
  1250. singlePageCount: Math.max(0, controller.effectivePreferences.singlePageCount - 1)
  1251. });
  1252. break;
  1253. case "'":
  1254. controller.reloadErrored();
  1255. break;
  1256. default:
  1257. return false;
  1258. }
  1259. event.stopPropagation();
  1260. return true;
  1261. };
  1262. const globalKeyHandler = (event) => {
  1263. if (maybeNotHotkey(event)) {
  1264. return false;
  1265. }
  1266. if (["KeyI", "Numpad0", "Enter"].includes(event.code)) {
  1267. if (event.shiftKey) {
  1268. controller.toggleFullscreen();
  1269. } else {
  1270. controller.toggleImmersive();
  1271. }
  1272. return true;
  1273. }
  1274. return false;
  1275. };
  1276. const controller = {
  1277. get options() {
  1278. return get(viewerStateAtom).options;
  1279. },
  1280. get status() {
  1281. return get(viewerStateAtom).status;
  1282. },
  1283. get container() {
  1284. return get(scrollBarStyleFactorAtom).viewerElement;
  1285. },
  1286. downloader,
  1287. get pages() {
  1288. return get(pagesAtom);
  1289. },
  1290. get viewerMode() {
  1291. return get(viewerModeAtom);
  1292. },
  1293. get effectivePreferences() {
  1294. return get(preferencesAtom);
  1295. },
  1296. get manualPreferences() {
  1297. return get(manualPreferencesAtom);
  1298. },
  1299. setOptions: (value) => set(setViewerOptionsAtom, value),
  1300. goPrevious: () => set(goPreviousAtom),
  1301. goNext: () => set(goNextAtom),
  1302. setManualPreferences: (value) => {
  1303. return set(manualPreferencesAtom, value);
  1304. },
  1305. setScriptPreferences: ({ manualPreset, preferences }) => {
  1306. if (manualPreset) {
  1307. set(preferencesPresetAtom, manualPreset);
  1308. }
  1309. if (preferences) {
  1310. set(scriptPreferencesAtom, preferences);
  1311. }
  1312. },
  1313. setImmersive: (value) => {
  1314. return set(setViewerImmersiveAtom, value);
  1315. },
  1316. setIsFullscreenPreferred: (value) => {
  1317. return set(isFullscreenPreferredSettingsAtom, value);
  1318. },
  1319. toggleImmersive: () => set(toggleImmersiveAtom),
  1320. toggleFullscreen: () => set(toggleFullscreenAtom),
  1321. reloadErrored: () => set(reloadErroredAtom),
  1322. elementKeyHandler,
  1323. globalKeyHandler,
  1324. unmount: () => get(rootAtom)?.unmount()
  1325. };
  1326. return controller;
  1327. }
  1328. function maybeNotHotkey(event) {
  1329. const { ctrlKey, altKey, metaKey } = event;
  1330. return ctrlKey || altKey || metaKey || isTyping(event);
  1331. }
  1332. var Svg = styled("svg", {
  1333. opacity: "50%",
  1334. filter: "drop-shadow(0 0 1px white) drop-shadow(0 0 1px white)",
  1335. color: "black"
  1336. });
  1337. var downloadCss = { width: "40px" };
  1338. var fullscreenCss = {
  1339. position: "absolute",
  1340. right: "1%",
  1341. bottom: "1%"
  1342. };
  1343. var IconButton = styled("button", {
  1344. background: "transparent",
  1345. border: "none",
  1346. cursor: "pointer",
  1347. padding: 0,
  1348. "& > svg": {
  1349. pointerEvents: "none"
  1350. },
  1351. "&:hover > svg": {
  1352. opacity: "100%",
  1353. transform: "scale(1.1)"
  1354. },
  1355. "&:focus > svg": {
  1356. opacity: "100%"
  1357. }
  1358. });
  1359. var DownloadButton = (props) => React.createElement(IconButton, { ...props }, React.createElement(
  1360. Svg,
  1361. {
  1362. version: "1.1",
  1363. xmlns: "http://www.w3.org/2000/svg",
  1364. x: "0px",
  1365. y: "0px",
  1366. viewBox: "0 -34.51 122.88 122.87",
  1367. css: downloadCss
  1368. },
  1369. 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" }))
  1370. ));
  1371. var FullscreenButton = (props) => React.createElement(IconButton, { css: fullscreenCss, ...props }, React.createElement(
  1372. Svg,
  1373. {
  1374. version: "1.1",
  1375. xmlns: "http://www.w3.org/2000/svg",
  1376. x: "0px",
  1377. y: "0px",
  1378. viewBox: "0 0 122.88 122.87",
  1379. width: "40px",
  1380. ...props
  1381. },
  1382. 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" }))
  1383. ));
  1384. var ErrorIcon = styled("svg", {
  1385. width: "10vmin",
  1386. height: "10vmin",
  1387. fill: "hsl(0, 50%, 20%)",
  1388. margin: "2rem"
  1389. });
  1390. var CircledX = (props) => {
  1391. return React.createElement(
  1392. ErrorIcon,
  1393. {
  1394. x: "0px",
  1395. y: "0px",
  1396. viewBox: "0 0 122.881 122.88",
  1397. "enable-background": "new 0 0 122.881 122.88",
  1398. ...props
  1399. },
  1400. 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" }))
  1401. );
  1402. };
  1403. var SettingsButton = (props) => {
  1404. return React.createElement(IconButton, { ...props }, React.createElement(
  1405. Svg,
  1406. {
  1407. fill: "none",
  1408. stroke: "currentColor",
  1409. strokeLinecap: "round",
  1410. strokeLinejoin: "round",
  1411. strokeWidth: 2,
  1412. viewBox: "0 0 24 24",
  1413. height: "40px",
  1414. width: "40px"
  1415. },
  1416. 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" }),
  1417. 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" })
  1418. ));
  1419. };
  1420. var defaultScrollbar = {
  1421. "scrollbarWidth": "initial",
  1422. "scrollbarColor": "initial",
  1423. "&::-webkit-scrollbar": { all: "initial" },
  1424. "&::-webkit-scrollbar-thumb": {
  1425. all: "initial",
  1426. background: "#00000088"
  1427. },
  1428. "&::-webkit-scrollbar-track": { all: "initial" }
  1429. };
  1430. var Container = styled("div", {
  1431. position: "relative",
  1432. height: "100%",
  1433. overflow: "hidden",
  1434. userSelect: "none",
  1435. fontFamily: "Pretendard, NanumGothic, sans-serif",
  1436. fontSize: "16px",
  1437. color: "black",
  1438. variants: {
  1439. immersive: {
  1440. true: {
  1441. position: "fixed",
  1442. top: 0,
  1443. bottom: 0,
  1444. left: 0,
  1445. right: 0
  1446. }
  1447. }
  1448. }
  1449. });
  1450. var ScrollableLayout = styled("div", {
  1451. position: "relative",
  1452. width: "100%",
  1453. height: "100%",
  1454. display: "flex",
  1455. justifyContent: "center",
  1456. alignItems: "center",
  1457. flexFlow: "row-reverse wrap",
  1458. overflowY: "auto",
  1459. outline: "none",
  1460. ...defaultScrollbar,
  1461. variants: {
  1462. fullscreen: {
  1463. true: {
  1464. position: "fixed",
  1465. top: 0,
  1466. bottom: 0,
  1467. overflow: "auto"
  1468. }
  1469. },
  1470. ltr: {
  1471. true: {
  1472. flexFlow: "row wrap"
  1473. }
  1474. },
  1475. dark: {
  1476. true: {
  1477. "&::-webkit-scrollbar-thumb": {
  1478. all: "initial",
  1479. background: "#ffffff88"
  1480. }
  1481. }
  1482. }
  1483. }
  1484. });
  1485. function useDefault({ enable, controller }) {
  1486. (0, import_react3.useEffect)(() => {
  1487. if (!controller || !enable) {
  1488. return;
  1489. }
  1490. const { container, elementKeyHandler, globalKeyHandler } = controller;
  1491. const scrollable = container?.firstElementChild;
  1492. addEventListener("keydown", globalKeyHandler);
  1493. container?.addEventListener("keydown", elementKeyHandler);
  1494. scrollable?.addEventListener("keydown", elementKeyHandler);
  1495. return () => {
  1496. scrollable?.removeEventListener("keydown", elementKeyHandler);
  1497. container?.removeEventListener("keydown", elementKeyHandler);
  1498. removeEventListener("keydown", globalKeyHandler);
  1499. };
  1500. }, [controller, enable]);
  1501. }
  1502. var import_jotai3 = require("jotai");
  1503. var Backdrop = styled("div", {
  1504. position: "absolute",
  1505. top: 0,
  1506. left: 0,
  1507. width: "100%",
  1508. height: "100%",
  1509. display: "flex",
  1510. alignItems: "center",
  1511. justifyContent: "center",
  1512. background: "rgba(0, 0, 0, 0.5)",
  1513. transition: "0.2s",
  1514. variants: {
  1515. isOpen: {
  1516. true: {
  1517. opacity: 1,
  1518. pointerEvents: "auto"
  1519. },
  1520. false: {
  1521. opacity: 0,
  1522. pointerEvents: "none"
  1523. }
  1524. }
  1525. }
  1526. });
  1527. var CenterDialog = styled("div", {
  1528. minWidth: "20em",
  1529. minHeight: "20em",
  1530. transition: "0.2s",
  1531. background: "white",
  1532. padding: "20px",
  1533. borderRadius: "10px",
  1534. boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.2)"
  1535. });
  1536. function BackdropDialog({ onClose, ...props }) {
  1537. const [isOpen, setIsOpen] = (0, import_react3.useState)(false);
  1538. const close = async () => {
  1539. setIsOpen(false);
  1540. await timeout(200);
  1541. onClose();
  1542. };
  1543. const closeIfEnter = (event) => {
  1544. if (event.key === "Enter") {
  1545. close();
  1546. event.stopPropagation();
  1547. }
  1548. };
  1549. (0, import_react3.useEffect)(() => {
  1550. setIsOpen(true);
  1551. }, []);
  1552. return React.createElement(Backdrop, { isOpen, onClick: close, onKeyDown: closeIfEnter }, React.createElement(
  1553. CenterDialog,
  1554. {
  1555. onClick: (event) => event.stopPropagation(),
  1556. ...props
  1557. }
  1558. ));
  1559. }
  1560. function HelpTab() {
  1561. const keyBindings = (0, import_jotai.useAtomValue)(keyBindingsAtom);
  1562. const strings = (0, import_jotai.useAtomValue)(i18nAtom);
  1563. 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)))));
  1564. }
  1565. var keyBindingsAtom = (0, import_jotai.atom)((get) => {
  1566. const strings = get(i18nAtom);
  1567. return [
  1568. [
  1569. strings.toggleViewer,
  1570. React.createElement(React.Fragment, null, React.createElement("kbd", null, "i"), ", ", React.createElement("kbd", null, "Enter⏎"), ", ", React.createElement("kbd", null, "NumPad0"))
  1571. ],
  1572. [
  1573. strings.toggleFullscreenSetting,
  1574. React.createElement(React.Fragment, null, React.createElement("kbd", null, "⇧Shift"), "+(", React.createElement("kbd", null, "i"), ", ", React.createElement("kbd", null, "Enter⏎"), ", ", React.createElement("kbd", null, "NumPad0"), ")")
  1575. ],
  1576. [strings.nextPage, React.createElement("kbd", null, "j")],
  1577. [strings.previousPage, React.createElement("kbd", null, "k")],
  1578. [strings.download, React.createElement("kbd", null, ";")],
  1579. [strings.refresh, React.createElement("kbd", null, "'")],
  1580. [strings.increaseSinglePageCount, React.createElement("kbd", null, "/")],
  1581. [strings.decreaseSinglePageCount, React.createElement("kbd", null, "?")]
  1582. ];
  1583. });
  1584. var ActionName = styled("td", {
  1585. width: "50%"
  1586. });
  1587. function SettingsTab() {
  1588. const [maxZoomOutExponent, setMaxZoomOutExponent] = (0, import_jotai.useAtom)(maxZoomOutExponentAtom);
  1589. const [maxZoomInExponent, setMaxZoomInExponent] = (0, import_jotai.useAtom)(maxZoomInExponentAtom);
  1590. const [singlePageCount, setSinglePageCount] = (0, import_jotai.useAtom)(singlePageCountAtom);
  1591. const [backgroundColor, setBackgroundColor] = (0, import_jotai.useAtom)(backgroundColorAtom);
  1592. const [pageDirection, setPageDirection] = (0, import_jotai.useAtom)(pageDirectionAtom);
  1593. const [isFullscreenPreferred, setIsFullscreenPreferred] = (0, import_jotai.useAtom)(
  1594. isFullscreenPreferredSettingsAtom
  1595. );
  1596. const setManualPreferences = (0, import_jotai.useSetAtom)(manualPreferencesAtom);
  1597. const zoomOutExponentInputId = (0, import_react3.useId)();
  1598. const zoomInExponentInputId = (0, import_react3.useId)();
  1599. const singlePageCountInputId = (0, import_react3.useId)();
  1600. const colorInputId = (0, import_react3.useId)();
  1601. const pageDirectionInputId = (0, import_react3.useId)();
  1602. const fullscreenInputId = (0, import_react3.useId)();
  1603. const strings = (0, import_jotai.useAtomValue)(i18nAtom);
  1604. const [isResetConfirming, setResetConfirming] = (0, import_react3.useState)(false);
  1605. const maxZoomOut = formatMultiplier(maxZoomOutExponent);
  1606. const maxZoomIn = formatMultiplier(maxZoomInExponent);
  1607. function tryReset() {
  1608. if (isResetConfirming) {
  1609. setManualPreferences({});
  1610. setResetConfirming(false);
  1611. } else {
  1612. setResetConfirming(true);
  1613. }
  1614. }
  1615. return React.createElement(ConfigSheet, null, React.createElement(ConfigRow, null, React.createElement(ConfigLabel, { htmlFor: zoomOutExponentInputId }, strings.maxZoomOut, ": ", maxZoomOut), React.createElement(
  1616. "input",
  1617. {
  1618. type: "number",
  1619. min: 0,
  1620. step: 0.1,
  1621. id: zoomOutExponentInputId,
  1622. value: maxZoomOutExponent,
  1623. onChange: (event) => {
  1624. setMaxZoomOutExponent(event.currentTarget.valueAsNumber || 0);
  1625. }
  1626. }
  1627. )), React.createElement(ConfigRow, null, React.createElement(ConfigLabel, { htmlFor: zoomInExponentInputId }, strings.maxZoomIn, ": ", maxZoomIn), React.createElement(
  1628. "input",
  1629. {
  1630. type: "number",
  1631. min: 0,
  1632. step: 0.1,
  1633. id: zoomInExponentInputId,
  1634. value: maxZoomInExponent,
  1635. onChange: (event) => {
  1636. setMaxZoomInExponent(event.currentTarget.valueAsNumber || 0);
  1637. }
  1638. }
  1639. )), React.createElement(ConfigRow, null, React.createElement(ConfigLabel, { htmlFor: singlePageCountInputId }, strings.singlePageCount), React.createElement(
  1640. "input",
  1641. {
  1642. type: "number",
  1643. min: 0,
  1644. step: 1,
  1645. id: singlePageCountInputId,
  1646. value: singlePageCount,
  1647. onChange: (event) => {
  1648. setSinglePageCount(event.currentTarget.valueAsNumber || 0);
  1649. }
  1650. }
  1651. )), React.createElement(ConfigRow, null, React.createElement(ConfigLabel, { htmlFor: colorInputId }, strings.backgroundColor), React.createElement(
  1652. ColorInput,
  1653. {
  1654. type: "color",
  1655. id: colorInputId,
  1656. value: backgroundColor,
  1657. onChange: (event) => {
  1658. setBackgroundColor(event.currentTarget.value);
  1659. }
  1660. }
  1661. )), React.createElement(ConfigRow, null, React.createElement("p", null, strings.useFullScreen), React.createElement(Toggle, null, React.createElement(
  1662. HiddenInput,
  1663. {
  1664. type: "checkbox",
  1665. id: fullscreenInputId,
  1666. checked: isFullscreenPreferred,
  1667. onChange: (event) => {
  1668. setIsFullscreenPreferred(event.currentTarget.checked);
  1669. }
  1670. }
  1671. ), React.createElement("label", { htmlFor: fullscreenInputId }, strings.useFullScreen))), React.createElement(ConfigRow, null, React.createElement("p", null, strings.leftToRight), React.createElement(Toggle, null, React.createElement(
  1672. HiddenInput,
  1673. {
  1674. type: "checkbox",
  1675. id: pageDirectionInputId,
  1676. checked: pageDirection === "leftToRight",
  1677. onChange: (event) => {
  1678. setPageDirection(event.currentTarget.checked ? "leftToRight" : "rightToLeft");
  1679. }
  1680. }
  1681. ), React.createElement("label", { htmlFor: pageDirectionInputId }, strings.leftToRight))), React.createElement(ResetButton, { onClick: tryReset }, isResetConfirming ? strings.doYouReallyWantToReset : strings.reset));
  1682. }
  1683. function formatMultiplier(maxZoomOutExponent) {
  1684. return Math.sqrt(2) ** maxZoomOutExponent === Infinity ? "∞" : `${(Math.sqrt(2) ** maxZoomOutExponent).toPrecision(2)}x`;
  1685. }
  1686. var ConfigLabel = styled("label", {
  1687. margin: 0
  1688. });
  1689. var ResetButton = styled("button", {
  1690. padding: "0.2em 0.5em",
  1691. background: "none",
  1692. border: "red 1px solid",
  1693. borderRadius: "0.2em",
  1694. color: "red",
  1695. cursor: "pointer",
  1696. transition: "0.3s",
  1697. "&:hover": {
  1698. background: "#ffe0e0"
  1699. }
  1700. });
  1701. var ColorInput = styled("input", {
  1702. height: "1.5em"
  1703. });
  1704. var ConfigRow = styled("div", {
  1705. display: "flex",
  1706. alignItems: "center",
  1707. justifyContent: "space-between",
  1708. gap: "10%",
  1709. "&& > *": {
  1710. fontSize: "1em",
  1711. fontWeight: "medium",
  1712. minWidth: 0
  1713. },
  1714. "& > input": {
  1715. appearance: "meter",
  1716. border: "gray 1px solid",
  1717. borderRadius: "0.2em",
  1718. textAlign: "center"
  1719. },
  1720. ":first-child": {
  1721. flex: "2 1 0"
  1722. },
  1723. ":nth-child(2)": {
  1724. flex: "1 1 0"
  1725. }
  1726. });
  1727. var HiddenInput = styled("input", {
  1728. opacity: 0,
  1729. width: 0,
  1730. height: 0
  1731. });
  1732. var Toggle = styled("span", {
  1733. "--width": "60px",
  1734. "label": {
  1735. position: "relative",
  1736. display: "inline-flex",
  1737. margin: 0,
  1738. width: "var(--width)",
  1739. height: "calc(var(--width) / 2)",
  1740. borderRadius: "calc(var(--width) / 2)",
  1741. cursor: "pointer",
  1742. textIndent: "-9999px",
  1743. background: "grey"
  1744. },
  1745. "label:after": {
  1746. position: "absolute",
  1747. top: "calc(var(--width) * 0.025)",
  1748. left: "calc(var(--width) * 0.025)",
  1749. width: "calc(var(--width) * 0.45)",
  1750. height: "calc(var(--width) * 0.45)",
  1751. borderRadius: "calc(var(--width) * 0.45)",
  1752. content: "",
  1753. background: "#fff",
  1754. transition: "0.3s"
  1755. },
  1756. "input:checked + label": {
  1757. background: "#bada55"
  1758. },
  1759. "input:checked + label:after": {
  1760. left: "calc(var(--width) * 0.975)",
  1761. transform: "translateX(-100%)"
  1762. },
  1763. "label:active:after": {
  1764. width: "calc(var(--width) * 0.65)"
  1765. }
  1766. });
  1767. var ConfigSheet = styled("div", {
  1768. display: "flex",
  1769. flexFlow: "column nowrap",
  1770. alignItems: "stretch",
  1771. gap: "0.8em"
  1772. });
  1773. function ViewerDialog({ onClose }) {
  1774. const strings = (0, import_jotai.useAtomValue)(i18nAtom);
  1775. 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)))));
  1776. }
  1777. var PlainTab = styled("button", {
  1778. flex: 1,
  1779. padding: "0.5em 1em",
  1780. background: "transparent",
  1781. border: "none",
  1782. borderRadius: "0.5em",
  1783. color: "#888",
  1784. cursor: "pointer",
  1785. fontSize: "1.2em",
  1786. fontWeight: "bold",
  1787. textAlign: "center",
  1788. '&[data-headlessui-state="selected"]': {
  1789. border: "1px solid black",
  1790. color: "black"
  1791. },
  1792. "&:hover": {
  1793. color: "black"
  1794. }
  1795. });
  1796. var TabList = styled("div", {
  1797. display: "flex",
  1798. flexFlow: "row nowrap",
  1799. gap: "0.5em"
  1800. });
  1801. var TabPanels = styled("div", {
  1802. marginTop: "1em"
  1803. });
  1804. var LeftBottomFloat = styled("div", {
  1805. position: "absolute",
  1806. bottom: "1%",
  1807. left: "1%",
  1808. display: "flex",
  1809. flexFlow: "column"
  1810. });
  1811. var MenuActions = styled("div", {
  1812. display: "flex",
  1813. flexFlow: "column nowrap",
  1814. alignItems: "center",
  1815. gap: "16px"
  1816. });
  1817. function LeftBottomControl() {
  1818. const downloadAndSave = (0, import_jotai.useSetAtom)(downloadAndSaveAtom);
  1819. const [isOpen, setIsOpen] = (0, import_react3.useState)(false);
  1820. const scrollable = (0, import_jotai3.useAtomValue)(scrollElementAtom);
  1821. const closeDialog = () => {
  1822. setIsOpen(false);
  1823. scrollable?.focus();
  1824. };
  1825. return React.createElement(React.Fragment, null, React.createElement(LeftBottomFloat, null, React.createElement(MenuActions, null, React.createElement(SettingsButton, { onClick: () => setIsOpen((value) => !value) }), React.createElement(DownloadButton, { onClick: () => downloadAndSave() }))), isOpen && React.createElement(ViewerDialog, { onClose: closeDialog }));
  1826. }
  1827. var stretch = keyframes({
  1828. "0%": {
  1829. top: "8px",
  1830. height: "64px"
  1831. },
  1832. "50%": {
  1833. top: "24px",
  1834. height: "32px"
  1835. },
  1836. "100%": {
  1837. top: "24px",
  1838. height: "32px"
  1839. }
  1840. });
  1841. var SpinnerContainer = styled("div", {
  1842. position: "absolute",
  1843. left: "0",
  1844. top: "0",
  1845. right: "0",
  1846. bottom: "0",
  1847. margin: "auto",
  1848. display: "flex",
  1849. justifyContent: "center",
  1850. alignItems: "center",
  1851. div: {
  1852. display: "inline-block",
  1853. width: "16px",
  1854. margin: "0 4px",
  1855. background: "#fff",
  1856. animation: `${stretch} 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite`
  1857. },
  1858. "div:nth-child(1)": {
  1859. "animation-delay": "-0.24s"
  1860. },
  1861. "div:nth-child(2)": {
  1862. "animation-delay": "-0.12s"
  1863. },
  1864. "div:nth-child(3)": {
  1865. "animation-delay": "0"
  1866. }
  1867. });
  1868. var Spinner = () => React.createElement(SpinnerContainer, null, React.createElement("div", null), React.createElement("div", null), React.createElement("div", null));
  1869. var Overlay = styled("div", {
  1870. position: "relative",
  1871. maxWidth: "100%",
  1872. height: "100%",
  1873. display: "flex",
  1874. alignItems: "center",
  1875. justifyContent: "center",
  1876. "@media print": {
  1877. margin: 0
  1878. },
  1879. variants: {
  1880. placeholder: {
  1881. true: { width: "45%", height: "100%" }
  1882. },
  1883. fullWidth: {
  1884. true: { width: "100%" }
  1885. },
  1886. originalSize: {
  1887. true: {
  1888. minHeight: "100%",
  1889. height: "auto"
  1890. }
  1891. }
  1892. }
  1893. });
  1894. var LinkColumn = styled("div", {
  1895. display: "flex",
  1896. flexFlow: "column nowrap",
  1897. alignItems: "center",
  1898. justifyContent: "center",
  1899. cursor: "pointer",
  1900. boxShadow: "1px 1px 3px",
  1901. padding: "1rem 1.5rem",
  1902. transition: "box-shadow 1s easeOutExpo",
  1903. lineBreak: "anywhere",
  1904. "&:hover": {
  1905. boxShadow: "2px 2px 5px"
  1906. },
  1907. "&:active": {
  1908. boxShadow: "0 0 2px"
  1909. }
  1910. });
  1911. var Image = styled("img", {
  1912. position: "relative",
  1913. height: "100%",
  1914. maxWidth: "100%",
  1915. objectFit: "contain",
  1916. variants: {
  1917. originalSize: {
  1918. true: { height: "auto" }
  1919. }
  1920. }
  1921. });
  1922. var Page = ({ atom: atom3, ...props }) => {
  1923. const { imageProps, fullWidth, reloadAtom, shouldBeOriginalSize, state: pageState, setDiv } = (0, import_jotai.useAtomValue)(atom3);
  1924. const strings = (0, import_jotai.useAtomValue)(i18nAtom);
  1925. const reload = (0, import_jotai.useSetAtom)(reloadAtom);
  1926. const { status } = pageState;
  1927. const reloadErrored = async (event) => {
  1928. event.stopPropagation();
  1929. await reload();
  1930. };
  1931. return React.createElement(
  1932. Overlay,
  1933. {
  1934. ref: setDiv,
  1935. placeholder: status !== "complete",
  1936. originalSize: shouldBeOriginalSize,
  1937. fullWidth
  1938. },
  1939. status === "loading" && React.createElement(Spinner, null),
  1940. 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"))),
  1941. React.createElement(Image, { ...imageProps, originalSize: shouldBeOriginalSize, ...props })
  1942. );
  1943. };
  1944. function InnerViewer(props) {
  1945. const { options: viewerOptions, onInitialized, ...otherProps } = props;
  1946. const isFullscreen = (0, import_jotai.useAtomValue)(viewerFullscreenAtom);
  1947. const backgroundColor = (0, import_jotai.useAtomValue)(backgroundColorAtom);
  1948. const viewer = (0, import_jotai.useAtomValue)(viewerStateAtom);
  1949. const setViewerOptions = (0, import_jotai.useSetAtom)(setViewerOptionsAtom);
  1950. const pageDirection = (0, import_jotai.useAtomValue)(pageDirectionAtom);
  1951. const strings = (0, import_jotai.useAtomValue)(i18nAtom);
  1952. const mode = (0, import_jotai.useAtomValue)(viewerModeAtom);
  1953. (0, import_jotai.useAtomValue)(fullscreenSynchronizationAtom);
  1954. const { status } = viewer;
  1955. const controller = (0, import_jotai.useAtomValue)(controllerCreationAtom);
  1956. const options = controller?.options;
  1957. useDefault({ enable: !options?.noDefaultBinding, controller });
  1958. (0, import_react3.useEffect)(() => {
  1959. if (controller) {
  1960. onInitialized?.(controller);
  1961. }
  1962. }, [controller, onInitialized]);
  1963. (0, import_react3.useEffect)(() => {
  1964. setViewerOptions(viewerOptions);
  1965. }, [viewerOptions]);
  1966.  
  1967. return React.createElement(
  1968. Container,
  1969. {
  1970. ref: (0, import_jotai.useSetAtom)(setViewerElementAtom),
  1971. css: { backgroundColor },
  1972. immersive: mode === "window"
  1973. },
  1974. React.createElement(
  1975. ScrollableLayout,
  1976. {
  1977. tabIndex: 0,
  1978. ref: (0, import_jotai.useSetAtom)(setScrollElementAtom),
  1979. dark: isDarkColor(backgroundColor),
  1980. fullscreen: isFullscreen,
  1981. ltr: pageDirection === "leftToRight",
  1982. onWheel: (0, import_jotai.useSetAtom)(wheelAtom),
  1983. onScroll: (0, import_jotai.useSetAtom)(synchronizeScrollAtom),
  1984. onClick: (0, import_jotai.useSetAtom)(navigateAtom),
  1985. onMouseDown: (0, import_jotai.useSetAtom)(blockSelectionAtom),
  1986. children: status === "complete" ? viewer.pages.map((atom3) => React.createElement(
  1987. Page,
  1988. {
  1989. key: `${atom3}`,
  1990. atom: atom3,
  1991. ...options?.imageProps
  1992. }
  1993. )) : React.createElement("p", null, status === "error" ? strings.errorIsOccurred : strings.loading),
  1994. ...otherProps
  1995. }
  1996. ),
  1997. status === "complete" ? React.createElement(LeftBottomControl, null) : false,
  1998. React.createElement(FullscreenButton, { onClick: (0, import_jotai.useSetAtom)(toggleImmersiveAtom) }),
  1999. React.createElement(import_react_toastify.ToastContainer, null)
  2000. );
  2001. }
  2002. function isDarkColor(rgbColor) {
  2003. const match = rgbColor.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
  2004. if (!match) {
  2005. return false;
  2006. }
  2007. const [_, r, g, b] = match.map((x) => parseInt(x, 16));
  2008. const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
  2009. return luminance < 0.5;
  2010. }
  2011. var types_exports = {};
  2012. function initialize(options) {
  2013. const store = (0, import_jotai.createStore)();
  2014. const root = (0, import_react_dom.createRoot)(getDefaultRoot());
  2015. const deferredController = deferred();
  2016. root.render(
  2017. React.createElement(import_jotai.Provider, { store }, React.createElement(InnerViewer, { onInitialized: deferredController.resolve, options }))
  2018. );
  2019. store.set(rootAtom, root);
  2020. return deferredController;
  2021. }
  2022. var Viewer = (0, import_react3.forwardRef)(({ options, onInitialized }) => {
  2023. const store = (0, import_react3.useMemo)(import_jotai.createStore, []);
  2024. return React.createElement(import_jotai.Provider, { store }, React.createElement(InnerViewer, { options, onInitialized }));
  2025. });
  2026. function getDefaultRoot() {
  2027. const div = document.createElement("div");
  2028. div.setAttribute("style", "width: 0; height: 0; z-index: 9999999; position: fixed;");
  2029. document.body.append(div);
  2030. return div;
  2031. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址