vim comic viewer

Universal comic reader

当前为 2021-01-04 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/417893/887558/vim%20comic%20viewer.js

  1. // ==UserScript==
  2. // @name vim comic viewer
  3. // @description Universal comic reader
  4. // @version 3.1.0
  5. // @namespace https://greasyfork.org/en/users/713014-nanikit
  6. // @exclude *
  7. // @match http://unused-field.space/
  8. // @author nanikit
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. "use strict";
  13.  
  14. Object.defineProperty(exports, "__esModule", { value: true });
  15.  
  16. var react$1 = require("react");
  17. var react = require("@stitches/react");
  18. var JSZip = require("jszip");
  19. var reactDom = require("react-dom");
  20.  
  21. function _interopDefaultLegacy(e) {
  22. return e && typeof e === "object" && "default" in e ? e : { "default": e };
  23. }
  24.  
  25. var JSZip__default = /*#__PURE__*/ _interopDefaultLegacy(JSZip);
  26.  
  27. const { styled, css } = react.createStyled({});
  28.  
  29. const Svg = styled("svg", {
  30. position: "fixed",
  31. top: "8px",
  32. right: "8px",
  33. cursor: "pointer",
  34. ":hover": {
  35. filter: "hue-rotate(-145deg)",
  36. },
  37. variants: {
  38. error: {
  39. true: {
  40. filter: "hue-rotate(140deg)",
  41. },
  42. },
  43. },
  44. });
  45. const Circle = styled("circle", {
  46. transition: "stroke-dashoffset 0.35s",
  47. transform: "rotate(-90deg)",
  48. transformOrigin: "50% 50%",
  49. stroke: "url(#aEObn)",
  50. fill: "#fff8",
  51. });
  52. const GradientDef = react$1.createElement(
  53. "defs",
  54. null,
  55. react$1.createElement(
  56. "linearGradient",
  57. {
  58. id: "aEObn",
  59. x1: "100%",
  60. y1: "0%",
  61. x2: "0%",
  62. y2: "100%",
  63. },
  64. react$1.createElement("stop", {
  65. offset: "0%",
  66. style: {
  67. stopColor: "#53baff",
  68. stopOpacity: 1,
  69. },
  70. }),
  71. react$1.createElement("stop", {
  72. offset: "100%",
  73. style: {
  74. stopColor: "#0067bb",
  75. stopOpacity: 1,
  76. },
  77. }),
  78. ),
  79. );
  80. const CenterText = styled("text", {
  81. dominantBaseline: "middle",
  82. textAnchor: "middle",
  83. fontSize: "30px",
  84. fontWeight: "bold",
  85. fill: "#004b9e",
  86. });
  87. const CircularProgress = (props) => {
  88. const { radius, strokeWidth, value, text, ...otherProps } = props;
  89. const circumference = 2 * Math.PI * radius;
  90. const strokeDashoffset = circumference - value * circumference;
  91. const center = radius + strokeWidth / 2;
  92. const side = center * 2;
  93. return react$1.createElement(
  94. Svg,
  95. Object.assign({
  96. height: side,
  97. width: side,
  98. }, otherProps),
  99. GradientDef,
  100. react$1.createElement(
  101. Circle,
  102. Object.assign({}, {
  103. strokeWidth,
  104. strokeDasharray: `${circumference} ${circumference}`,
  105. strokeDashoffset,
  106. r: radius,
  107. cx: center,
  108. cy: center,
  109. }),
  110. ),
  111. react$1.createElement(CenterText, {
  112. x: "50%",
  113. y: "50%",
  114. }, text || ""),
  115. );
  116. };
  117.  
  118. const ScrollableLayout = styled("div", {
  119. // chrome user-agent style override
  120. outline: 0,
  121. position: "relative",
  122. backgroundColor: "#eee",
  123. height: "100%",
  124. display: "flex",
  125. justifyContent: "center",
  126. alignItems: "center",
  127. flexFlow: "row-reverse wrap",
  128. overflowY: "auto",
  129. variants: {
  130. fullscreen: {
  131. true: {
  132. display: "flex",
  133. position: "fixed",
  134. top: 0,
  135. bottom: 0,
  136. overflow: "auto",
  137. },
  138. },
  139. },
  140. });
  141.  
  142. const timeout = (millisecond) =>
  143. new Promise((resolve) => setTimeout(resolve, millisecond));
  144. const waitDomContent = (document) =>
  145. document.readyState === "loading"
  146. ? new Promise((r) =>
  147. document.addEventListener("readystatechange", r, {
  148. once: true,
  149. })
  150. )
  151. : true;
  152. const insertCss = (css) => {
  153. const style = document.createElement("style");
  154. style.innerHTML = css;
  155. document.head.append(style);
  156. };
  157. const waitBody = async (document) => {
  158. while (!document.body) {
  159. await timeout(1);
  160. }
  161. };
  162. const isTyping = (event) =>
  163. event.target?.tagName?.match?.(/INPUT|TEXTAREA/) ||
  164. event.target?.isContentEditable;
  165. const saveAs = async (blob, name) => {
  166. const a = document.createElement("a");
  167. a.download = name;
  168. a.rel = "noopener";
  169. a.href = URL.createObjectURL(blob);
  170. a.click();
  171. await timeout(40000);
  172. URL.revokeObjectURL(a.href);
  173. };
  174. const getSafeFileName = (str) => {
  175. return str.replace(/[<>:"/\\|?*\x00-\x1f]+/gi, "").trim() || "download";
  176. };
  177. const defer = () => {
  178. let resolve, reject;
  179. const promise = new Promise((res, rej) => {
  180. resolve = res;
  181. reject = rej;
  182. });
  183. return {
  184. promise,
  185. resolve,
  186. reject,
  187. };
  188. };
  189.  
  190. var utils = /*#__PURE__*/ Object.freeze({
  191. __proto__: null,
  192. timeout: timeout,
  193. waitDomContent: waitDomContent,
  194. insertCss: insertCss,
  195. waitBody: waitBody,
  196. isTyping: isTyping,
  197. saveAs: saveAs,
  198. getSafeFileName: getSafeFileName,
  199. defer: defer,
  200. });
  201.  
  202. const useDeferred = () => {
  203. const [deferred] = react$1.useState(defer);
  204. return deferred;
  205. };
  206.  
  207. const useFullscreenElement = () => {
  208. const [element, setElement] = react$1.useState(
  209. document.fullscreenElement || undefined,
  210. );
  211. react$1.useEffect(() => {
  212. const notify = () => setElement(document.fullscreenElement || undefined);
  213. document.addEventListener("fullscreenchange", notify);
  214. return () => document.removeEventListener("fullscreenchange", notify);
  215. }, []);
  216. return element;
  217. };
  218.  
  219. const GM_xmlhttpRequest = module.config().GM_xmlhttpRequest;
  220.  
  221. const fetchBlob = async (url, init) => {
  222. try {
  223. const response = await fetch(url, init);
  224. return await response.blob();
  225. } catch (error) {
  226. const isOriginDifferent = new URL(url).origin !== location.origin;
  227. if (isOriginDifferent && gmFetch) {
  228. return await gmFetch(url, init).blob();
  229. } else {
  230. throw error;
  231. }
  232. }
  233. };
  234. const GMxhr = GM_xmlhttpRequest;
  235. const gmFetch = GMxhr
  236. ? (resource, init) => {
  237. const method = init?.body ? "POST" : "GET";
  238. const xhr = (type) => {
  239. return new Promise((resolve, reject) => {
  240. const request = GMxhr({
  241. method,
  242. url: resource,
  243. headers: init?.headers,
  244. responseType: type === "text" ? undefined : type,
  245. data: init?.body,
  246. onload: (response) => {
  247. if (type === "text") {
  248. resolve(response.responseText);
  249. } else {
  250. resolve(response.response);
  251. }
  252. },
  253. onerror: reject,
  254. onabort: reject,
  255. });
  256. if (init?.signal) {
  257. init.signal.addEventListener("abort", () => {
  258. request.abort();
  259. });
  260. }
  261. });
  262. };
  263. return {
  264. blob: () => xhr("blob"),
  265. json: () => xhr("json"),
  266. text: () => xhr("text"),
  267. };
  268. }
  269. : undefined;
  270.  
  271. const imageSourceToIterable = (source) => {
  272. if (typeof source === "string") {
  273. return (async function* () {
  274. yield source;
  275. })();
  276. } else if (Array.isArray(source)) {
  277. return (async function* () {
  278. for (const url of source) {
  279. yield url;
  280. }
  281. })();
  282. } else {
  283. return source();
  284. }
  285. };
  286. const transformToBlobUrl = (source) =>
  287. async () => {
  288. const imageSources = await source();
  289. return imageSources.map((imageSource) =>
  290. async function* () {
  291. for await (const url of imageSourceToIterable(imageSource)) {
  292. try {
  293. const blob = await fetchBlob(url);
  294. yield URL.createObjectURL(blob);
  295. } catch (error) {
  296. console.log(error);
  297. }
  298. }
  299. }
  300. );
  301. };
  302.  
  303. const download = async (images, options) => {
  304. const { onError, onProgress } = options || {};
  305. const aborter = new AbortController();
  306. let resolvedCount = 0;
  307. let rejectedCount = 0;
  308. let zipPercent = 0;
  309. let cancelled = false;
  310. const reportProgress = () => {
  311. const total = images.length;
  312. const settled = resolvedCount + rejectedCount;
  313. onProgress?.({
  314. total,
  315. settled,
  316. rejected: rejectedCount,
  317. cancelled,
  318. zipPercent,
  319. });
  320. };
  321. const downloadImage = async (source) => {
  322. const errors = [];
  323. for await (const url of imageSourceToIterable(source)) {
  324. try {
  325. const blob = await fetchBlob(url);
  326. resolvedCount++;
  327. reportProgress();
  328. return {
  329. url,
  330. blob,
  331. };
  332. } catch (error) {
  333. errors.push(error);
  334. onError?.(error);
  335. }
  336. }
  337. rejectedCount++;
  338. reportProgress();
  339. return {
  340. url: "",
  341. blob: new Blob([
  342. errors.map((x) => x.toString()).join("\n\n"),
  343. ]),
  344. };
  345. };
  346. const deferred = defer();
  347. const tasks = images.map(downloadImage);
  348. reportProgress();
  349. const archive = async () => {
  350. const cancellation = async () => {
  351. if (await deferred.promise === undefined) {
  352. aborter.abort();
  353. }
  354. return Symbol();
  355. };
  356. const checkout = Promise.all(tasks);
  357. const result = await Promise.race([
  358. cancellation(),
  359. checkout,
  360. ]);
  361. if (typeof result === "symbol") {
  362. cancelled = true;
  363. reportProgress();
  364. return;
  365. }
  366. const cipher = Math.floor(Math.log10(tasks.length)) + 1;
  367. const getExtension = (url) => {
  368. if (!url) {
  369. return ".txt";
  370. }
  371. const extension = url.match(/\.[^/?#]{3,4}?(?=[?#]|$)/);
  372. return extension || ".jpg";
  373. };
  374. const getName = (url, index) => {
  375. const pad = `${index}`.padStart(cipher, "0");
  376. const name = `${pad}${getExtension(url)}`;
  377. return name;
  378. };
  379. const zip = JSZip__default["default"]();
  380. for (let i = 0; i < result.length; i++) {
  381. const file = result[i];
  382. zip.file(getName(file.url, i), file.blob);
  383. }
  384. const proxy = new Proxy(zip, {
  385. get: (target, property, receiver) => {
  386. const ret = Reflect.get(target, property, receiver);
  387. if (property !== "generateAsync") {
  388. return ret;
  389. }
  390. return (options, onUpdate) =>
  391. ret.bind(target)(options, (metadata) => {
  392. zipPercent = metadata.percent;
  393. reportProgress();
  394. onUpdate?.(metadata);
  395. });
  396. },
  397. });
  398. deferred.resolve(proxy);
  399. };
  400. archive();
  401. return {
  402. zip: deferred.promise,
  403. cancel: () => deferred.resolve(undefined),
  404. };
  405. };
  406.  
  407. const useIntersectionObserver = (callback, options) => {
  408. const [observer, setObserver] = react$1.useState();
  409. react$1.useEffect(() => {
  410. const newObserver = new IntersectionObserver(callback, options);
  411. setObserver(newObserver);
  412. return () => newObserver.disconnect();
  413. }, [
  414. callback,
  415. options,
  416. ]);
  417. return observer;
  418. };
  419. const useIntersection = (callback, options) => {
  420. const memo = react$1.useRef(new Map());
  421. const filterIntersections = react$1.useCallback((newEntries) => {
  422. const memoized = memo.current;
  423. for (const entry of newEntries) {
  424. if (entry.isIntersecting) {
  425. memoized.set(entry.target, entry);
  426. } else {
  427. memoized.delete(entry.target);
  428. }
  429. }
  430. callback([
  431. ...memoized.values(),
  432. ]);
  433. }, [
  434. callback,
  435. ]);
  436. return useIntersectionObserver(filterIntersections, options);
  437. };
  438.  
  439. const useResize = (target, transformer) => {
  440. const [value, setValue] = react$1.useState(() => transformer(undefined));
  441. const callbackRef = react$1.useRef(transformer);
  442. callbackRef.current = transformer;
  443. react$1.useEffect(() => {
  444. if (!target) {
  445. return;
  446. }
  447. const observer = new ResizeObserver((entries) => {
  448. setValue(callbackRef.current(entries[0]));
  449. });
  450. observer.observe(target);
  451. return () => observer.disconnect();
  452. }, [
  453. target,
  454. callbackRef,
  455. ]);
  456. return value;
  457. };
  458. const getCurrentPage = (container, entries) => {
  459. if (!entries.length) {
  460. return container.firstElementChild || undefined;
  461. }
  462. const children = [
  463. ...container.children,
  464. ];
  465. const fullyVisibles = entries.filter((x) => x.intersectionRatio === 1);
  466. if (fullyVisibles.length) {
  467. fullyVisibles.sort((a, b) => {
  468. return children.indexOf(a.target) - children.indexOf(b.target);
  469. });
  470. return fullyVisibles[Math.floor(fullyVisibles.length / 2)].target;
  471. }
  472. return entries.sort((a, b) => {
  473. const ratio = {
  474. a: a.intersectionRatio,
  475. b: b.intersectionRatio,
  476. };
  477. const index = {
  478. a: children.indexOf(a.target),
  479. b: children.indexOf(b.target),
  480. };
  481. return (ratio.b - ratio.a) * 10000 + (index.a - index.b);
  482. })[0].target;
  483. };
  484. const usePageNavigator = (container) => {
  485. const [anchor, setAnchor] = react$1.useState({
  486. currentPage: undefined,
  487. ratio: 0.5,
  488. });
  489. const { currentPage, ratio } = anchor;
  490. const ignoreIntersection = react$1.useRef(false);
  491. const resetAnchor = react$1.useCallback((entries) => {
  492. if (!container?.clientHeight || entries.length === 0) {
  493. return;
  494. }
  495. if (ignoreIntersection.current) {
  496. ignoreIntersection.current = false;
  497. return;
  498. }
  499. const page = getCurrentPage(container, entries);
  500. const y = container.scrollTop + container.clientHeight / 2;
  501. const newRatio = (y - page.offsetTop) / page.clientHeight;
  502. const newAnchor = {
  503. currentPage: page,
  504. ratio: newRatio,
  505. };
  506. setAnchor(newAnchor);
  507. }, [
  508. container,
  509. ]);
  510. const goNext = react$1.useCallback(() => {
  511. ignoreIntersection.current = false;
  512. if (!currentPage) {
  513. return;
  514. }
  515. const originBound = currentPage.getBoundingClientRect();
  516. let cursor = currentPage;
  517. while (cursor.nextElementSibling) {
  518. const next = cursor.nextElementSibling;
  519. const nextBound = next.getBoundingClientRect();
  520. if (originBound.bottom < nextBound.top) {
  521. next.scrollIntoView({
  522. block: "center",
  523. });
  524. break;
  525. }
  526. cursor = next;
  527. }
  528. }, [
  529. currentPage,
  530. ]);
  531. const goPrevious = react$1.useCallback(() => {
  532. ignoreIntersection.current = false;
  533. if (!currentPage) {
  534. return;
  535. }
  536. const originBound = currentPage.getBoundingClientRect();
  537. let cursor = currentPage;
  538. while (cursor.previousElementSibling) {
  539. const previous = cursor.previousElementSibling;
  540. const previousBound = previous.getBoundingClientRect();
  541. if (previousBound.bottom < originBound.top) {
  542. previous.scrollIntoView({
  543. block: "center",
  544. });
  545. break;
  546. }
  547. cursor = previous;
  548. }
  549. }, [
  550. currentPage,
  551. ]);
  552. const restoreScroll = react$1.useCallback(() => {
  553. if (!container || ratio === undefined || currentPage === undefined) {
  554. return;
  555. }
  556. const restoredY = currentPage.offsetTop +
  557. currentPage.clientHeight * (ratio - 0.5);
  558. container.scroll({
  559. top: restoredY,
  560. });
  561. ignoreIntersection.current = true;
  562. }, [
  563. container,
  564. currentPage,
  565. ratio,
  566. ]);
  567. const intersectionOption = react$1.useMemo(() => ({
  568. threshold: [
  569. 0.01,
  570. 0.5,
  571. 1,
  572. ],
  573. }), []);
  574. const observer = useIntersection(resetAnchor, intersectionOption);
  575. useResize(container, restoreScroll);
  576. return react$1.useMemo(() => ({
  577. goNext,
  578. goPrevious,
  579. observer,
  580. }), [
  581. goNext,
  582. goPrevious,
  583. observer,
  584. ]);
  585. };
  586.  
  587. var ActionType;
  588. (function (ActionType) {
  589. ActionType[ActionType["GoPrevious"] = 0] = "GoPrevious";
  590. ActionType[ActionType["GoNext"] = 1] = "GoNext";
  591. ActionType[ActionType["ToggleFullscreen"] = 2] = "ToggleFullscreen";
  592. ActionType[ActionType["Unmount"] = 3] = "Unmount";
  593. ActionType[ActionType["SetState"] = 4] = "SetState";
  594. ActionType[ActionType["Download"] = 5] = "Download";
  595. })(ActionType || (ActionType = {}));
  596. const reducer = (state, action) => {
  597. switch (action.type) {
  598. case ActionType.SetState:
  599. return {
  600. ...state,
  601. ...action.state,
  602. };
  603. case ActionType.GoPrevious:
  604. state.navigator.goPrevious();
  605. break;
  606. case ActionType.GoNext:
  607. state.navigator.goNext();
  608. break;
  609. case ActionType.ToggleFullscreen:
  610. if (document.fullscreenElement) {
  611. document.exitFullscreen();
  612. } else {
  613. state.ref.current?.requestFullscreen?.();
  614. }
  615. break;
  616. case ActionType.Unmount:
  617. if (state.ref.current) {
  618. reactDom.unmountComponentAtNode(state.ref.current);
  619. }
  620. break;
  621. default:
  622. debugger;
  623. break;
  624. }
  625. return state;
  626. };
  627. const getAsyncReducer = (dispatch) => {
  628. let images = [];
  629. let cancelDownload;
  630. const setInnerState = (state) => {
  631. dispatch({
  632. type: ActionType.SetState,
  633. state,
  634. });
  635. };
  636. const setState = async (state) => {
  637. const source = state.options?.source;
  638. if (source) {
  639. try {
  640. setInnerState({
  641. status: "loading",
  642. images: [],
  643. });
  644. images = await source();
  645. if (!Array.isArray(images)) {
  646. console.log(`Invalid comic source type: ${typeof images}`);
  647. setInnerState({
  648. status: "error",
  649. });
  650. return;
  651. }
  652. setInnerState({
  653. status: "complete",
  654. images,
  655. });
  656. } catch (error) {
  657. setInnerState({
  658. status: "error",
  659. });
  660. console.log(error);
  661. throw error;
  662. }
  663. } else {
  664. setInnerState(state);
  665. }
  666. };
  667. const clearCancel = () => {
  668. setInnerState({
  669. cancelDownload: undefined,
  670. });
  671. cancelDownload = undefined;
  672. };
  673. const startDownload = async (options) => {
  674. if (cancelDownload) {
  675. cancelDownload();
  676. clearCancel();
  677. return;
  678. }
  679. if (!images.length) {
  680. return;
  681. }
  682. const { zip, cancel } = await download(images, options);
  683. cancelDownload = () => {
  684. cancel();
  685. clearCancel();
  686. };
  687. setInnerState({
  688. cancelDownload,
  689. });
  690. const result = await zip;
  691. clearCancel();
  692. return result;
  693. };
  694. return (action) => {
  695. switch (action.type) {
  696. case ActionType.Download:
  697. return startDownload(action.options);
  698. case ActionType.SetState:
  699. return setState(action.state);
  700. default:
  701. return dispatch(action);
  702. }
  703. };
  704. };
  705. const useViewerReducer = (ref) => {
  706. const navigator = usePageNavigator(ref.current);
  707. const [state, dispatch] = react$1.useReducer(reducer, {
  708. ref,
  709. navigator,
  710. options: {},
  711. images: [],
  712. status: "loading",
  713. });
  714. const [asyncDispatch] = react$1.useState(() => getAsyncReducer(dispatch));
  715. react$1.useEffect(() => {
  716. dispatch({
  717. type: ActionType.SetState,
  718. state: {
  719. navigator,
  720. },
  721. });
  722. }, [
  723. navigator,
  724. ]);
  725. return [
  726. state,
  727. asyncDispatch,
  728. ];
  729. };
  730.  
  731. const stretch = css.keyframes({
  732. "0%": {
  733. top: "8px",
  734. height: "64px",
  735. },
  736. "50%": {
  737. top: "24px",
  738. height: "32px",
  739. },
  740. "100%": {
  741. top: "24px",
  742. height: "32px",
  743. },
  744. });
  745. const SpinnerContainer = styled("div", {
  746. position: "absolute",
  747. left: "0",
  748. top: "0",
  749. right: "0",
  750. bottom: "0",
  751. margin: "auto",
  752. display: "flex",
  753. justifyContent: "center",
  754. alignItems: "center",
  755. div: {
  756. display: "inline-block",
  757. width: "16px",
  758. margin: "0 4px",
  759. background: "#fff",
  760. animation: `${stretch} 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite`,
  761. },
  762. "div:nth-child(1)": {
  763. "animation-delay": "-0.24s",
  764. },
  765. "div:nth-child(2)": {
  766. "animation-delay": "-0.12s",
  767. },
  768. "div:nth-child(3)": {
  769. "animation-delay": "0",
  770. },
  771. });
  772. const Spinner = () =>
  773. react$1.createElement(
  774. SpinnerContainer,
  775. null,
  776. react$1.createElement("div", null),
  777. react$1.createElement("div", null),
  778. react$1.createElement("div", null),
  779. );
  780. const Overlay = styled("div", {
  781. position: "relative",
  782. maxWidth: "100%",
  783. height: "100%",
  784. variants: {
  785. placeholder: {
  786. true: {
  787. width: "45%",
  788. },
  789. },
  790. },
  791. margin: "4px 1px",
  792. "@media print": {
  793. margin: 0,
  794. },
  795. });
  796. const Image1 = styled("img", {
  797. position: "relative",
  798. height: "100%",
  799. objectFit: "contain",
  800. maxWidth: "100%",
  801. });
  802.  
  803. var PageActionType;
  804. (function (PageActionType) {
  805. PageActionType[PageActionType["SetState"] = 0] = "SetState";
  806. PageActionType[PageActionType["SetSource"] = 1] = "SetSource";
  807. PageActionType[PageActionType["Fallback"] = 2] = "Fallback";
  808. })(PageActionType || (PageActionType = {}));
  809. const reducer$1 = (state, action) => {
  810. switch (action.type) {
  811. case PageActionType.SetState:
  812. return {
  813. ...state,
  814. ...action.state,
  815. };
  816. default:
  817. debugger;
  818. return state;
  819. }
  820. };
  821. const getAsyncReducer$1 = (dispatch) => {
  822. const empty = async function* () {
  823. }();
  824. let iterator = empty;
  825. const setState = (state) => {
  826. dispatch({
  827. type: PageActionType.SetState,
  828. state,
  829. });
  830. };
  831. const takeNext = async () => {
  832. const snapshot = iterator;
  833. try {
  834. const item = await snapshot.next();
  835. if (snapshot !== iterator) {
  836. return;
  837. }
  838. if (item.done) {
  839. setState({
  840. src: undefined,
  841. status: "error",
  842. });
  843. } else {
  844. setState({
  845. src: item.value,
  846. status: "loading",
  847. });
  848. }
  849. } catch (error) {
  850. console.error(error);
  851. setState({
  852. src: undefined,
  853. status: "error",
  854. });
  855. }
  856. };
  857. const setSource = async (source) => {
  858. iterator = imageSourceToIterable(source)[Symbol.asyncIterator]();
  859. await takeNext();
  860. };
  861. return (action) => {
  862. switch (action.type) {
  863. case PageActionType.SetSource:
  864. return setSource(action.source);
  865. case PageActionType.Fallback:
  866. return takeNext();
  867. default:
  868. return dispatch(action);
  869. }
  870. };
  871. };
  872. const usePageReducer = (source) => {
  873. const [state, dispatch] = react$1.useReducer(reducer$1, {
  874. status: "loading",
  875. });
  876. const [asyncDispatch] = react$1.useState(() => getAsyncReducer$1(dispatch));
  877. const onError = react$1.useCallback(() => {
  878. asyncDispatch({
  879. type: PageActionType.Fallback,
  880. });
  881. }, []);
  882. const onLoad = react$1.useCallback(() => {
  883. asyncDispatch({
  884. type: PageActionType.SetState,
  885. state: {
  886. status: "complete",
  887. },
  888. });
  889. }, []);
  890. react$1.useEffect(() => {
  891. asyncDispatch({
  892. type: PageActionType.SetSource,
  893. source,
  894. });
  895. }, [
  896. source,
  897. ]);
  898. return [
  899. {
  900. ...state,
  901. onLoad,
  902. onError,
  903. },
  904. asyncDispatch,
  905. ];
  906. };
  907.  
  908. const Page = ({ source, observer, ...props }) => {
  909. const [{ status, src, ...imageProps }] = usePageReducer(source);
  910. const ref = react$1.useRef();
  911. react$1.useEffect(() => {
  912. const target = ref.current;
  913. if (target && observer) {
  914. observer.observe(target);
  915. return () => observer.unobserve(target);
  916. }
  917. }, [
  918. observer,
  919. ref.current,
  920. ]);
  921. return react$1.createElement(
  922. Overlay,
  923. {
  924. ref: ref,
  925. placeholder: status === "loading",
  926. },
  927. status === "loading" && react$1.createElement(Spinner, null),
  928. react$1.createElement(
  929. Image1,
  930. Object.assign(
  931. {},
  932. src
  933. ? {
  934. src,
  935. }
  936. : {},
  937. imageProps,
  938. props,
  939. ),
  940. ),
  941. );
  942. };
  943.  
  944. const Viewer_ = (props, refHandle) => {
  945. const ref = react$1.useRef();
  946. const fullscreenElement = useFullscreenElement();
  947. const { promise: refPromise, resolve: resolveRef } = useDeferred();
  948. const [{ options, images, navigator, status, cancelDownload }, dispatch] =
  949. useViewerReducer(ref);
  950. const [{ value, text, error }, setProgress] = react$1.useState({
  951. value: 0,
  952. text: "",
  953. error: false,
  954. });
  955. const cache = {
  956. text: "",
  957. };
  958. const reportProgress = react$1.useCallback((event) => {
  959. const value = event.settled / images.length * 0.9 +
  960. event.zipPercent * 0.001;
  961. const text = `${(value * 100).toFixed(1)}%`;
  962. const error = !!event.rejected;
  963. if (value === 1 && !error || event.cancelled) {
  964. setProgress({
  965. value: 0,
  966. text: "",
  967. error: false,
  968. });
  969. } else if (text !== cache.text) {
  970. cache.text = text;
  971. setProgress({
  972. value,
  973. text,
  974. error,
  975. });
  976. }
  977. }, [
  978. images.length,
  979. ]);
  980. const navigate = react$1.useCallback((event) => {
  981. const height = ref.current?.clientHeight;
  982. if (!height || event.button !== 0) {
  983. return;
  984. }
  985. event.preventDefault();
  986. window.getSelection()?.empty?.();
  987. const isTop = event.clientY < height / 2;
  988. if (isTop) {
  989. dispatch({
  990. type: ActionType.GoPrevious,
  991. });
  992. } else {
  993. dispatch({
  994. type: ActionType.GoNext,
  995. });
  996. }
  997. }, []);
  998. const blockSelection = react$1.useCallback((event) => {
  999. if (event.detail >= 2) {
  1000. event.preventDefault();
  1001. }
  1002. }, []);
  1003. react$1.useImperativeHandle(refHandle, () => ({
  1004. refPromise,
  1005. goNext: () =>
  1006. dispatch({
  1007. type: ActionType.GoNext,
  1008. }),
  1009. goPrevious: () =>
  1010. dispatch({
  1011. type: ActionType.GoPrevious,
  1012. }),
  1013. toggleFullscreen: () =>
  1014. dispatch({
  1015. type: ActionType.ToggleFullscreen,
  1016. }),
  1017. setOptions: (options) =>
  1018. dispatch({
  1019. type: ActionType.SetState,
  1020. state: {
  1021. options,
  1022. },
  1023. }),
  1024. download: () =>
  1025. dispatch({
  1026. type: ActionType.Download,
  1027. options: {
  1028. onError: console.log,
  1029. onProgress: reportProgress,
  1030. },
  1031. }),
  1032. unmount: () =>
  1033. dispatch({
  1034. type: ActionType.Unmount,
  1035. }),
  1036. }), [
  1037. dispatch,
  1038. refPromise,
  1039. reportProgress,
  1040. ]);
  1041. react$1.useEffect(() => {
  1042. if (!ref.current) {
  1043. return;
  1044. }
  1045. ref.current?.focus?.();
  1046. resolveRef(ref.current);
  1047. }, [
  1048. ref.current,
  1049. ]);
  1050. react$1.useEffect(() => {
  1051. if (ref.current && fullscreenElement === ref.current) {
  1052. ref.current?.focus?.();
  1053. }
  1054. }, [
  1055. ref.current,
  1056. fullscreenElement,
  1057. ]);
  1058. react$1.useEffect(() => {
  1059. if (error || !text) {
  1060. return;
  1061. }
  1062. // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Example
  1063. const guard = (event) => {
  1064. event.preventDefault();
  1065. event.returnValue = "";
  1066. };
  1067. window.addEventListener("beforeunload", guard);
  1068. return () => window.removeEventListener("beforeunload", guard);
  1069. }, [
  1070. error || !text,
  1071. ]);
  1072. return react$1.createElement(
  1073. ScrollableLayout,
  1074. Object.assign({
  1075. ref: ref,
  1076. tabIndex: -1,
  1077. className: "vim_comic_viewer",
  1078. fullscreen: fullscreenElement === ref.current,
  1079. onClick: navigate,
  1080. onMouseDown: blockSelection,
  1081. }, props),
  1082. status === "complete"
  1083. ? images?.map?.((image, index) =>
  1084. react$1.createElement(
  1085. Page,
  1086. Object.assign({
  1087. key: index,
  1088. source: image,
  1089. observer: navigator.observer,
  1090. }, options?.imageProps),
  1091. )
  1092. ) || false
  1093. : react$1.createElement(
  1094. "p",
  1095. null,
  1096. status === "error" ? "에러가 발생했습니다" : "로딩 중...",
  1097. ),
  1098. !!text && react$1.createElement(CircularProgress, {
  1099. radius: 50,
  1100. strokeWidth: 10,
  1101. value: value,
  1102. text: text,
  1103. error: error,
  1104. onClick: cancelDownload,
  1105. }),
  1106. );
  1107. };
  1108. const Viewer = react$1.forwardRef(Viewer_);
  1109.  
  1110. var types = /*#__PURE__*/ Object.freeze({
  1111. __proto__: null,
  1112. });
  1113.  
  1114. /** @jsx createElement */
  1115. /// <reference lib="dom" />
  1116. const getDefaultRoot = async () => {
  1117. const div = document.createElement("div");
  1118. div.style.height = "100vh";
  1119. await waitBody(document);
  1120. document.body.append(div);
  1121. return div;
  1122. };
  1123. const initialize = (root) => {
  1124. const ref = react$1.createRef();
  1125. reactDom.render(
  1126. react$1.createElement(Viewer, {
  1127. ref: ref,
  1128. }),
  1129. root,
  1130. );
  1131. return new Proxy(ref, {
  1132. get: (target, ...args) => {
  1133. return Reflect.get(target.current, ...args);
  1134. },
  1135. });
  1136. };
  1137. const maybeNotHotkey = (event) =>
  1138. event.ctrlKey || event.shiftKey || event.altKey || isTyping(event);
  1139. const initializeWithDefault = async (source) => {
  1140. const root = source.getRoot?.() || await getDefaultRoot();
  1141. const controller = initialize(root);
  1142. const defaultKeyHandler = async (event) => {
  1143. if (maybeNotHotkey(event)) {
  1144. return;
  1145. }
  1146. switch (event.key) {
  1147. case "j":
  1148. controller.goNext();
  1149. break;
  1150. case "k":
  1151. controller.goPrevious();
  1152. break;
  1153. case ";": {
  1154. const zip = await controller.download();
  1155. if (!zip) {
  1156. return;
  1157. }
  1158. const blob = await zip.generateAsync({
  1159. type: "blob",
  1160. });
  1161. saveAs(blob, `${getSafeFileName(document.title)}.zip`);
  1162. break;
  1163. }
  1164. }
  1165. };
  1166. const defaultGlobalKeyHandler = (event) => {
  1167. if (maybeNotHotkey(event)) {
  1168. return;
  1169. }
  1170. if (event.key === "i") {
  1171. controller.toggleFullscreen();
  1172. }
  1173. };
  1174. controller.setOptions({
  1175. source: source.comicSource,
  1176. });
  1177. const div = await controller.refPromise;
  1178. if (source.withController) {
  1179. source.withController(controller, div);
  1180. } else {
  1181. div.addEventListener("keydown", defaultKeyHandler);
  1182. window.addEventListener("keydown", defaultGlobalKeyHandler);
  1183. }
  1184. return controller;
  1185. };
  1186.  
  1187. exports.download = download;
  1188. exports.initialize = initialize;
  1189. exports.initializeWithDefault = initializeWithDefault;
  1190. exports.transformToBlobUrl = transformToBlobUrl;
  1191. exports.types = types;
  1192. exports.utils = utils;