vim comic viewer

Universal comic reader

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

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

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