vim comic viewer

Universal comic reader

当前为 2024-09-26 提交的版本,查看 最新版本

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