vim comic viewer

Universal comic reader

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