vim comic viewer

Universal comic reader

当前为 2024-10-21 提交的版本,查看 最新版本

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