vim comic viewer

Universal comic reader

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

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

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