vim comic viewer

Universal comic reader

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

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

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