vim comic viewer

Universal comic reader

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

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

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