vim comic viewer

Universal comic reader

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

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

  1. // ==UserScript==
  2. // @name vim comic viewer
  3. // @description Universal comic reader
  4. // @version 3.4.2
  5. // @namespace https://greasyfork.org/en/users/713014-nanikit
  6. // @exclude *
  7. // @match http://unused-field.space/
  8. // @author nanikit
  9. // @license MIT
  10. // @resource jszip https://cdn.jsdelivr.net/npm/jszip@3.6.0/dist/jszip.min.js
  11. // @resource react https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js
  12. // @resource react-dom https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js
  13. // @resource @stitches/core https://cdn.jsdelivr.net/npm/@stitches/core@0.1.9/dist/index.cjs
  14. // @resource @stitches/react https://cdn.jsdelivr.net/npm/@stitches/react@0.1.9/dist/index.cjs
  15. // ==/UserScript==
  16.  
  17. "use strict";
  18.  
  19. Object.defineProperty(exports, "__esModule", { value: true });
  20.  
  21. var react$1 = require("react");
  22. var react = require("@stitches/react");
  23. var JSZip = require("jszip");
  24. var reactDom = require("react-dom");
  25.  
  26. function _interopDefaultLegacy(e) {
  27. return e && typeof e === "object" && "default" in e ? e : { "default": e };
  28. }
  29.  
  30. var JSZip__default = /*#__PURE__*/ _interopDefaultLegacy(JSZip);
  31.  
  32. const { styled, css, keyframes } = react.createCss({});
  33.  
  34. const Svg$1 = styled("svg", {
  35. position: "absolute",
  36. bottom: "8px",
  37. left: "8px",
  38. cursor: "pointer",
  39. ":hover": {
  40. filter: "hue-rotate(-145deg)",
  41. },
  42. variants: {
  43. error: {
  44. true: {
  45. filter: "hue-rotate(140deg)",
  46. },
  47. },
  48. },
  49. });
  50. const Circle = styled("circle", {
  51. transition: "stroke-dashoffset 0.35s",
  52. transform: "rotate(-90deg)",
  53. transformOrigin: "50% 50%",
  54. stroke: "url(#aEObn)",
  55. fill: "#fff8",
  56. });
  57. const GradientDef = /*#__PURE__*/ react$1.createElement(
  58. "defs",
  59. null,
  60. /*#__PURE__*/ react$1.createElement(
  61. "linearGradient",
  62. {
  63. id: "aEObn",
  64. x1: "100%",
  65. y1: "0%",
  66. x2: "0%",
  67. y2: "100%",
  68. },
  69. /*#__PURE__*/ react$1.createElement("stop", {
  70. offset: "0%",
  71. style: {
  72. stopColor: "#53baff",
  73. stopOpacity: 1,
  74. },
  75. }),
  76. /*#__PURE__*/ react$1.createElement("stop", {
  77. offset: "100%",
  78. style: {
  79. stopColor: "#0067bb",
  80. stopOpacity: 1,
  81. },
  82. }),
  83. ),
  84. );
  85. const CenterText = styled("text", {
  86. dominantBaseline: "middle",
  87. textAnchor: "middle",
  88. fontSize: "30px",
  89. fontWeight: "bold",
  90. fill: "#004b9e",
  91. });
  92. const CircularProgress = (props) => {
  93. const { radius, strokeWidth, value, text, ...otherProps } = props;
  94. const circumference = 2 * Math.PI * radius;
  95. const strokeDashoffset = circumference - value * circumference;
  96. const center = radius + strokeWidth / 2;
  97. const side = center * 2;
  98. return (/*#__PURE__*/ react$1.createElement(
  99. Svg$1,
  100. Object.assign({
  101. height: side,
  102. width: side,
  103. }, otherProps),
  104. GradientDef,
  105. /*#__PURE__*/ react$1.createElement(
  106. Circle,
  107. Object.assign({}, {
  108. strokeWidth,
  109. strokeDasharray: `${circumference} ${circumference}`,
  110. strokeDashoffset,
  111. r: radius,
  112. cx: center,
  113. cy: center,
  114. }),
  115. ),
  116. /*#__PURE__*/ react$1.createElement(CenterText, {
  117. x: "50%",
  118. y: "50%",
  119. }, text || ""),
  120. ));
  121. };
  122.  
  123. const Svg = styled("svg", {
  124. position: "absolute",
  125. width: "40px",
  126. bottom: "8px",
  127. opacity: "50%",
  128. filter: "drop-shadow(0 0 1px white) drop-shadow(0 0 1px white)",
  129. color: "black",
  130. ":hover": {
  131. opacity: "100%",
  132. transform: "scale(1.1)",
  133. },
  134. });
  135. const downloadCss = {
  136. left: "8px",
  137. };
  138. const fullscreenCss = {
  139. right: "24px",
  140. };
  141. const DownloadIcon = (props) =>
  142. /*#__PURE__*/ react$1.createElement(
  143. Svg,
  144. Object.assign({
  145. version: "1.1",
  146. xmlns: "http://www.w3.org/2000/svg",
  147. x: "0px",
  148. y: "0px",
  149. viewBox: "0 -34.51 122.88 122.87",
  150. css: downloadCss,
  151. }, props),
  152. /*#__PURE__*/ react$1.createElement(
  153. "g",
  154. null,
  155. /*#__PURE__*/ react$1.createElement("path", {
  156. 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",
  157. }),
  158. ),
  159. );
  160. const FullscreenIcon = (props) =>
  161. /*#__PURE__*/ react$1.createElement(
  162. Svg,
  163. Object.assign({
  164. version: "1.1",
  165. xmlns: "http://www.w3.org/2000/svg",
  166. x: "0px",
  167. y: "0px",
  168. viewBox: "0 0 122.88 122.87",
  169. css: fullscreenCss,
  170. }, props),
  171. /*#__PURE__*/ react$1.createElement(
  172. "g",
  173. null,
  174. /*#__PURE__*/ react$1.createElement("path", {
  175. 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",
  176. }),
  177. ),
  178. );
  179.  
  180. const Container = styled("div", {
  181. position: "relative",
  182. height: "100%",
  183. });
  184. const ScrollableLayout = styled("div", {
  185. // chrome user-agent style override
  186. outline: 0,
  187. position: "relative",
  188. backgroundColor: "#eee",
  189. width: "100%",
  190. height: "100%",
  191. display: "flex",
  192. justifyContent: "center",
  193. alignItems: "center",
  194. flexFlow: "row-reverse wrap",
  195. overflowY: "auto",
  196. variants: {
  197. fullscreen: {
  198. true: {
  199. display: "flex",
  200. position: "fixed",
  201. top: 0,
  202. bottom: 0,
  203. overflow: "auto",
  204. },
  205. },
  206. },
  207. });
  208.  
  209. const timeout = (millisecond) =>
  210. new Promise((resolve) => setTimeout(resolve, millisecond));
  211. const waitDomContent = (document) =>
  212. document.readyState === "loading"
  213. ? new Promise((r) =>
  214. document.addEventListener("readystatechange", r, {
  215. once: true,
  216. })
  217. )
  218. : true;
  219. const insertCss = (css) => {
  220. const style = document.createElement("style");
  221. style.innerHTML = css;
  222. document.head.append(style);
  223. };
  224. const isTyping = (event) =>
  225. event.target?.tagName?.match?.(/INPUT|TEXTAREA/) ||
  226. event.target?.isContentEditable;
  227. const saveAs = async (blob, name) => {
  228. const a = document.createElement("a");
  229. a.download = name;
  230. a.rel = "noopener";
  231. a.href = URL.createObjectURL(blob);
  232. a.click();
  233. await timeout(40000);
  234. URL.revokeObjectURL(a.href);
  235. };
  236. const getSafeFileName = (str) => {
  237. return str.replace(/[<>:"/\\|?*\x00-\x1f]+/gi, "").trim() || "download";
  238. };
  239. const saveZipAs = async (zip) => {
  240. if (!zip) {
  241. return;
  242. }
  243. const blob = await zip.generateAsync({
  244. type: "blob",
  245. });
  246. return saveAs(blob, `${getSafeFileName(document.title)}.zip`);
  247. };
  248. const defer = () => {
  249. let resolve, reject;
  250. const promise = new Promise((res, rej) => {
  251. resolve = res;
  252. reject = rej;
  253. });
  254. return {
  255. promise,
  256. resolve,
  257. reject,
  258. };
  259. };
  260.  
  261. var utils = /*#__PURE__*/ Object.freeze({
  262. __proto__: null,
  263. timeout: timeout,
  264. waitDomContent: waitDomContent,
  265. insertCss: insertCss,
  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 = (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$1 = (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. }
  704. return state;
  705. };
  706. const getAsyncReducer$1 = (dispatch) => {
  707. let images = [];
  708. let cancelDownload;
  709. const setInnerState = (state) => {
  710. dispatch({
  711. type: ActionType.SetState,
  712. state,
  713. });
  714. };
  715. const setState = async (state) => {
  716. const source = state.options?.source;
  717. if (source) {
  718. try {
  719. setInnerState({
  720. status: "loading",
  721. images: [],
  722. });
  723. images = await source();
  724. if (!Array.isArray(images)) {
  725. console.log(`Invalid comic source type: ${typeof images}`);
  726. setInnerState({
  727. status: "error",
  728. });
  729. return;
  730. }
  731. setInnerState({
  732. status: "complete",
  733. images,
  734. });
  735. } catch (error) {
  736. setInnerState({
  737. status: "error",
  738. });
  739. console.log(error);
  740. throw error;
  741. }
  742. } else {
  743. setInnerState(state);
  744. }
  745. };
  746. const clearCancel = () => {
  747. setInnerState({
  748. cancelDownload: undefined,
  749. });
  750. cancelDownload = undefined;
  751. };
  752. const startDownload = async (options) => {
  753. if (cancelDownload) {
  754. cancelDownload();
  755. clearCancel();
  756. return;
  757. }
  758. if (!images.length) {
  759. return;
  760. }
  761. const { zip, cancel } = await download(images, options);
  762. cancelDownload = () => {
  763. cancel();
  764. clearCancel();
  765. };
  766. setInnerState({
  767. cancelDownload,
  768. });
  769. const result = await zip;
  770. clearCancel();
  771. return result;
  772. };
  773. return (action) => {
  774. switch (action.type) {
  775. case ActionType.Download:
  776. return startDownload(action.options);
  777. case ActionType.SetState:
  778. return setState(action.state);
  779. default:
  780. return dispatch(action);
  781. }
  782. };
  783. };
  784. const useViewerReducer = (ref, scrollRef) => {
  785. const navigator = usePageNavigator(scrollRef.current);
  786. const [state, dispatch] = react$1.useReducer(reducer$1, {
  787. ref,
  788. navigator,
  789. options: {},
  790. images: [],
  791. status: "loading",
  792. });
  793. const [asyncDispatch] = react$1.useState(() => getAsyncReducer$1(dispatch));
  794. react$1.useEffect(() => {
  795. dispatch({
  796. type: ActionType.SetState,
  797. state: {
  798. navigator,
  799. },
  800. });
  801. }, [
  802. navigator,
  803. ]);
  804. return [
  805. state,
  806. asyncDispatch,
  807. ];
  808. };
  809.  
  810. const stretch = keyframes({
  811. "0%": {
  812. top: "8px",
  813. height: "64px",
  814. },
  815. "50%": {
  816. top: "24px",
  817. height: "32px",
  818. },
  819. "100%": {
  820. top: "24px",
  821. height: "32px",
  822. },
  823. });
  824. const SpinnerContainer = styled("div", {
  825. position: "absolute",
  826. left: "0",
  827. top: "0",
  828. right: "0",
  829. bottom: "0",
  830. margin: "auto",
  831. display: "flex",
  832. justifyContent: "center",
  833. alignItems: "center",
  834. div: {
  835. display: "inline-block",
  836. width: "16px",
  837. margin: "0 4px",
  838. background: "#fff",
  839. animation: `${stretch} 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite`,
  840. },
  841. "div:nth-child(1)": {
  842. "animation-delay": "-0.24s",
  843. },
  844. "div:nth-child(2)": {
  845. "animation-delay": "-0.12s",
  846. },
  847. "div:nth-child(3)": {
  848. "animation-delay": "0",
  849. },
  850. });
  851. const Spinner = () =>
  852. /*#__PURE__*/ react$1.createElement(
  853. SpinnerContainer,
  854. null,
  855. /*#__PURE__*/ react$1.createElement("div", null),
  856. /*#__PURE__*/ react$1.createElement("div", null),
  857. /*#__PURE__*/ react$1.createElement("div", null),
  858. );
  859. const Overlay = styled("div", {
  860. position: "relative",
  861. maxWidth: "100%",
  862. height: "100%",
  863. variants: {
  864. placeholder: {
  865. true: {
  866. width: "45%",
  867. },
  868. },
  869. },
  870. margin: "4px 1px",
  871. "@media print": {
  872. margin: 0,
  873. },
  874. });
  875. const Image1 = styled("img", {
  876. position: "relative",
  877. height: "100%",
  878. objectFit: "contain",
  879. maxWidth: "100%",
  880. });
  881.  
  882. var PageActionType;
  883. (function (PageActionType) {
  884. PageActionType[PageActionType["SetState"] = 0] = "SetState";
  885. PageActionType[PageActionType["SetSource"] = 1] = "SetSource";
  886. PageActionType[PageActionType["Fallback"] = 2] = "Fallback";
  887. })(PageActionType || (PageActionType = {}));
  888. const reducer = (state, action) => {
  889. switch (action.type) {
  890. case PageActionType.SetState:
  891. return {
  892. ...state,
  893. ...action.state,
  894. };
  895. default:
  896. return state;
  897. }
  898. };
  899. const getAsyncReducer = (dispatch) => {
  900. const empty = async function* () {
  901. }();
  902. let iterator = empty;
  903. const setState = (state) => {
  904. dispatch({
  905. type: PageActionType.SetState,
  906. state,
  907. });
  908. };
  909. const takeNext = async () => {
  910. const snapshot = iterator;
  911. try {
  912. const item = await snapshot.next();
  913. if (snapshot !== iterator) {
  914. return;
  915. }
  916. if (item.done) {
  917. setState({
  918. src: undefined,
  919. status: "error",
  920. });
  921. } else {
  922. setState({
  923. src: item.value,
  924. status: "loading",
  925. });
  926. }
  927. } catch (error) {
  928. console.error(error);
  929. setState({
  930. src: undefined,
  931. status: "error",
  932. });
  933. }
  934. };
  935. const setSource = async (source) => {
  936. iterator = imageSourceToIterable(source)[Symbol.asyncIterator]();
  937. await takeNext();
  938. };
  939. return (action) => {
  940. switch (action.type) {
  941. case PageActionType.SetSource:
  942. return setSource(action.source);
  943. case PageActionType.Fallback:
  944. return takeNext();
  945. default:
  946. return dispatch(action);
  947. }
  948. };
  949. };
  950. const usePageReducer = (source) => {
  951. const [state, dispatch] = react$1.useReducer(reducer, {
  952. status: "loading",
  953. });
  954. const [asyncDispatch] = react$1.useState(() => getAsyncReducer(dispatch));
  955. const onError = react$1.useCallback(() => {
  956. asyncDispatch({
  957. type: PageActionType.Fallback,
  958. });
  959. }, []);
  960. const onLoad = react$1.useCallback(() => {
  961. asyncDispatch({
  962. type: PageActionType.SetState,
  963. state: {
  964. status: "complete",
  965. },
  966. });
  967. }, []);
  968. react$1.useEffect(() => {
  969. asyncDispatch({
  970. type: PageActionType.SetSource,
  971. source,
  972. });
  973. }, [
  974. source,
  975. ]);
  976. return [
  977. {
  978. ...state,
  979. onLoad,
  980. onError,
  981. },
  982. asyncDispatch,
  983. ];
  984. };
  985.  
  986. const Page = ({ source, observer, ...props }) => {
  987. const [{ status, src, ...imageProps }] = usePageReducer(source);
  988. const ref = react$1.useRef();
  989. react$1.useEffect(() => {
  990. const target = ref.current;
  991. if (target && observer) {
  992. observer.observe(target);
  993. return () => observer.unobserve(target);
  994. }
  995. }, [
  996. observer,
  997. ref.current,
  998. ]);
  999. return (/*#__PURE__*/ react$1.createElement(
  1000. Overlay,
  1001. {
  1002. ref: ref,
  1003. placeholder: status === "loading",
  1004. },
  1005. status === "loading" && /*#__PURE__*/ react$1.createElement(Spinner, null),
  1006. /*#__PURE__*/ react$1.createElement(
  1007. Image1,
  1008. Object.assign(
  1009. {},
  1010. src
  1011. ? {
  1012. src,
  1013. }
  1014. : {},
  1015. imageProps,
  1016. props,
  1017. ),
  1018. ),
  1019. ));
  1020. };
  1021.  
  1022. const Viewer_ = (props, refHandle) => {
  1023. const ref = react$1.useRef();
  1024. const scrollRef = react$1.useRef();
  1025. const fullscreenElement = useFullscreenElement();
  1026. const { promise: refPromise, resolve: resolveRef } = useDeferred();
  1027. const [{ options, images, navigator, status, cancelDownload }, dispatch] =
  1028. useViewerReducer(ref, scrollRef);
  1029. const [{ value, text, error }, setProgress] = react$1.useState({
  1030. value: 0,
  1031. text: "",
  1032. error: false,
  1033. });
  1034. const cache = {
  1035. text: "",
  1036. };
  1037. const reportProgress = react$1.useCallback((event) => {
  1038. const { total, started, settled, rejected, isCancelled, zipPercent } =
  1039. event;
  1040. const value = started / total * 0.1 + settled / total * 0.7 +
  1041. zipPercent * 0.002;
  1042. const text = `${(value * 100).toFixed(1)}%`;
  1043. const error = !!rejected;
  1044. if (value === 1 && !error || isCancelled) {
  1045. setProgress({
  1046. value: 0,
  1047. text: "",
  1048. error: false,
  1049. });
  1050. } else if (text !== cache.text) {
  1051. cache.text = text;
  1052. setProgress({
  1053. value,
  1054. text,
  1055. error,
  1056. });
  1057. }
  1058. }, []);
  1059. const navigate = react$1.useCallback((event) => {
  1060. const height = ref.current?.clientHeight;
  1061. if (!height || event.button !== 0) {
  1062. return;
  1063. }
  1064. event.preventDefault();
  1065. const isTop = event.clientY < height / 2;
  1066. if (isTop) {
  1067. dispatch({
  1068. type: ActionType.GoPrevious,
  1069. });
  1070. } else {
  1071. dispatch({
  1072. type: ActionType.GoNext,
  1073. });
  1074. }
  1075. }, []);
  1076. const blockSelection = react$1.useCallback((event) => {
  1077. if (event.detail >= 2) {
  1078. event.preventDefault();
  1079. }
  1080. if (event.buttons === 3) {
  1081. dispatch({
  1082. type: ActionType.ToggleFullscreen,
  1083. });
  1084. event.preventDefault();
  1085. }
  1086. }, []);
  1087. const toggleFullscreen = react$1.useCallback(() => {
  1088. dispatch({
  1089. type: ActionType.ToggleFullscreen,
  1090. });
  1091. }, []);
  1092. const download = react$1.useCallback(() => {
  1093. return dispatch({
  1094. type: ActionType.Download,
  1095. options: {
  1096. onError: console.log,
  1097. onProgress: reportProgress,
  1098. },
  1099. });
  1100. }, [
  1101. reportProgress,
  1102. ]);
  1103. const downloadAndSave = react$1.useCallback(async () => {
  1104. const zip = await download();
  1105. await saveZipAs(zip);
  1106. }, [
  1107. download,
  1108. ]);
  1109. react$1.useImperativeHandle(refHandle, () => ({
  1110. refPromise,
  1111. setOptions: (options) =>
  1112. dispatch({
  1113. type: ActionType.SetState,
  1114. state: {
  1115. options,
  1116. },
  1117. }),
  1118. goNext: () =>
  1119. dispatch({
  1120. type: ActionType.GoNext,
  1121. }),
  1122. goPrevious: () =>
  1123. dispatch({
  1124. type: ActionType.GoPrevious,
  1125. }),
  1126. toggleFullscreen,
  1127. downloadAndSave,
  1128. download,
  1129. unmount: () =>
  1130. dispatch({
  1131. type: ActionType.Unmount,
  1132. }),
  1133. }), [
  1134. dispatch,
  1135. refPromise,
  1136. downloadAndSave,
  1137. ]);
  1138. react$1.useEffect(() => {
  1139. if (!ref.current) {
  1140. return;
  1141. }
  1142. ref.current?.focus?.();
  1143. resolveRef(ref.current);
  1144. }, [
  1145. ref.current,
  1146. ]);
  1147. react$1.useEffect(() => {
  1148. if (ref.current && fullscreenElement === ref.current) {
  1149. ref.current?.focus?.();
  1150. }
  1151. }, [
  1152. ref.current,
  1153. fullscreenElement,
  1154. ]);
  1155. react$1.useEffect(() => {
  1156. if (error || !text) {
  1157. return;
  1158. }
  1159. // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Example
  1160. const guard = (event) => {
  1161. event.preventDefault();
  1162. event.returnValue = "";
  1163. };
  1164. window.addEventListener("beforeunload", guard);
  1165. return () => window.removeEventListener("beforeunload", guard);
  1166. }, [
  1167. error || !text,
  1168. ]);
  1169. return (/*#__PURE__*/ react$1.createElement(
  1170. Container,
  1171. {
  1172. ref: ref,
  1173. tabIndex: -1,
  1174. className: "vim_comic_viewer",
  1175. },
  1176. /*#__PURE__*/ react$1.createElement(
  1177. ScrollableLayout,
  1178. Object.assign({
  1179. ref: scrollRef,
  1180. fullscreen: fullscreenElement === ref.current,
  1181. onClick: navigate,
  1182. onMouseDown: blockSelection,
  1183. }, props),
  1184. status === "complete"
  1185. ? images?.map?.((image, index) =>
  1186. /*#__PURE__*/ react$1.createElement(
  1187. Page,
  1188. Object.assign({
  1189. key: index,
  1190. source: image,
  1191. observer: navigator.observer,
  1192. }, options?.imageProps),
  1193. )
  1194. ) || false
  1195. : /*#__PURE__*/ react$1.createElement(
  1196. "p",
  1197. null,
  1198. status === "error" ? "에러가 발생했습니다" : "로딩 중...",
  1199. ),
  1200. ),
  1201. /*#__PURE__*/ react$1.createElement(FullscreenIcon, {
  1202. onClick: toggleFullscreen,
  1203. }),
  1204. text
  1205. ? /*#__PURE__*/ react$1.createElement(CircularProgress, {
  1206. radius: 50,
  1207. strokeWidth: 10,
  1208. value: value,
  1209. text: text,
  1210. error: error,
  1211. onClick: cancelDownload,
  1212. })
  1213. : /*#__PURE__*/ react$1.createElement(DownloadIcon, {
  1214. onClick: downloadAndSave,
  1215. }),
  1216. ));
  1217. };
  1218. const Viewer = /*#__PURE__*/ react$1.forwardRef(Viewer_);
  1219.  
  1220. var types = /*#__PURE__*/ Object.freeze({
  1221. __proto__: null,
  1222. });
  1223.  
  1224. /** @jsx createElement */
  1225. /// <reference lib="dom" />
  1226. const initialize = (root) => {
  1227. const ref = /*#__PURE__*/ react$1.createRef();
  1228. reactDom.render(
  1229. /*#__PURE__*/ react$1.createElement(Viewer, {
  1230. ref: ref,
  1231. }),
  1232. root,
  1233. );
  1234. return new Proxy(ref, {
  1235. get: (target, ...args) => {
  1236. return Reflect.get(target.current, ...args);
  1237. },
  1238. });
  1239. };
  1240. const maybeNotHotkey = (event) =>
  1241. event.ctrlKey || event.shiftKey || event.altKey || isTyping(event);
  1242. const getDefaultRoot = async () => {
  1243. const div = document.createElement("div");
  1244. div.setAttribute(
  1245. "style",
  1246. "width: 0; height: 0; position: fixed; top: 0; bottom: 0;",
  1247. );
  1248. document.body.append(div);
  1249. return div;
  1250. };
  1251. const initializeWithDefault = async (source) => {
  1252. const root = source.getRoot?.() || await getDefaultRoot();
  1253. const controller = initialize(root);
  1254. const defaultKeyHandler = async (event) => {
  1255. if (maybeNotHotkey(event)) {
  1256. return;
  1257. }
  1258. switch (event.key) {
  1259. case "j":
  1260. controller.goNext();
  1261. break;
  1262. case "k":
  1263. controller.goPrevious();
  1264. break;
  1265. case ";": {
  1266. await controller.downloadAndSave();
  1267. break;
  1268. }
  1269. }
  1270. };
  1271. const defaultGlobalKeyHandler = (event) => {
  1272. if (maybeNotHotkey(event)) {
  1273. return;
  1274. }
  1275. if (event.key === "i") {
  1276. controller.toggleFullscreen();
  1277. }
  1278. };
  1279. controller.setOptions({
  1280. source: source.comicSource,
  1281. });
  1282. const div = await controller.refPromise;
  1283. if (source.withController) {
  1284. source.withController(controller, div);
  1285. } else {
  1286. div.addEventListener("keydown", defaultKeyHandler);
  1287. window.addEventListener("keydown", defaultGlobalKeyHandler);
  1288. }
  1289. return controller;
  1290. };
  1291.  
  1292. exports.download = download;
  1293. exports.initialize = initialize;
  1294. exports.initializeWithDefault = initializeWithDefault;
  1295. exports.transformToBlobUrl = transformToBlobUrl;
  1296. exports.types = types;
  1297. exports.utils = utils;