vim comic viewer

Universal comic reader

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

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

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