vim comic viewer

Universal comic reader

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

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