vim comic viewer

Universal comic reader

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

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