vim comic viewer

Universal comic reader

当前为 2023-10-30 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/417893/1272833/vim%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 12.0.1
  7. // @namespace https://greasyfork.org/en/users/713014-nanikit
  8. // @exclude *
  9. // @match http://unused-field.space/
  10. // @author nanikit
  11. // @license MIT
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_xmlhttpRequest
  15. // @grant unsafeWindow
  16. // @resource react-toastify-css https://cdn.jsdelivr.net/npm/react-toastify@9.1.3/dist/ReactToastify.css
  17. // @resource link:clsx https://cdn.jsdelivr.net/npm/clsx@2.0.0/dist/clsx.js
  18. // @resource link:@stitches/react https://cdn.jsdelivr.net/npm/@stitches/react@1.3.1-1/dist/index.cjs
  19. // @resource link:fflate https://cdn.jsdelivr.net/npm/fflate@0.8.1/lib/browser.cjs
  20. // @resource link:jotai https://cdn.jsdelivr.net/npm/jotai@2.4.2/index.js
  21. // @resource link:jotai/react https://cdn.jsdelivr.net/npm/jotai@2.4.2/react.js
  22. // @resource link:jotai/react/utils https://cdn.jsdelivr.net/npm/jotai@2.4.2/react/utils.js
  23. // @resource link:jotai/utils https://cdn.jsdelivr.net/npm/jotai@2.4.2/utils.js
  24. // @resource link:jotai/vanilla https://cdn.jsdelivr.net/npm/jotai@2.4.2/vanilla.js
  25. // @resource link:jotai/vanilla/utils https://cdn.jsdelivr.net/npm/jotai@2.4.2/vanilla/utils.js
  26. // @resource link:react https://cdn.jsdelivr.net/npm/react@18.2.0/cjs/react.production.min.js
  27. // @resource link:react-dom https://cdn.jsdelivr.net/npm/react-dom@18.2.0/cjs/react-dom.production.min.js
  28. // @resource link:react-toastify https://cdn.jsdelivr.net/npm/react-toastify@9.1.3/dist/react-toastify.js
  29. // @resource link:scheduler https://cdn.jsdelivr.net/npm/scheduler@0.23.0/cjs/scheduler.production.min.js
  30. // @resource link:vcv-inject-node-env data:,unsafeWindow.process=%7Benv:%7BNODE_ENV:%22production%22%7D%7D
  31. // ==/UserScript==
  32. "use strict";
  33.  
  34. var __create = Object.create;
  35. var __defProp = Object.defineProperty;
  36. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  37. var __getOwnPropNames = Object.getOwnPropertyNames;
  38. var __getProtoOf = Object.getPrototypeOf;
  39. var __hasOwnProp = Object.prototype.hasOwnProperty;
  40. var __export = (target, all) => {
  41. for (var name in all)
  42. __defProp(target, name, { get: all[name], enumerable: true });
  43. };
  44. var __copyProps = (to, from, except, desc) => {
  45. if (from && typeof from === "object" || typeof from === "function") {
  46. for (let key of __getOwnPropNames(from))
  47. if (!__hasOwnProp.call(to, key) && key !== except)
  48. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  49. }
  50. return to;
  51. };
  52. var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
  53. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  54. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  55. mod
  56. ));
  57. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  58. var mod_exports = {};
  59. __export(mod_exports, {
  60. Viewer: () => Viewer,
  61. download: () => download,
  62. initialize: () => initialize,
  63. types: () => types_exports,
  64. utils: () => utils_exports
  65. });
  66. module.exports = __toCommonJS(mod_exports);
  67. var React = __toESM(require("react"));
  68. var import_vcv_inject_node_env = require("vcv-inject-node-env");
  69. var deps_exports = {};
  70. __export(deps_exports, {
  71. Fragment: () => import_react2.Fragment,
  72. Provider: () => import_jotai.Provider,
  73. RESET: () => import_utils2.RESET,
  74. ToastContainer: () => import_react_toastify.ToastContainer,
  75. atom: () => import_jotai.atom,
  76. atomWithStorage: () => import_utils2.atomWithStorage,
  77. createJSONStorage: () => import_utils2.createJSONStorage,
  78. createRef: () => import_react2.createRef,
  79. createStitches: () => import_react.createStitches,
  80. createStore: () => import_jotai.createStore,
  81. deferred: () => deferred,
  82. forwardRef: () => import_react2.forwardRef,
  83. selectAtom: () => import_utils2.selectAtom,
  84. toast: () => import_react_toastify.toast,
  85. useAtom: () => import_jotai.useAtom,
  86. useAtomValue: () => import_jotai.useAtomValue,
  87. useCallback: () => import_react2.useCallback,
  88. useEffect: () => import_react2.useEffect,
  89. useId: () => import_react2.useId,
  90. useImperativeHandle: () => import_react2.useImperativeHandle,
  91. useLayoutEffect: () => import_react2.useLayoutEffect,
  92. useMemo: () => import_react2.useMemo,
  93. useReducer: () => import_react2.useReducer,
  94. useRef: () => import_react2.useRef,
  95. useSetAtom: () => import_jotai.useSetAtom,
  96. useState: () => import_react2.useState,
  97. useStore: () => import_jotai.useStore
  98. });
  99. var import_react = require("@stitches/react");
  100. __reExport(deps_exports, require("fflate"));
  101. function deferred() {
  102. let methods;
  103. let state = "pending";
  104. const promise = new Promise((resolve, reject) => {
  105. methods = {
  106. async resolve(value) {
  107. await value;
  108. state = "fulfilled";
  109. resolve(value);
  110. },
  111. reject(reason) {
  112. state = "rejected";
  113. reject(reason);
  114. }
  115. };
  116. });
  117. Object.defineProperty(promise, "state", { get: () => state });
  118. return Object.assign(promise, methods);
  119. }
  120. var import_jotai = require("jotai");
  121. var import_utils2 = require("jotai/utils");
  122. var import_react_toastify = require("react-toastify");
  123. var utils_exports = {};
  124. __export(utils_exports, {
  125. getSafeFileName: () => getSafeFileName,
  126. insertCss: () => insertCss,
  127. isTyping: () => isTyping,
  128. save: () => save,
  129. saveAs: () => saveAs,
  130. timeout: () => timeout,
  131. waitDomContent: () => waitDomContent
  132. });
  133. var timeout = (millisecond) => new Promise((resolve) => setTimeout(resolve, millisecond));
  134. var waitDomContent = (document2) => document2.readyState === "loading" ? new Promise((r) => document2.addEventListener("readystatechange", r, { once: true })) : true;
  135. var insertCss = (css2) => {
  136. const style = document.createElement("style");
  137. style.innerHTML = css2;
  138. document.head.append(style);
  139. };
  140. var isTyping = (event) => event.target?.tagName?.match?.(/INPUT|TEXTAREA/) || event.target?.isContentEditable;
  141. var saveAs = async (blob, name) => {
  142. const a = document.createElement("a");
  143. a.download = name;
  144. a.rel = "noopener";
  145. a.href = URL.createObjectURL(blob);
  146. a.click();
  147. await timeout(4e4);
  148. URL.revokeObjectURL(a.href);
  149. };
  150. var getSafeFileName = (str) => {
  151. return str.replace(/[<>:"/\\|?*\x00-\x1f]+/gi, "").trim() || "download";
  152. };
  153. var save = (blob) => {
  154. return saveAs(blob, `${getSafeFileName(document.title)}.zip`);
  155. };
  156. insertCss(GM_getResourceText("react-toastify-css"));
  157. var import_react2 = require("react");
  158. __reExport(deps_exports, require("react-dom"));
  159. var globalCss = document.createElement("style");
  160. globalCss.innerHTML = `html, body {
  161. overflow: hidden;
  162. }`;
  163. function showBodyScrollbar(doShow) {
  164. if (doShow) {
  165. globalCss.remove();
  166. } else {
  167. document.head.append(globalCss);
  168. }
  169. }
  170. async function setFullscreenElement(element) {
  171. if (element) {
  172. await element.requestFullscreen?.();
  173. } else {
  174. await document.exitFullscreen?.();
  175. }
  176. }
  177. var gmStorage = {
  178. getItem: GM_getValue,
  179. setItem: GM_setValue,
  180. removeItem: (key) => GM_deleteValue(key)
  181. };
  182. function atomWithGmValue(key, defaultValue) {
  183. return (0, import_utils2.atomWithStorage)(key, GM_getValue(key, defaultValue), gmStorage);
  184. }
  185. var jsonSessionStorage = (0, import_utils2.createJSONStorage)(() => sessionStorage);
  186. function atomWithSession(key, defaultValue) {
  187. const atom2 = (0, import_utils2.atomWithStorage)(
  188. key,
  189. jsonSessionStorage.getItem(key, defaultValue),
  190. jsonSessionStorage
  191. );
  192. return atom2;
  193. }
  194. var backgroundColorAtom = atomWithGmValue("vim_comic_viewer.background_color", "#eeeeee");
  195. var compactWidthIndexAtom = atomWithGmValue("vim_comic_viewer.single_page_count", 1);
  196. var maxZoomOutExponentAtom = atomWithGmValue("vim_comic_viewer.max_zoom_out_exponent", 3);
  197. var maxZoomInExponentAtom = atomWithGmValue("vim_comic_viewer.max_zoom_in_exponent", 3);
  198. var pageDirectionAtom = atomWithGmValue(
  199. "vim_comic_viewer.page_direction",
  200. "rightToLeft"
  201. );
  202. var isFullscreenPreferredAtom = atomWithGmValue("vim_comic_viewer.use_full_screen", true);
  203. var fullscreenNoticeCountAtom = atomWithGmValue(
  204. "vim_comic_viewer.full_screen_notice_count",
  205. 0
  206. );
  207. var isImmersiveAtom = atomWithSession("vim_comic_viewer.is_immersive", false);
  208. var fullscreenElementStateAtom = (0, import_jotai.atom)(
  209. document.fullscreenElement ?? null
  210. );
  211. var viewerElementStateAtom = (0, import_jotai.atom)(null);
  212. var beforeUnloadStateAtom = (0, import_jotai.atom)(false);
  213. var beforeUnloadAtom = (0, import_jotai.atom)(null, async (_get, set) => {
  214. set(beforeUnloadStateAtom, true);
  215. for (let i = 0; i < 5; i++) {
  216. await timeout(100);
  217. }
  218. set(beforeUnloadStateAtom, false);
  219. });
  220. beforeUnloadAtom.onMount = (set) => {
  221. addEventListener("beforeunload", set);
  222. return () => removeEventListener("beforeunload", set);
  223. };
  224. var fullscreenSynchronizationAtom = (0, import_jotai.atom)(
  225. (get) => {
  226. get(beforeUnloadAtom);
  227. return get(fullscreenElementStateAtom);
  228. },
  229. (get, set, element) => {
  230. set(fullscreenElementStateAtom, element);
  231. const isFullscreenPreferred = get(isFullscreenPreferredAtom);
  232. if (!isFullscreenPreferred) {
  233. return;
  234. }
  235. const isFullscreen = get(viewerElementStateAtom) === element;
  236. const wasImmersive = get(cssImmersiveAtom);
  237. const isViewerFullscreenExit = wasImmersive && !isFullscreen;
  238. const isNavigationExit = get(beforeUnloadStateAtom);
  239. if (isViewerFullscreenExit && !isNavigationExit) {
  240. set(cssImmersiveAtom, false);
  241. }
  242. }
  243. );
  244. fullscreenSynchronizationAtom.onMount = (set) => {
  245. const notify = () => set(document.fullscreenElement ?? null);
  246. document.addEventListener("fullscreenchange", notify);
  247. return () => document.removeEventListener("fullscreenchange", notify);
  248. };
  249. var fullscreenElementAtom = (0, import_jotai.atom)(
  250. (get) => get(fullscreenSynchronizationAtom),
  251. async (get, set, element) => {
  252. const fullscreenElement = get(fullscreenSynchronizationAtom);
  253. if (element === fullscreenElement) {
  254. return;
  255. }
  256. await setFullscreenElement(element);
  257. set(fullscreenSynchronizationAtom, element);
  258. }
  259. );
  260. var viewerFullscreenAtom = (0, import_jotai.atom)((get) => {
  261. const fullscreenElement = get(fullscreenElementAtom);
  262. const viewerElement = get(viewerElementStateAtom);
  263. return fullscreenElement === viewerElement;
  264. }, async (get, set, value) => {
  265. const viewer = get(viewerElementStateAtom);
  266. await set(fullscreenElementAtom, value ? viewer : null);
  267. set(doubleScrollBarHideAtom);
  268. });
  269. var doubleScrollBarHideAtom = (0, import_jotai.atom)(null, (get) => {
  270. const shouldRemoveDuplicateScrollBar = !get(viewerFullscreenAtom) && get(isImmersiveAtom);
  271. showBodyScrollbar(!shouldRemoveDuplicateScrollBar);
  272. });
  273. doubleScrollBarHideAtom.onMount = (set) => set();
  274. var cssImmersiveAtom = (0, import_jotai.atom)(
  275. (get) => {
  276. get(doubleScrollBarHideAtom);
  277. return get(isImmersiveAtom);
  278. },
  279. async (get, set, value) => {
  280. set(isImmersiveAtom, value);
  281. set(doubleScrollBarHideAtom);
  282. const isFullscreenPreferred = get(isFullscreenPreferredAtom);
  283. if (isFullscreenPreferred) {
  284. await set(viewerFullscreenAtom, value);
  285. }
  286. if (value) {
  287. get(viewerElementStateAtom)?.focus({ preventScroll: true });
  288. }
  289. }
  290. );
  291. var viewerModeAtom = (0, import_jotai.atom)((get) => {
  292. const isFullscreen = get(viewerFullscreenAtom);
  293. const isImmersive = get(cssImmersiveAtom);
  294. return isFullscreen ? "fullscreen" : isImmersive ? "window" : "normal";
  295. });
  296. var isFullscreenPreferredSettingsAtom = (0, import_jotai.atom)(
  297. (get) => get(isFullscreenPreferredAtom),
  298. (get, set, value) => {
  299. set(isFullscreenPreferredAtom, value);
  300. set(doubleScrollBarHideAtom);
  301. const isImmersive = get(cssImmersiveAtom);
  302. const shouldEnterFullscreen = value && isImmersive;
  303. set(viewerFullscreenAtom, shouldEnterFullscreen);
  304. }
  305. );
  306. var en_default = {
  307. "@@locale": "en",
  308. settings: "Settings",
  309. maxZoomOut: "Maximum zoom out",
  310. maxZoomIn: "Maximum zoom in",
  311. backgroundColor: "Background color",
  312. leftToRight: "Left to right",
  313. errorIsOccurred: "Error is occurred.",
  314. failedToLoadImage: "Failed to load image.",
  315. loading: "Loading...",
  316. fullScreenRestorationGuide: "Enter full screen yourself if you want to keep the viewer open in full screen.",
  317. useFullScreen: "Use full screen"
  318. };
  319. var ko_default = {
  320. "@@locale": "ko",
  321. settings: "\uC124\uC815",
  322. maxZoomOut: "\uCD5C\uB300 \uCD95\uC18C",
  323. maxZoomIn: "\uCD5C\uB300 \uD655\uB300",
  324. backgroundColor: "\uBC30\uACBD\uC0C9",
  325. leftToRight: "\uC67C\uCABD\uBD80\uD130 \uBCF4\uAE30",
  326. errorIsOccurred: "\uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.",
  327. failedToLoadImage: "\uC774\uBBF8\uC9C0\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
  328. loading: "\uB85C\uB529 \uC911...",
  329. fullScreenRestorationGuide: "\uBDF0\uC5B4 \uC804\uCCB4 \uD654\uBA74\uC744 \uC720\uC9C0\uD558\uB824\uBA74 \uC9C1\uC811 \uC804\uCCB4 \uD654\uBA74\uC744 \uCF1C \uC8FC\uC138\uC694 (F11).",
  330. useFullScreen: "\uC804\uCCB4 \uD654\uBA74"
  331. };
  332. var translations = { en: en_default, ko: ko_default };
  333. var i18nStateAtom = (0, import_jotai.atom)(getLanguage());
  334. var i18nAtom = (0, import_jotai.atom)((get) => get(i18nStateAtom), (_get, set) => {
  335. set(i18nStateAtom, getLanguage());
  336. });
  337. i18nAtom.onMount = (set) => {
  338. addEventListener("languagechange", set);
  339. return () => {
  340. removeEventListener("languagechange", set);
  341. };
  342. };
  343. function getLanguage() {
  344. for (const language of navigator.languages) {
  345. const locale = language.split("-")[0];
  346. const translation = translations[locale];
  347. if (translation) {
  348. return translation;
  349. }
  350. }
  351. return en_default;
  352. }
  353. var scrollElementStateAtom = (0, import_jotai.atom)(null);
  354. var initialPageScrollState = { page: null, ratio: 0.5 };
  355. var scrollElementSizeAtom = (0, import_jotai.atom)({ width: 0, height: 0 });
  356. var pageScrollStateAtom = (0, import_jotai.atom)(initialPageScrollState);
  357. var synchronizeScrollAtom = (0, import_jotai.atom)(null, (get, set) => {
  358. const scrollElement = get(scrollElementAtom);
  359. const previous = { ...get(pageScrollStateAtom), ...get(scrollElementSizeAtom) };
  360. const current = getCurrentPage(scrollElement);
  361. const height = scrollElement?.clientHeight ?? 0;
  362. const width = scrollElement?.clientWidth ?? 0;
  363. const isResizing = !current.page || height !== previous.height || width !== previous.width;
  364. if (isResizing) {
  365. set(restoreScrollAtom);
  366. set(scrollElementSizeAtom, (previous2) => {
  367. const isChanged = previous2.width !== width || previous2.height !== height;
  368. return isChanged ? previous2 : { width, height };
  369. });
  370. } else {
  371. set(pageScrollStateAtom, current);
  372. }
  373. });
  374. var restoreScrollAtom = (0, import_jotai.atom)(null, (get) => {
  375. const { page, ratio } = get(pageScrollStateAtom);
  376. const element = get(scrollElementAtom);
  377. if (!element || !page) {
  378. return;
  379. }
  380. const { offsetTop, clientHeight } = page;
  381. const restoredY = Math.floor(offsetTop + clientHeight * ratio - element.clientHeight / 2);
  382. element.scroll({ top: restoredY });
  383. });
  384. var scrollElementAtom = (0, import_jotai.atom)(
  385. (get) => get(scrollElementStateAtom)?.div ?? null,
  386. (_get, set, div) => {
  387. set(scrollElementStateAtom, (previous) => {
  388. if (previous?.div === div) {
  389. return previous;
  390. }
  391. previous?.resizeObserver.disconnect();
  392. if (div === null) {
  393. return null;
  394. }
  395. set(scrollElementSizeAtom, { width: div.clientWidth, height: div.clientHeight });
  396. const resizeObserver = new ResizeObserver(() => {
  397. set(scrollElementSizeAtom, { width: div.clientWidth, height: div.clientHeight });
  398. set(restoreScrollAtom);
  399. });
  400. resizeObserver.observe(div);
  401. return { div, resizeObserver };
  402. });
  403. }
  404. );
  405. scrollElementAtom.onMount = (set) => () => set(null);
  406. var goNextAtom = (0, import_jotai.atom)(null, (get) => {
  407. const scrollElement = get(scrollElementAtom);
  408. const { page } = getCurrentPage(scrollElement);
  409. if (!page) {
  410. return;
  411. }
  412. const viewerHeight = scrollElement.clientHeight;
  413. const ignorableHeight = viewerHeight * 0.05;
  414. const scrollBottom = scrollElement.scrollTop + viewerHeight;
  415. const remainingHeight = page.offsetTop + page.clientHeight - Math.ceil(scrollBottom) - 1;
  416. if (remainingHeight > ignorableHeight) {
  417. const divisor = Math.ceil(remainingHeight / viewerHeight);
  418. const delta = Math.ceil(remainingHeight / divisor);
  419. scrollElement.scroll({ top: Math.floor(scrollElement.scrollTop + delta) });
  420. } else {
  421. scrollToNextPageTopOrEnd(page);
  422. }
  423. });
  424. var goPreviousAtom = (0, import_jotai.atom)(null, (get) => {
  425. const scrollElement = get(scrollElementAtom);
  426. const { page } = getCurrentPage(scrollElement);
  427. if (!page) {
  428. return;
  429. }
  430. const viewerHeight = scrollElement.clientHeight;
  431. const ignorableHeight = viewerHeight * 0.05;
  432. const remainingHeight = scrollElement.scrollTop - Math.ceil(page.offsetTop) - 1;
  433. if (remainingHeight > ignorableHeight) {
  434. const divisor = Math.ceil(remainingHeight / viewerHeight);
  435. const delta = -Math.ceil(remainingHeight / divisor);
  436. scrollElement.scroll({ top: Math.floor(scrollElement.scrollTop + delta) });
  437. } else {
  438. scrollToPreviousPageBottomOrStart(page);
  439. }
  440. });
  441. var navigateAtom = (0, import_jotai.atom)(null, (get, set, event) => {
  442. const height = get(scrollElementAtom)?.clientHeight;
  443. if (!height || event.button !== 0) {
  444. return;
  445. }
  446. event.preventDefault();
  447. const isTop = event.clientY < height / 2;
  448. if (isTop) {
  449. set(goPreviousAtom);
  450. } else {
  451. set(goNextAtom);
  452. }
  453. });
  454. function scrollToNextPageTopOrEnd(page) {
  455. const pageBottom = page.offsetTop + page.clientHeight;
  456. let cursor = page;
  457. while (cursor.nextElementSibling) {
  458. const next = cursor.nextElementSibling;
  459. if (pageBottom < next.offsetTop) {
  460. next.scrollIntoView({ block: "start" });
  461. return;
  462. }
  463. cursor = next;
  464. }
  465. cursor.scrollIntoView({ block: "end" });
  466. }
  467. function scrollToPreviousPageBottomOrStart(page) {
  468. const pageTop = page.offsetTop;
  469. let cursor = page;
  470. while (cursor.previousElementSibling) {
  471. const previous = cursor.previousElementSibling;
  472. const previousBottom = previous.offsetTop + previous.clientHeight;
  473. if (previousBottom < pageTop) {
  474. previous.scrollIntoView({ block: "end" });
  475. return;
  476. }
  477. cursor = previous;
  478. }
  479. cursor.scrollIntoView({ block: "start" });
  480. }
  481. function getCurrentPage(container) {
  482. const clientHeight = container?.clientHeight;
  483. if (!clientHeight) {
  484. return initialPageScrollState;
  485. }
  486. const children = [...container.children];
  487. if (!children.length) {
  488. return initialPageScrollState;
  489. }
  490. const viewportTop = container.scrollTop;
  491. const viewportBottom = viewportTop + container.clientHeight;
  492. const fullyVisiblePages = children.filter(
  493. (x) => x.offsetTop >= viewportTop && x.offsetTop + x.clientHeight <= viewportBottom
  494. );
  495. if (fullyVisiblePages.length) {
  496. return { page: fullyVisiblePages[Math.floor(fullyVisiblePages.length / 2)], ratio: 0.5 };
  497. }
  498. const scrollCenter = (viewportTop + viewportBottom) / 2;
  499. const centerCrossingPage = children.find(
  500. (x) => x.offsetTop <= scrollCenter && x.offsetTop + x.clientHeight >= scrollCenter
  501. );
  502. if (!centerCrossingPage) {
  503. return initialPageScrollState;
  504. }
  505. const ratio = (scrollCenter - centerCrossingPage.offsetTop) / centerCrossingPage.clientHeight;
  506. return { page: centerCrossingPage, ratio };
  507. }
  508. function imageSourceToIterable(source) {
  509. if (typeof source === "string") {
  510. return async function* () {
  511. yield source;
  512. }();
  513. } else if (Array.isArray(source)) {
  514. return async function* () {
  515. for (const url of source) {
  516. yield url;
  517. }
  518. }();
  519. } else {
  520. return source();
  521. }
  522. }
  523. function createPageAtom({ index, source }) {
  524. let imageLoad = deferred();
  525. const stateAtom = (0, import_jotai.atom)({ state: "loading" });
  526. const loadAtom = (0, import_jotai.atom)(null, async (_get, set) => {
  527. const urls = [];
  528. for await (const url of imageSourceToIterable(source)) {
  529. urls.push(url);
  530. imageLoad = deferred();
  531. set(stateAtom, { src: url, state: "loading" });
  532. const result = await imageLoad;
  533. switch (result) {
  534. case false:
  535. continue;
  536. case null:
  537. return;
  538. default: {
  539. const img = result;
  540. set(stateAtom, { src: url, naturalHeight: img.naturalHeight, state: "complete" });
  541. return;
  542. }
  543. }
  544. }
  545. set(stateAtom, { urls, state: "error" });
  546. });
  547. loadAtom.onMount = (set) => {
  548. set();
  549. };
  550. const reloadAtom = (0, import_jotai.atom)(null, async (_get, set) => {
  551. imageLoad.resolve(null);
  552. await set(loadAtom);
  553. });
  554. const imageToViewerSizeRatioAtom = (0, import_jotai.atom)((get) => {
  555. const viewerSize = get(scrollElementSizeAtom);
  556. if (!viewerSize) {
  557. return 1;
  558. }
  559. const state = get(stateAtom);
  560. if (state.state !== "complete") {
  561. return 1;
  562. }
  563. return state.naturalHeight / viewerSize.height;
  564. });
  565. const shouldBeOriginalSizeAtom = (0, import_jotai.atom)((get) => {
  566. const maxZoomInExponent = get(maxZoomInExponentAtom);
  567. const maxZoomOutExponent = get(maxZoomOutExponentAtom);
  568. const imageRatio = get(imageToViewerSizeRatioAtom);
  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. const aggregateAtom = (0, import_jotai.atom)((get) => {
  575. get(loadAtom);
  576. const state = get(stateAtom);
  577. const compactWidthIndex = get(compactWidthIndexAtom);
  578. const shouldBeOriginalSize = get(shouldBeOriginalSizeAtom);
  579. const ratio = get(imageToViewerSizeRatioAtom);
  580. const isLarge = ratio > 1;
  581. const canMessUpRow = shouldBeOriginalSize && isLarge;
  582. return {
  583. state,
  584. reloadAtom,
  585. fullWidth: index < compactWidthIndex || canMessUpRow,
  586. shouldBeOriginalSize,
  587. imageProps: {
  588. ..."src" in state ? { src: state.src } : {},
  589. onError: () => imageLoad.resolve(false),
  590. onLoad: (event) => imageLoad.resolve(event.currentTarget)
  591. }
  592. };
  593. });
  594. return aggregateAtom;
  595. }
  596. var viewerElementAtom = (0, import_jotai.atom)(
  597. (get) => get(viewerElementStateAtom),
  598. async (get, set, element) => {
  599. set(viewerElementStateAtom, element);
  600. const isViewerFullscreen = get(viewerFullscreenAtom);
  601. const isFullscreenPreferred = get(isFullscreenPreferredAtom);
  602. const isImmersive = get(cssImmersiveAtom);
  603. const shouldEnterFullscreen = isFullscreenPreferred && isImmersive;
  604. if (isViewerFullscreen === shouldEnterFullscreen || !element) {
  605. return;
  606. }
  607. const isUserFullscreen = window.innerHeight === screen.height || window.innerWidth === screen.width;
  608. if (isUserFullscreen) {
  609. return;
  610. }
  611. try {
  612. if (shouldEnterFullscreen) {
  613. await set(viewerFullscreenAtom, true);
  614. }
  615. } catch (error) {
  616. if (error?.message === "Permissions check failed") {
  617. if (get(fullscreenNoticeCountAtom) >= 3) {
  618. return;
  619. }
  620. (0, import_react_toastify.toast)(get(i18nAtom).fullScreenRestorationGuide);
  621. await timeout(5e3);
  622. set(fullscreenNoticeCountAtom, (count) => count + 1);
  623. return;
  624. }
  625. throw error;
  626. }
  627. }
  628. );
  629. var viewerStateAtom = (0, import_jotai.atom)({
  630. options: {},
  631. status: "loading"
  632. });
  633. var pagesAtom = (0, import_utils2.selectAtom)(
  634. viewerStateAtom,
  635. (state) => state.pages
  636. );
  637. var setViewerOptionsAtom = (0, import_jotai.atom)(
  638. null,
  639. async (get, set, options) => {
  640. try {
  641. const { source } = options;
  642. if (source === get(viewerStateAtom).options.source) {
  643. return;
  644. }
  645. if (!source) {
  646. set(viewerStateAtom, (state) => ({
  647. ...state,
  648. status: "complete",
  649. images: [],
  650. pages: []
  651. }));
  652. return;
  653. }
  654. set(viewerStateAtom, (state) => ({ ...state, status: "loading" }));
  655. const images = await source();
  656. if (!Array.isArray(images)) {
  657. throw new Error(`Invalid comic source type: ${typeof images}`);
  658. }
  659. set(viewerStateAtom, (state) => ({
  660. ...state,
  661. status: "complete",
  662. images,
  663. pages: images.map((source2, index) => createPageAtom({ source: source2, index }))
  664. }));
  665. } catch (error) {
  666. set(viewerStateAtom, (state) => ({ ...state, status: "error" }));
  667. console.error(error);
  668. throw error;
  669. }
  670. }
  671. );
  672. var reloadErroredAtom = (0, import_jotai.atom)(null, (get, set) => {
  673. window.stop();
  674. const pages = get(pagesAtom);
  675. for (const atom2 of pages ?? []) {
  676. const page = get(atom2);
  677. if (page.state.state !== "complete") {
  678. set(page.reloadAtom);
  679. }
  680. }
  681. });
  682. var toggleImmersiveAtom = (0, import_jotai.atom)(null, async (get, set) => {
  683. await set(cssImmersiveAtom, !get(cssImmersiveAtom));
  684. });
  685. var blockSelectionAtom = (0, import_jotai.atom)(null, (_get, set, event) => {
  686. if (event.detail >= 2) {
  687. event.preventDefault();
  688. }
  689. if (event.buttons === 3) {
  690. set(toggleImmersiveAtom);
  691. event.preventDefault();
  692. }
  693. });
  694. var { styled, css, keyframes } = (0, import_react.createStitches)({});
  695. var Svg = styled("svg", {
  696. opacity: "50%",
  697. filter: "drop-shadow(0 0 1px white) drop-shadow(0 0 1px white)",
  698. color: "black",
  699. cursor: "pointer",
  700. "&:hover": {
  701. opacity: "100%",
  702. transform: "scale(1.1)"
  703. }
  704. });
  705. var downloadCss = { width: "40px" };
  706. var fullscreenCss = {
  707. position: "absolute",
  708. right: "1%",
  709. bottom: "1%",
  710. width: "40px"
  711. };
  712. var DownloadIcon = (props) => React.createElement(
  713. Svg,
  714. {
  715. version: "1.1",
  716. xmlns: "http://www.w3.org/2000/svg",
  717. x: "0px",
  718. y: "0px",
  719. viewBox: "0 -34.51 122.88 122.87",
  720. css: downloadCss,
  721. ...props
  722. },
  723. 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" }))
  724. );
  725. var FullscreenIcon = (props) => React.createElement(
  726. Svg,
  727. {
  728. version: "1.1",
  729. xmlns: "http://www.w3.org/2000/svg",
  730. x: "0px",
  731. y: "0px",
  732. viewBox: "0 0 122.88 122.87",
  733. css: fullscreenCss,
  734. ...props
  735. },
  736. 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" }))
  737. );
  738. var ErrorIcon = styled("svg", {
  739. width: "10vmin",
  740. height: "10vmin",
  741. fill: "hsl(0, 50%, 20%)",
  742. margin: "2rem"
  743. });
  744. var CircledX = (props) => {
  745. return React.createElement(
  746. ErrorIcon,
  747. {
  748. x: "0px",
  749. y: "0px",
  750. viewBox: "0 0 122.881 122.88",
  751. "enable-background": "new 0 0 122.881 122.88",
  752. ...props
  753. },
  754. 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" }))
  755. );
  756. };
  757. var IconSettings = (props) => {
  758. return React.createElement(
  759. Svg,
  760. {
  761. fill: "none",
  762. stroke: "currentColor",
  763. strokeLinecap: "round",
  764. strokeLinejoin: "round",
  765. strokeWidth: 2,
  766. viewBox: "0 0 24 24",
  767. height: "40px",
  768. width: "40px",
  769. ...props
  770. },
  771. 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" }),
  772. 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" })
  773. );
  774. };
  775. var defaultScrollbar = {
  776. "scrollbarWidth": "initial",
  777. "scrollbarColor": "initial",
  778. "&::-webkit-scrollbar": { all: "initial" },
  779. "&::-webkit-scrollbar-thumb": {
  780. all: "initial",
  781. background: "#00000088"
  782. },
  783. "&::-webkit-scrollbar-track": { all: "initial" }
  784. };
  785. var Container = styled("div", {
  786. position: "relative",
  787. height: "100%",
  788. overflow: "hidden",
  789. userSelect: "none",
  790. fontFamily: "Pretendard, NanumGothic, sans-serif",
  791. fontSize: "1vmin",
  792. color: "black",
  793. "&:focus-visible": {
  794. outline: "none"
  795. },
  796. variants: {
  797. immersive: {
  798. true: {
  799. position: "fixed",
  800. top: 0,
  801. bottom: 0,
  802. left: 0,
  803. right: 0,
  804. zIndex: 9999999
  805. }
  806. }
  807. }
  808. });
  809. var ScrollableLayout = styled("div", {
  810. outline: 0,
  811. position: "relative",
  812. width: "100%",
  813. height: "100%",
  814. display: "flex",
  815. justifyContent: "center",
  816. alignItems: "center",
  817. flexFlow: "row-reverse wrap",
  818. overflowY: "auto",
  819. gap: "1px",
  820. ...defaultScrollbar,
  821. variants: {
  822. fullscreen: {
  823. true: {
  824. position: "fixed",
  825. top: 0,
  826. bottom: 0,
  827. overflow: "auto"
  828. }
  829. },
  830. ltr: {
  831. true: {
  832. flexFlow: "row wrap"
  833. }
  834. },
  835. dark: {
  836. true: {
  837. "&::-webkit-scrollbar-thumb": {
  838. all: "initial",
  839. background: "#ffffff88"
  840. }
  841. }
  842. }
  843. }
  844. });
  845. function useDefault({ enable, controller }) {
  846. const defaultKeyHandler = async (event) => {
  847. if (maybeNotHotkey(event)) {
  848. return;
  849. }
  850. switch (event.key) {
  851. case "j":
  852. case "ArrowDown":
  853. controller.goNext();
  854. break;
  855. case "k":
  856. case "ArrowUp":
  857. controller.goPrevious();
  858. break;
  859. case ";":
  860. await controller.downloader?.downloadAndSave();
  861. break;
  862. case "/":
  863. controller.compactWidthIndex++;
  864. break;
  865. case "?":
  866. controller.compactWidthIndex--;
  867. break;
  868. case "'":
  869. controller.reloadErrored();
  870. break;
  871. default:
  872. return;
  873. }
  874. event.stopPropagation();
  875. };
  876. const defaultGlobalKeyHandler = (event) => {
  877. if (maybeNotHotkey(event)) {
  878. return;
  879. }
  880. if (["KeyI", "Numpad0", "Enter"].includes(event.code)) {
  881. controller.toggleFullscreen();
  882. }
  883. };
  884. (0, import_react2.useEffect)(() => {
  885. if (!controller || !enable) {
  886. return;
  887. }
  888. controller.container?.addEventListener("keydown", defaultKeyHandler);
  889. addEventListener("keydown", defaultGlobalKeyHandler);
  890. return () => {
  891. controller.container?.removeEventListener("keydown", defaultKeyHandler);
  892. removeEventListener("keydown", defaultGlobalKeyHandler);
  893. };
  894. }, [controller, enable]);
  895. }
  896. function maybeNotHotkey(event) {
  897. const { ctrlKey, altKey, metaKey } = event;
  898. return ctrlKey || altKey || metaKey || isTyping(event);
  899. }
  900. async function fetchBlob(url, init) {
  901. try {
  902. const response = await fetch(url, init);
  903. return await response.blob();
  904. } catch (error) {
  905. if (init?.signal?.aborted) {
  906. throw error;
  907. }
  908. const isOriginDifferent = new URL(url).origin !== location.origin;
  909. if (isOriginDifferent) {
  910. return await gmFetch(url, init).blob();
  911. } else {
  912. throw new Error("CORS blocked and cannot use GM_xmlhttpRequest", {
  913. cause: error
  914. });
  915. }
  916. }
  917. }
  918. function gmFetch(resource, init) {
  919. const method = init?.body ? "POST" : "GET";
  920. const xhr = (type) => {
  921. return new Promise((resolve, reject) => {
  922. const request = GM_xmlhttpRequest({
  923. method,
  924. url: resource,
  925. headers: init?.headers,
  926. responseType: type === "text" ? void 0 : type,
  927. data: init?.body,
  928. onload: (response) => {
  929. if (type === "text") {
  930. resolve(response.responseText);
  931. } else {
  932. resolve(response.response);
  933. }
  934. },
  935. onerror: reject,
  936. onabort: reject
  937. });
  938. init?.signal?.addEventListener(
  939. "abort",
  940. () => {
  941. request.abort();
  942. },
  943. { once: true }
  944. );
  945. });
  946. };
  947. return {
  948. blob: () => xhr("blob"),
  949. json: () => xhr("json"),
  950. text: () => xhr("text")
  951. };
  952. }
  953. var isGmCancelled = (error) => {
  954. return error instanceof Function;
  955. };
  956. async function* downloadImage({ source, signal }) {
  957. for await (const url of imageSourceToIterable(source)) {
  958. if (signal?.aborted) {
  959. break;
  960. }
  961. try {
  962. const blob = await fetchBlob(url, { signal });
  963. yield { url, blob };
  964. } catch (error) {
  965. if (isGmCancelled(error)) {
  966. yield { error: new Error("download aborted") };
  967. } else {
  968. yield { error };
  969. }
  970. }
  971. }
  972. }
  973. var getExtension = (url) => {
  974. if (!url) {
  975. return ".txt";
  976. }
  977. const extension = url.match(/\.[^/?#]{3,4}?(?=[?#]|$)/);
  978. return extension?.[0] || ".jpg";
  979. };
  980. var guessExtension = (array) => {
  981. const { 0: a, 1: b, 2: c, 3: d } = array;
  982. if (a === 255 && b === 216 && c === 255) {
  983. return ".jpg";
  984. }
  985. if (a === 137 && b === 80 && c === 78 && d === 71) {
  986. return ".png";
  987. }
  988. if (a === 82 && b === 73 && c === 70 && d === 70) {
  989. return ".webp";
  990. }
  991. if (a === 71 && b === 73 && c === 70 && d === 56) {
  992. return ".gif";
  993. }
  994. };
  995. var download = (images, options) => {
  996. const { onError, onProgress, signal } = options || {};
  997. let startedCount = 0;
  998. let resolvedCount = 0;
  999. let rejectedCount = 0;
  1000. let hasCancelled = false;
  1001. const reportProgress = ({ isCancelled, isComplete } = {}) => {
  1002. if (hasCancelled) {
  1003. return;
  1004. }
  1005. if (isCancelled) {
  1006. hasCancelled = true;
  1007. }
  1008. const total = images.length;
  1009. const settled = resolvedCount + rejectedCount;
  1010. onProgress?.({
  1011. total,
  1012. started: startedCount,
  1013. settled,
  1014. rejected: rejectedCount,
  1015. isCancelled: hasCancelled,
  1016. isComplete
  1017. });
  1018. };
  1019. const downloadWithReport = async (source) => {
  1020. const errors = [];
  1021. startedCount++;
  1022. reportProgress();
  1023. for await (const event of downloadImage({ source, signal })) {
  1024. if ("error" in event) {
  1025. errors.push(event.error);
  1026. onError?.(event.error);
  1027. continue;
  1028. }
  1029. if (event.url) {
  1030. resolvedCount++;
  1031. } else {
  1032. rejectedCount++;
  1033. }
  1034. reportProgress();
  1035. return event;
  1036. }
  1037. return {
  1038. url: "",
  1039. blob: new Blob([errors.map((x) => `${x}`).join("\n\n")])
  1040. };
  1041. };
  1042. const cipher = Math.floor(Math.log10(images.length)) + 1;
  1043. const toPair = async ({ url, blob }, index) => {
  1044. const array = new Uint8Array(await blob.arrayBuffer());
  1045. const pad = `${index}`.padStart(cipher, "0");
  1046. const name = `${pad}${guessExtension(array) ?? getExtension(url)}`;
  1047. return { [name]: array };
  1048. };
  1049. const archiveWithReport = async (sources) => {
  1050. const result = await Promise.all(sources.map(downloadWithReport));
  1051. if (signal?.aborted) {
  1052. reportProgress({ isCancelled: true });
  1053. throw new Error("aborted");
  1054. }
  1055. const pairs = await Promise.all(result.map(toPair));
  1056. const data = Object.assign({}, ...pairs);
  1057. const value = deferred();
  1058. const abort = (0, deps_exports.zip)(data, { level: 0 }, (error, array) => {
  1059. if (error) {
  1060. value.reject(error);
  1061. } else {
  1062. reportProgress({ isComplete: true });
  1063. value.resolve(array);
  1064. }
  1065. });
  1066. signal?.addEventListener("abort", abort, { once: true });
  1067. return value;
  1068. };
  1069. return archiveWithReport(images);
  1070. };
  1071. var aborterAtom = (0, import_jotai.atom)(null);
  1072. var cancelDownloadAtom = (0, import_jotai.atom)(null, (get) => {
  1073. get(aborterAtom)?.abort();
  1074. });
  1075. var downloadProgressAtom = (0, import_jotai.atom)({
  1076. value: 0,
  1077. text: "",
  1078. error: false
  1079. });
  1080. var startDownloadAtom = (0, import_jotai.atom)(null, async (get, set, options) => {
  1081. const viewerState = get(viewerStateAtom);
  1082. if (viewerState.status !== "complete") {
  1083. return;
  1084. }
  1085. const aborter = new AbortController();
  1086. set(aborterAtom, (previous) => {
  1087. previous?.abort();
  1088. return aborter;
  1089. });
  1090. addEventListener("beforeunload", confirmDownloadAbort);
  1091. try {
  1092. return await download(options?.images ?? viewerState.images, {
  1093. onProgress: reportProgress,
  1094. onError: logIfNotAborted,
  1095. signal: aborter.signal
  1096. });
  1097. } finally {
  1098. removeEventListener("beforeunload", confirmDownloadAbort);
  1099. }
  1100. function reportProgress(event) {
  1101. const { total, started, settled, rejected, isCancelled, isComplete } = event;
  1102. const value = started / total * 0.1 + settled / total * 0.89;
  1103. const text = `${(value * 100).toFixed(1)}%`;
  1104. const error = !!rejected;
  1105. if (isComplete || isCancelled) {
  1106. set(downloadProgressAtom, { value: 0, text: "", error: false });
  1107. } else {
  1108. set(downloadProgressAtom, (previous) => {
  1109. if (text !== previous.text) {
  1110. return { value, text, error };
  1111. }
  1112. return previous;
  1113. });
  1114. }
  1115. }
  1116. });
  1117. var downloadAndSaveAtom = (0, import_jotai.atom)(null, async (_get, set, options) => {
  1118. const zip2 = await set(startDownloadAtom, options);
  1119. if (zip2) {
  1120. await save(new Blob([zip2]));
  1121. }
  1122. });
  1123. function logIfNotAborted(error) {
  1124. if (isNotAbort(error)) {
  1125. console.error(error);
  1126. }
  1127. }
  1128. function isNotAbort(error) {
  1129. return !/aborted/i.test(`${error}`);
  1130. }
  1131. function confirmDownloadAbort(event) {
  1132. event.preventDefault();
  1133. event.returnValue = "";
  1134. }
  1135. function useViewerController() {
  1136. const store = (0, import_jotai.useStore)();
  1137. return (0, import_react2.useMemo)(() => createViewerController(store), [store]);
  1138. }
  1139. function createViewerController(store) {
  1140. const downloader = {
  1141. get progress() {
  1142. return store.get(downloadProgressAtom);
  1143. },
  1144. download: (options) => store.set(startDownloadAtom, options),
  1145. downloadAndSave: (options) => store.set(downloadAndSaveAtom, options),
  1146. cancel: () => store.set(cancelDownloadAtom)
  1147. };
  1148. return {
  1149. get options() {
  1150. return store.get(viewerStateAtom).options;
  1151. },
  1152. get status() {
  1153. return store.get(viewerStateAtom).status;
  1154. },
  1155. get container() {
  1156. return store.get(viewerElementAtom);
  1157. },
  1158. get compactWidthIndex() {
  1159. return store.get(compactWidthIndexAtom);
  1160. },
  1161. downloader,
  1162. get pages() {
  1163. return store.get(pagesAtom);
  1164. },
  1165. set compactWidthIndex(value) {
  1166. store.set(compactWidthIndexAtom, Math.max(0, value));
  1167. },
  1168. setOptions: (value) => store.set(setViewerOptionsAtom, value),
  1169. goPrevious: () => store.set(goPreviousAtom),
  1170. goNext: () => store.set(goNextAtom),
  1171. toggleFullscreen: () => store.set(toggleImmersiveAtom),
  1172. reloadErrored: () => store.set(reloadErroredAtom),
  1173. unmount: () => (0, deps_exports.unmountComponentAtNode)(store.get(viewerElementAtom))
  1174. };
  1175. }
  1176. var Svg2 = styled("svg", {
  1177. position: "absolute",
  1178. bottom: "8px",
  1179. left: "8px",
  1180. cursor: "pointer",
  1181. "&:hover": {
  1182. filter: "hue-rotate(-145deg)"
  1183. },
  1184. variants: {
  1185. error: {
  1186. true: {
  1187. filter: "hue-rotate(140deg)"
  1188. }
  1189. }
  1190. }
  1191. });
  1192. var Circle = styled("circle", {
  1193. transform: "rotate(-90deg)",
  1194. transformOrigin: "50% 50%",
  1195. stroke: "url(#aEObn)",
  1196. fill: "#fff8"
  1197. });
  1198. var GradientDef = React.createElement("defs", null, React.createElement("linearGradient", { id: "aEObn", x1: "100%", y1: "0%", x2: "0%", y2: "100%" }, React.createElement("stop", { offset: "0%", style: { stopColor: "#53baff", stopOpacity: 1 } }), React.createElement("stop", { offset: "100%", style: { stopColor: "#0067bb", stopOpacity: 1 } })));
  1199. var CenterText = styled("text", {
  1200. dominantBaseline: "middle",
  1201. textAnchor: "middle",
  1202. fontSize: "30px",
  1203. fontWeight: "bold",
  1204. fill: "#004b9e"
  1205. });
  1206. var CircularProgress = (props) => {
  1207. const { radius, strokeWidth, value, text, ...otherProps } = props;
  1208. const circumference = 2 * Math.PI * radius;
  1209. const strokeDashoffset = circumference - value * circumference;
  1210. const center = radius + strokeWidth / 2;
  1211. const side = center * 2;
  1212. return React.createElement(Svg2, { height: side, width: side, ...otherProps }, GradientDef, React.createElement(
  1213. Circle,
  1214. {
  1215. ...{
  1216. strokeWidth,
  1217. strokeDasharray: `${circumference} ${circumference}`,
  1218. strokeDashoffset,
  1219. r: radius,
  1220. cx: center,
  1221. cy: center
  1222. }
  1223. }
  1224. ), React.createElement(CenterText, { x: "50%", y: "50%" }, text || ""));
  1225. };
  1226. var import_jotai2 = require("jotai");
  1227. var Backdrop = styled("div", {
  1228. position: "absolute",
  1229. top: 0,
  1230. left: 0,
  1231. width: "100%",
  1232. height: "100%",
  1233. background: "rgba(0, 0, 0, 0.5)",
  1234. transition: "0.2s",
  1235. variants: {
  1236. isOpen: {
  1237. true: {
  1238. opacity: 1,
  1239. pointerEvents: "auto"
  1240. },
  1241. false: {
  1242. opacity: 0,
  1243. pointerEvents: "none"
  1244. }
  1245. }
  1246. }
  1247. });
  1248. var CenterDialog = styled("div", {
  1249. position: "absolute",
  1250. top: "50%",
  1251. left: "50%",
  1252. transform: "translate(-50%, -50%)",
  1253. display: "flex",
  1254. flexFlow: "column nowrap",
  1255. alignItems: "stretch",
  1256. justifyContent: "center",
  1257. transition: "0.2s",
  1258. background: "white",
  1259. padding: "20px",
  1260. borderRadius: "10px",
  1261. boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.2)"
  1262. });
  1263. function BackdropDialog({ onClose, ...props }) {
  1264. const [isOpen, setIsOpen] = (0, import_react2.useState)(false);
  1265. const close = async () => {
  1266. setIsOpen(false);
  1267. await timeout(200);
  1268. onClose();
  1269. };
  1270. const closeIfEnter = (event) => {
  1271. if (event.key === "Enter") {
  1272. close();
  1273. event.stopPropagation();
  1274. }
  1275. };
  1276. (0, import_react2.useEffect)(() => {
  1277. setIsOpen(true);
  1278. }, []);
  1279. return React.createElement(Backdrop, { isOpen, onClick: close, onKeyDown: closeIfEnter }, React.createElement(
  1280. CenterDialog,
  1281. {
  1282. onClick: (event) => event.stopPropagation(),
  1283. ...props
  1284. }
  1285. ));
  1286. }
  1287. var ColorInput = styled("input", {
  1288. height: "1.5em"
  1289. });
  1290. var ConfigRow = styled("div", {
  1291. display: "flex",
  1292. alignItems: "center",
  1293. justifyContent: "space-between",
  1294. gap: "10%",
  1295. "&& > *": {
  1296. fontSize: "1.3em",
  1297. fontWeight: "medium",
  1298. minWidth: "0",
  1299. margin: 0,
  1300. padding: 0
  1301. },
  1302. "& > input": {
  1303. appearance: "meter",
  1304. border: "gray 1px solid",
  1305. borderRadius: "0.2em",
  1306. textAlign: "center"
  1307. },
  1308. ":first-child": {
  1309. flex: "2 1 0"
  1310. },
  1311. ":nth-child(2)": {
  1312. flex: "1 1 0"
  1313. }
  1314. });
  1315. var HiddenInput = styled("input", {
  1316. opacity: 0,
  1317. width: 0,
  1318. height: 0
  1319. });
  1320. var Toggle = styled("span", {
  1321. "--width": "60px",
  1322. "label": {
  1323. position: "relative",
  1324. display: "inline-flex",
  1325. margin: 0,
  1326. width: "var(--width)",
  1327. height: "calc(var(--width) / 2)",
  1328. borderRadius: "calc(var(--width) / 2)",
  1329. cursor: "pointer",
  1330. textIndent: "-9999px",
  1331. background: "grey"
  1332. },
  1333. "label:after": {
  1334. position: "absolute",
  1335. top: "calc(var(--width) * 0.025)",
  1336. left: "calc(var(--width) * 0.025)",
  1337. width: "calc(var(--width) * 0.45)",
  1338. height: "calc(var(--width) * 0.45)",
  1339. borderRadius: "calc(var(--width) * 0.45)",
  1340. content: "",
  1341. background: "#fff",
  1342. transition: "0.3s"
  1343. },
  1344. "input:checked + label": {
  1345. background: "#bada55"
  1346. },
  1347. "input:checked + label:after": {
  1348. left: "calc(var(--width) * 0.975)",
  1349. transform: "translateX(-100%)"
  1350. },
  1351. "label:active:after": {
  1352. width: "calc(var(--width) * 0.65)"
  1353. }
  1354. });
  1355. var Title = styled("h3", {
  1356. fontSize: "2em",
  1357. fontWeight: "bold",
  1358. lineHeight: 1.5
  1359. });
  1360. function SettingsDialog({ onClose }) {
  1361. const [maxZoomOutExponent, setMaxZoomOutExponent] = (0, import_jotai.useAtom)(maxZoomOutExponentAtom);
  1362. const [maxZoomInExponent, setMaxZoomInExponent] = (0, import_jotai.useAtom)(maxZoomInExponentAtom);
  1363. const [backgroundColor, setBackgroundColor] = (0, import_jotai.useAtom)(backgroundColorAtom);
  1364. const [pageDirection, setPageDirection] = (0, import_jotai.useAtom)(pageDirectionAtom);
  1365. const [isFullscreenPreferred, setIsFullscreenPreferred] = (0, import_jotai.useAtom)(
  1366. isFullscreenPreferredSettingsAtom
  1367. );
  1368. const zoomOutExponentInputId = (0, import_react2.useId)();
  1369. const zoomInExponentInputId = (0, import_react2.useId)();
  1370. const colorInputId = (0, import_react2.useId)();
  1371. const pageDirectionInputId = (0, import_react2.useId)();
  1372. const fullscreenInputId = (0, import_react2.useId)();
  1373. const strings = (0, import_jotai2.useAtomValue)(i18nAtom);
  1374. const maxZoomOut = formatMultiplier(maxZoomOutExponent);
  1375. const maxZoomIn = formatMultiplier(maxZoomInExponent);
  1376. return React.createElement(BackdropDialog, { css: { gap: "1.3em" }, onClose }, React.createElement(Title, null, strings.settings), React.createElement(ConfigRow, null, React.createElement("label", { htmlFor: zoomOutExponentInputId }, strings.maxZoomOut, ": ", maxZoomOut), React.createElement(
  1377. "input",
  1378. {
  1379. type: "number",
  1380. min: 0,
  1381. step: 0.1,
  1382. id: zoomOutExponentInputId,
  1383. value: maxZoomOutExponent,
  1384. onChange: (event) => {
  1385. setMaxZoomOutExponent(event.currentTarget.valueAsNumber || 0);
  1386. }
  1387. }
  1388. )), React.createElement(ConfigRow, null, React.createElement("label", { htmlFor: zoomInExponentInputId }, strings.maxZoomIn, ": ", maxZoomIn), React.createElement(
  1389. "input",
  1390. {
  1391. type: "number",
  1392. min: 0,
  1393. step: 0.1,
  1394. id: zoomInExponentInputId,
  1395. value: maxZoomInExponent,
  1396. onChange: (event) => {
  1397. setMaxZoomInExponent(event.currentTarget.valueAsNumber || 0);
  1398. }
  1399. }
  1400. )), React.createElement(ConfigRow, null, React.createElement("label", { htmlFor: colorInputId }, strings.backgroundColor), React.createElement(
  1401. ColorInput,
  1402. {
  1403. type: "color",
  1404. id: colorInputId,
  1405. value: backgroundColor,
  1406. onChange: (event) => {
  1407. setBackgroundColor(event.currentTarget.value);
  1408. }
  1409. }
  1410. )), React.createElement(ConfigRow, null, React.createElement("p", null, strings.useFullScreen), React.createElement(Toggle, null, React.createElement(
  1411. HiddenInput,
  1412. {
  1413. type: "checkbox",
  1414. id: fullscreenInputId,
  1415. checked: isFullscreenPreferred,
  1416. onChange: (event) => {
  1417. setIsFullscreenPreferred(event.currentTarget.checked);
  1418. }
  1419. }
  1420. ), React.createElement("label", { htmlFor: fullscreenInputId }, strings.useFullScreen))), React.createElement(ConfigRow, null, React.createElement("p", null, strings.leftToRight), React.createElement(Toggle, null, React.createElement(
  1421. HiddenInput,
  1422. {
  1423. type: "checkbox",
  1424. id: pageDirectionInputId,
  1425. checked: pageDirection === "leftToRight",
  1426. onChange: (event) => {
  1427. setPageDirection(event.currentTarget.checked ? "leftToRight" : "rightToLeft");
  1428. }
  1429. }
  1430. ), React.createElement("label", { htmlFor: pageDirectionInputId }, strings.leftToRight))));
  1431. }
  1432. function formatMultiplier(maxZoomOutExponent) {
  1433. return Math.sqrt(2) ** maxZoomOutExponent === Infinity ? "\u221E" : `${(Math.sqrt(2) ** maxZoomOutExponent).toPrecision(2)}x`;
  1434. }
  1435. var LeftBottomFloat = styled("div", {
  1436. position: "absolute",
  1437. bottom: "1%",
  1438. left: "1%",
  1439. display: "flex",
  1440. flexFlow: "column"
  1441. });
  1442. var MenuActions = styled("div", {
  1443. display: "flex",
  1444. flexFlow: "column nowrap",
  1445. alignItems: "center",
  1446. gap: "16px"
  1447. });
  1448. function LeftBottomControl() {
  1449. const { value, text, error } = (0, import_jotai.useAtomValue)(downloadProgressAtom);
  1450. const cancelDownload = (0, import_jotai.useSetAtom)(downloadProgressAtom);
  1451. const downloadAndSave = (0, import_jotai.useSetAtom)(downloadAndSaveAtom);
  1452. const [isOpen, setIsOpen] = (0, import_react2.useState)(false);
  1453. return React.createElement(React.Fragment, null, React.createElement(LeftBottomFloat, null, !!text && React.createElement(
  1454. CircularProgress,
  1455. {
  1456. radius: 50,
  1457. strokeWidth: 10,
  1458. value: value ?? 0,
  1459. text,
  1460. error,
  1461. onClick: cancelDownload
  1462. }
  1463. ), React.createElement(MenuActions, null, React.createElement(
  1464. IconSettings,
  1465. {
  1466. onClick: () => {
  1467. setIsOpen((value2) => !value2);
  1468. }
  1469. }
  1470. ), React.createElement(DownloadIcon, { onClick: () => downloadAndSave() }))), isOpen && React.createElement(SettingsDialog, { onClose: () => setIsOpen(false) }));
  1471. }
  1472. var stretch = keyframes({
  1473. "0%": {
  1474. top: "8px",
  1475. height: "64px"
  1476. },
  1477. "50%": {
  1478. top: "24px",
  1479. height: "32px"
  1480. },
  1481. "100%": {
  1482. top: "24px",
  1483. height: "32px"
  1484. }
  1485. });
  1486. var SpinnerContainer = styled("div", {
  1487. position: "absolute",
  1488. left: "0",
  1489. top: "0",
  1490. right: "0",
  1491. bottom: "0",
  1492. margin: "auto",
  1493. display: "flex",
  1494. justifyContent: "center",
  1495. alignItems: "center",
  1496. div: {
  1497. display: "inline-block",
  1498. width: "16px",
  1499. margin: "0 4px",
  1500. background: "#fff",
  1501. animation: `${stretch} 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite`
  1502. },
  1503. "div:nth-child(1)": {
  1504. "animation-delay": "-0.24s"
  1505. },
  1506. "div:nth-child(2)": {
  1507. "animation-delay": "-0.12s"
  1508. },
  1509. "div:nth-child(3)": {
  1510. "animation-delay": "0"
  1511. }
  1512. });
  1513. var Spinner = () => React.createElement(SpinnerContainer, null, React.createElement("div", null), React.createElement("div", null), React.createElement("div", null));
  1514. var Overlay = styled("div", {
  1515. position: "relative",
  1516. maxWidth: "100%",
  1517. height: "100%",
  1518. display: "flex",
  1519. alignItems: "center",
  1520. justifyContent: "center",
  1521. "@media print": {
  1522. margin: 0
  1523. },
  1524. variants: {
  1525. placeholder: {
  1526. true: { width: "45%", height: "100%" }
  1527. },
  1528. fullWidth: {
  1529. true: { width: "100%" }
  1530. },
  1531. originalSize: {
  1532. true: {
  1533. minHeight: "100%",
  1534. height: "auto"
  1535. }
  1536. }
  1537. }
  1538. });
  1539. var LinkColumn = styled("div", {
  1540. display: "flex",
  1541. flexFlow: "column nowrap",
  1542. alignItems: "center",
  1543. justifyContent: "center",
  1544. cursor: "pointer",
  1545. boxShadow: "1px 1px 3px",
  1546. padding: "1rem 1.5rem",
  1547. transition: "box-shadow 1s easeOutExpo",
  1548. "&:hover": {
  1549. boxShadow: "2px 2px 5px"
  1550. },
  1551. "&:active": {
  1552. boxShadow: "0 0 2px"
  1553. }
  1554. });
  1555. var Image = styled("img", {
  1556. position: "relative",
  1557. height: "100%",
  1558. maxWidth: "100%",
  1559. objectFit: "contain",
  1560. variants: {
  1561. originalSize: {
  1562. true: { height: "auto" }
  1563. }
  1564. }
  1565. });
  1566. var Page = ({ atom: atom2, ...props }) => {
  1567. const { imageProps, fullWidth, reloadAtom, shouldBeOriginalSize, state: pageState } = (0, import_jotai.useAtomValue)(atom2);
  1568. const strings = (0, import_jotai.useAtomValue)(i18nAtom);
  1569. const reload = (0, import_jotai.useSetAtom)(reloadAtom);
  1570. const { state } = pageState;
  1571. const reloadErrored = async (event) => {
  1572. event.stopPropagation();
  1573. await reload();
  1574. };
  1575. return React.createElement(
  1576. Overlay,
  1577. {
  1578. placeholder: state !== "complete",
  1579. originalSize: shouldBeOriginalSize,
  1580. fullWidth
  1581. },
  1582. state === "loading" && React.createElement(Spinner, null),
  1583. state === "error" && React.createElement(LinkColumn, { onClick: reloadErrored }, React.createElement(CircledX, null), React.createElement("p", null, strings.failedToLoadImage), React.createElement("p", null, pageState.urls?.join("\n"))),
  1584. React.createElement(Image, { ...imageProps, originalSize: shouldBeOriginalSize, ...props })
  1585. );
  1586. };
  1587. var InnerViewer = (0, import_react2.forwardRef)((props, refHandle) => {
  1588. const { useDefault: enableDefault, options: viewerOptions, ...otherProps } = props;
  1589. const [viewerElement, setViewerElement] = (0, import_jotai.useAtom)(viewerElementAtom);
  1590. const setScrollElement = (0, import_jotai.useSetAtom)(scrollElementAtom);
  1591. const fullscreenElement = (0, import_jotai.useAtomValue)(fullscreenElementAtom);
  1592. const backgroundColor = (0, import_jotai.useAtomValue)(backgroundColorAtom);
  1593. const viewer = (0, import_jotai.useAtomValue)(viewerStateAtom);
  1594. const setViewerOptions = (0, import_jotai.useSetAtom)(setViewerOptionsAtom);
  1595. const navigate = (0, import_jotai.useSetAtom)(navigateAtom);
  1596. const blockSelection = (0, import_jotai.useSetAtom)(blockSelectionAtom);
  1597. const synchronizeScroll = (0, import_jotai.useSetAtom)(synchronizeScrollAtom);
  1598. const pageDirection = (0, import_jotai.useAtomValue)(pageDirectionAtom);
  1599. const strings = (0, import_jotai.useAtomValue)(i18nAtom);
  1600. const mode = (0, import_jotai.useAtomValue)(viewerModeAtom);
  1601. const { status } = viewer;
  1602. const controller = useViewerController();
  1603. const { options, toggleFullscreen } = controller;
  1604. useDefault({ enable: props.useDefault, controller });
  1605. (0, import_react2.useImperativeHandle)(refHandle, () => controller, [controller]);
  1606. (0, import_react2.useEffect)(() => {
  1607. setViewerOptions(viewerOptions);
  1608. }, [viewerOptions]);
  1609. return React.createElement(
  1610. Container,
  1611. {
  1612. ref: setViewerElement,
  1613. tabIndex: -1,
  1614. css: { backgroundColor },
  1615. immersive: mode === "window"
  1616. },
  1617. React.createElement(
  1618. ScrollableLayout,
  1619. {
  1620. ref: setScrollElement,
  1621. dark: isDarkColor(backgroundColor),
  1622. fullscreen: fullscreenElement === viewerElement,
  1623. ltr: pageDirection === "leftToRight",
  1624. onScroll: synchronizeScroll,
  1625. onClick: navigate,
  1626. onMouseDown: blockSelection,
  1627. children: status === "complete" ? viewer.pages.map((atom2) => React.createElement(
  1628. Page,
  1629. {
  1630. key: `${atom2}`,
  1631. atom: atom2,
  1632. ...options?.imageProps
  1633. }
  1634. )) : React.createElement("p", null, status === "error" ? strings.errorIsOccurred : strings.loading),
  1635. ...otherProps
  1636. }
  1637. ),
  1638. React.createElement(FullscreenIcon, { onClick: toggleFullscreen }),
  1639. status === "complete" ? React.createElement(LeftBottomControl, null) : false,
  1640. React.createElement(import_react_toastify.ToastContainer, null)
  1641. );
  1642. });
  1643. function isDarkColor(rgbColor) {
  1644. const match = rgbColor.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
  1645. if (!match) {
  1646. return false;
  1647. }
  1648. const [_, r, g, b] = match.map((x) => parseInt(x, 16));
  1649. const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
  1650. return luminance < 0.5;
  1651. }
  1652. var types_exports = {};
  1653. function initialize(options) {
  1654. const store = (0, import_jotai.createStore)();
  1655. const ref = (0, import_react2.createRef)();
  1656. (0, deps_exports.render)(
  1657. React.createElement(import_jotai.Provider, { store }, React.createElement(InnerViewer, { ref, options, useDefault: true })),
  1658. getDefaultRoot()
  1659. );
  1660. return Promise.resolve(ref.current);
  1661. }
  1662. var Viewer = (0, import_react2.forwardRef)(({ options, useDefault: useDefault2 }, ref) => {
  1663. const store = (0, import_react2.useMemo)(import_jotai.createStore, []);
  1664. return React.createElement(import_jotai.Provider, { store }, React.createElement(InnerViewer, { ...{ options, ref, useDefault: useDefault2 } }));
  1665. });
  1666. function getDefaultRoot() {
  1667. const div = document.createElement("div");
  1668. div.setAttribute("style", "width: 0; height: 0;");
  1669. document.body.append(div);
  1670. return div;
  1671. }