vim comic viewer

Universal comic reader

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

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