vim comic viewer

Universal comic reader

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name           vim comic viewer
// @description    Universal comic reader
// @name:ko        vim comic viewer
// @description:ko 만화 뷰어 라이브러리
// @version        6.2.0
// @namespace      https://greasyfork.org/en/users/713014-nanikit
// @exclude        *
// @match          http://unused-field.space/
// @author         nanikit
// @license        MIT
// @resource       fflate           https://cdn.jsdelivr.net/npm/[email protected]/lib/browser.cjs
// @resource       react            https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js
// @resource       react-dom        https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js
// @resource       @stitches/core   https://cdn.jsdelivr.net/npm/@stitches/[email protected]/dist/index.cjs
// @resource       @stitches/react  https://cdn.jsdelivr.net/npm/@stitches/[email protected]/dist/index.cjs
// ==/UserScript==

"use strict";

Object.defineProperty(exports, "__esModule", { value: true });

var react$1 = require("react");
var react = require("@stitches/react");
var reactDom = require("react-dom");
var fflate = require("fflate");

const { styled, css, keyframes } = react.createCss({});

const Svg$1 = styled("svg", {
  position: "absolute",
  width: "40px",
  bottom: "8px",
  opacity: "50%",
  filter: "drop-shadow(0 0 1px white) drop-shadow(0 0 1px white)",
  color: "black",
  cursor: "pointer",
  "&:hover": {
    opacity: "100%",
    transform: "scale(1.1)",
  },
});
const downloadCss = {
  left: "8px",
};
const fullscreenCss = {
  right: "24px",
};
const DownloadIcon = (props) =>
  /*#__PURE__*/ react$1.createElement(
    Svg$1,
    Object.assign({
      version: "1.1",
      xmlns: "http://www.w3.org/2000/svg",
      x: "0px",
      y: "0px",
      viewBox: "0 -34.51 122.88 122.87",
      css: downloadCss,
    }, props),
    /*#__PURE__*/ react$1.createElement(
      "g",
      null,
      /*#__PURE__*/ react$1.createElement("path", {
        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",
      }),
    ),
  );
const FullscreenIcon = (props) =>
  /*#__PURE__*/ react$1.createElement(
    Svg$1,
    Object.assign({
      version: "1.1",
      xmlns: "http://www.w3.org/2000/svg",
      x: "0px",
      y: "0px",
      viewBox: "0 0 122.88 122.87",
      css: fullscreenCss,
    }, props),
    /*#__PURE__*/ react$1.createElement(
      "g",
      null,
      /*#__PURE__*/ react$1.createElement("path", {
        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",
      }),
    ),
  );
const ErrorIcon = styled("svg", {
  width: "10vmin",
  height: "10vmin",
  fill: "hsl(0, 50%, 20%)",
  margin: "2rem",
});
const CircledX = (props) => {
  return (/*#__PURE__*/ react$1.createElement(
    ErrorIcon,
    Object.assign(
      {
        version: "1.1",
        id: "Layer_1",
        xmlns: "http://www.w3.org/2000/svg",
        x: "0px",
        y: "0px",
        viewBox: "0 0 122.881 122.88",
        "enable-background": "new 0 0 122.881 122.88",
      },
      props,
      {
        crossOrigin: "",
      },
    ),
    /*#__PURE__*/ react$1.createElement(
      "g",
      null,
      /*#__PURE__*/ react$1.createElement("path", {
        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",
      }),
    ),
  ));
};

const defaultScrollbar = {
  "scrollbarWidth": "initial",
  "scrollbarColor": "initial",
  "&::-webkit-scrollbar": {
    all: "initial",
  },
  "&::-webkit-scrollbar-thumb": {
    all: "initial",
    background: "gray",
  },
  "&::-webkit-scrollbar-track": {
    all: "initial",
  },
};
const Container = styled("div", {
  position: "relative",
  height: "100%",
  ...defaultScrollbar,
});
const ScrollableLayout = styled("div", {
  // chrome user-agent style override
  outline: 0,
  position: "relative",
  backgroundColor: "#eee",
  width: "100%",
  height: "100%",
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
  flexFlow: "row-reverse wrap",
  overflowY: "auto",
  ...defaultScrollbar,
  variants: {
    fullscreen: {
      true: {
        display: "flex",
        position: "fixed",
        top: 0,
        bottom: 0,
        overflow: "auto",
      },
    },
  },
});

const useFullscreenElement = () => {
  const [element, setElement] = react$1.useState(
    document.fullscreenElement || undefined,
  );
  react$1.useEffect(() => {
    const notify = () => setElement(document.fullscreenElement || undefined);
    document.addEventListener("fullscreenchange", notify);
    return () => document.removeEventListener("fullscreenchange", notify);
  }, []);
  return element;
};

const useIntersectionObserver = (callback, options) => {
  const [observer, setObserver] = react$1.useState();
  react$1.useEffect(() => {
    const newObserver = new IntersectionObserver(callback, options);
    setObserver(newObserver);
    return () => newObserver.disconnect();
  }, [
    callback,
    options,
  ]);
  return observer;
};
const useIntersection = (callback, options) => {
  const memo = react$1.useRef(new Map());
  const filterIntersections = react$1.useCallback((newEntries) => {
    const memoized = memo.current;
    for (const entry of newEntries) {
      if (entry.isIntersecting) {
        memoized.set(entry.target, entry);
      } else {
        memoized.delete(entry.target);
      }
    }
    callback([
      ...memoized.values(),
    ]);
  }, [
    callback,
  ]);
  return useIntersectionObserver(filterIntersections, options);
};

const useResize = (target, transformer) => {
  const [value, setValue] = react$1.useState(() => transformer(undefined));
  const callbackRef = react$1.useRef(transformer);
  callbackRef.current = transformer;
  react$1.useEffect(() => {
    if (!target) {
      return;
    }
    const observer = new ResizeObserver((entries) => {
      setValue(callbackRef.current(entries[0]));
    });
    observer.observe(target);
    return () => observer.disconnect();
  }, [
    target,
    callbackRef,
  ]);
  return value;
};
const getCurrentPage = (container, entries) => {
  if (!entries.length) {
    return container.firstElementChild || undefined;
  }
  const children = [
    ...container.children,
  ];
  const fullyVisibles = entries.filter((x) => x.intersectionRatio === 1);
  if (fullyVisibles.length) {
    fullyVisibles.sort((a, b) => {
      return children.indexOf(a.target) - children.indexOf(b.target);
    });
    return fullyVisibles[Math.floor(fullyVisibles.length / 2)].target;
  }
  return entries.sort((a, b) => {
    const ratio = {
      a: a.intersectionRatio,
      b: b.intersectionRatio,
    };
    const index = {
      a: children.indexOf(a.target),
      b: children.indexOf(b.target),
    };
    return (ratio.b - ratio.a) * 10000 + (index.a - index.b);
  })[0].target;
};
const makePageNavigator = (ref) => {
  let currentPage;
  let ratio;
  let ignoreIntersection = false;
  const resetAnchor = (entries) => {
    const container = ref.current;
    if (!container?.clientHeight || entries.length === 0) {
      return;
    }
    if (ignoreIntersection) {
      ignoreIntersection = false;
      return;
    }
    const page = getCurrentPage(container, entries);
    const y = container.scrollTop + container.clientHeight / 2;
    currentPage = page;
    ratio = (y - page.offsetTop) / page.clientHeight;
  };
  const goNext = () => {
    ignoreIntersection = false;
    if (!currentPage) {
      return;
    }
    const originBound = currentPage.getBoundingClientRect();
    let cursor = currentPage;
    while (cursor.nextElementSibling) {
      const next = cursor.nextElementSibling;
      const nextBound = next.getBoundingClientRect();
      if (originBound.bottom < nextBound.top) {
        next.scrollIntoView({
          block: "center",
        });
        break;
      }
      cursor = next;
    }
  };
  const goPrevious = () => {
    ignoreIntersection = false;
    if (!currentPage) {
      return;
    }
    const originBound = currentPage.getBoundingClientRect();
    let cursor = currentPage;
    while (cursor.previousElementSibling) {
      const previous = cursor.previousElementSibling;
      const previousBound = previous.getBoundingClientRect();
      if (previousBound.bottom < originBound.top) {
        previous.scrollIntoView({
          block: "center",
        });
        break;
      }
      cursor = previous;
    }
  };
  const restoreScroll = () => {
    const container = ref.current;
    if (!container || ratio === undefined || currentPage === undefined) {
      return;
    }
    const restoredY = currentPage.offsetTop +
      currentPage.clientHeight * (ratio - 0.5);
    container.scroll({
      top: restoredY,
    });
    ignoreIntersection = true;
  };
  const intersectionOption = {
    threshold: [
      0.01,
      0.5,
      1,
    ],
  };
  let observer;
  const useInstance = () => {
    observer = useIntersection(resetAnchor, intersectionOption);
    useResize(ref.current, restoreScroll);
  };
  return {
    get observer() {
      return observer;
    },
    goNext,
    goPrevious,
    useInstance,
  };
};
const usePageNavigator = (ref) => {
  const navigator = react$1.useMemo(() => makePageNavigator(ref), [
    ref,
  ]);
  navigator.useInstance();
  return navigator;
};

const useRerender = () => {
  const [, rerender] = react$1.useReducer(() => ({}), {});
  return rerender;
};

const GM_xmlhttpRequest = module.config().GM_xmlhttpRequest;

const fetchBlob = async (url, init) => {
  try {
    const response = await fetch(url, init);
    return await response.blob();
  } catch (error) {
    if (init?.signal?.aborted) {
      throw error;
    }
    const isOriginDifferent = new URL(url).origin !== location.origin;
    if (isOriginDifferent && gmFetch) {
      return await gmFetch(url, init).blob();
    } else {
      throw error;
    }
  }
};
const gmFetch = GM_xmlhttpRequest
  ? (resource, init) => {
    const method = init?.body ? "POST" : "GET";
    const xhr = (type) => {
      return new Promise((resolve, reject) => {
        const request = GM_xmlhttpRequest({
          method,
          url: resource,
          headers: init?.headers,
          responseType: type === "text" ? undefined : type,
          data: init?.body,
          onload: (response) => {
            if (type === "text") {
              resolve(response.responseText);
            } else {
              resolve(response.response);
            }
          },
          onerror: reject,
          onabort: reject,
        });
        init?.signal?.addEventListener("abort", () => {
          request.abort();
        }, {
          once: true,
        });
      });
    };
    return {
      blob: () => xhr("blob"),
      json: () => xhr("json"),
      text: () => xhr("text"),
    };
  }
  : undefined;

const imageSourceToIterable = (source) => {
  if (typeof source === "string") {
    return (async function* () {
      yield source;
    })();
  } else if (Array.isArray(source)) {
    return (async function* () {
      for (const url of source) {
        yield url;
      }
    })();
  } else {
    return source();
  }
};
const transformToBlobUrl = (source) =>
  async () => {
    const imageSources = await source();
    return imageSources.map((imageSource) =>
      async function* () {
        for await (const url of imageSourceToIterable(imageSource)) {
          try {
            const blob = await fetchBlob(url);
            yield URL.createObjectURL(blob);
          } catch (error) {
            console.log(error);
          }
        }
      }
    );
  };

const timeout = (millisecond) =>
  new Promise((resolve) => setTimeout(resolve, millisecond));
const waitDomContent = (document) =>
  document.readyState === "loading"
    ? new Promise((r) =>
      document.addEventListener("readystatechange", r, {
        once: true,
      })
    )
    : true;
const insertCss = (css) => {
  const style = document.createElement("style");
  style.innerHTML = css;
  document.head.append(style);
};
const isTyping = (event) =>
  event.target?.tagName?.match?.(/INPUT|TEXTAREA/) ||
  event.target?.isContentEditable;
const saveAs = async (blob, name) => {
  const a = document.createElement("a");
  a.download = name;
  a.rel = "noopener";
  a.href = URL.createObjectURL(blob);
  a.click();
  await timeout(40000);
  URL.revokeObjectURL(a.href);
};
const getSafeFileName = (str) => {
  return str.replace(/[<>:"/\\|?*\x00-\x1f]+/gi, "").trim() || "download";
};
const save = async (blob) => {
  return saveAs(blob, `${getSafeFileName(document.title)}.zip`);
};
const defer = () => {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return {
    promise,
    resolve,
    reject,
  };
};

var utils = /*#__PURE__*/ Object.freeze({
  __proto__: null,
  timeout: timeout,
  waitDomContent: waitDomContent,
  insertCss: insertCss,
  isTyping: isTyping,
  saveAs: saveAs,
  getSafeFileName: getSafeFileName,
  save: save,
  defer: defer,
});

const isGmCancelled = (error) => {
  return error instanceof Function;
};
async function* downloadImage({ source, signal }) {
  for await (const url of imageSourceToIterable(source)) {
    if (signal?.aborted) {
      break;
    }
    try {
      const blob = await fetchBlob(url, {
        signal,
      });
      yield {
        url,
        blob,
      };
    } catch (error) {
      if (isGmCancelled(error)) {
        yield {
          error: new Error("download aborted"),
        };
      } else {
        yield {
          error,
        };
      }
    }
  }
}
const getExtension = (url) => {
  if (!url) {
    return ".txt";
  }
  const extension = url.match(/\.[^/?#]{3,4}?(?=[?#]|$)/);
  return extension?.[0] || ".jpg";
};
const guessExtension = (array) => {
  const { 0: a, 1: b, 2: c, 3: d } = array;
  if (a === 255 && b === 216 && c === 255) {
    return ".jpg";
  }
  if (a === 137 && b === 80 && c === 78 && d === 71) {
    return ".png";
  }
  if (a === 82 && b === 73 && c === 70 && d === 70) {
    return ".webp";
  }
  if (a === 71 && b === 73 && c === 70 && d === 56) {
    return ".gif";
  }
};
const download = (images, options) => {
  const { onError, onProgress, signal } = options || {};
  let startedCount = 0;
  let resolvedCount = 0;
  let rejectedCount = 0;
  let isCancelled = false;
  const reportProgress = (additionals = {}) => {
    const total = images.length;
    const settled = resolvedCount + rejectedCount;
    onProgress?.({
      total,
      started: startedCount,
      settled,
      rejected: rejectedCount,
      isCancelled,
      ...additionals,
    });
  };
  const downloadWithReport = async (source) => {
    const errors = [];
    startedCount++;
    reportProgress();
    for await (
      const event of downloadImage({
        source,
        signal,
      })
    ) {
      if ("error" in event) {
        errors.push(event.error);
        onError?.(event.error);
        continue;
      }
      if (event.url) {
        resolvedCount++;
      } else {
        rejectedCount++;
      }
      reportProgress();
      return event;
    }
    return {
      url: "",
      blob: new Blob([
        errors.map((x) => `${x}`).join("\n\n"),
      ]),
    };
  };
  const cipher = Math.floor(Math.log10(images.length)) + 1;
  const toPair = async ({ url, blob }, index) => {
    const array = new Uint8Array(await blob.arrayBuffer());
    const pad = `${index}`.padStart(cipher, "0");
    const name = `${pad}${guessExtension(array) ?? getExtension(url)}`;
    return {
      [name]: array,
    };
  };
  const archiveWithReport = async (sources) => {
    const result = await Promise.all(sources.map(downloadWithReport));
    if (signal?.aborted) {
      reportProgress({
        isCancelled: true,
      });
      throw new Error("aborted");
    }
    const pairs = await Promise.all(result.map(toPair));
    const data = Object.assign({}, ...pairs);
    const value = defer();
    const abort = fflate.zip(data, {
      level: 0,
    }, (error, array) => {
      if (error) {
        value.reject(error);
      } else {
        reportProgress({
          isComplete: true,
        });
        value.resolve(array);
      }
    });
    signal?.addEventListener("abort", abort, {
      once: true,
    });
    return value.promise;
  };
  return archiveWithReport(images);
};

const isNotAbort = (error) => !/aborted/i.test(`${error}`);
const logIfNotAborted = (error) => {
  if (isNotAbort(error)) {
    console.error(error);
  }
};
const makeDownloader = (images) => {
  let aborter = new AbortController();
  let rerender;
  let progress = {
    value: 0,
    text: "",
    error: false,
  };
  const startDownload = async (options) => {
    aborter = new AbortController();
    return download(images, {
      ...options,
      signal: aborter.signal,
    });
  };
  const downloadAndSave = async (options) => {
    const zip = await startDownload(options);
    if (zip) {
      await save(
        new Blob([
          zip,
        ]),
      );
    }
  };
  const reportProgress = (event) => {
    const { total, started, settled, rejected, isCancelled, isComplete } =
      event;
    const value = started / total * 0.1 + settled / total * 0.89;
    const text = `${(value * 100).toFixed(1)}%`;
    const error = !!rejected;
    if (isComplete || isCancelled) {
      progress = {
        value: 0,
        text: "",
        error: false,
      };
      rerender?.();
    } else if (text !== progress.text) {
      progress = {
        value,
        text,
        error,
      };
      rerender?.();
    }
  };
  const downloadWithProgress = async () => {
    try {
      await downloadAndSave({
        onProgress: reportProgress,
        onError: logIfNotAborted,
      });
    } catch (error) {
      if (isNotAbort(error)) {
        throw error;
      }
    }
  };
  // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Example
  const guard = (event) => {
    event.preventDefault();
    event.returnValue = "";
  };
  const useInstance = () => {
    const { error, text } = progress;
    rerender = useRerender();
    react$1.useEffect(() => {
      if (error || !text) {
        return;
      }
      window.addEventListener("beforeunload", guard);
      return () => window.removeEventListener("beforeunload", guard);
    }, [
      error || !text,
    ]);
  };
  return {
    get progress() {
      return progress;
    },
    download: startDownload,
    downloadAndSave,
    downloadWithProgress,
    cancelDownload: () => aborter.abort(),
    useInstance,
  };
};

const makePageController = ({ source, observer }) => {
  let imageLoad;
  let state;
  let setState;
  let key = "";
  let isReloaded = false;
  const load = async () => {
    const urls = [];
    key = `${Math.random()}`;
    for await (const url of imageSourceToIterable(source)) {
      urls.push(url);
      imageLoad = defer();
      setState?.({
        src: url,
        state: "loading",
      });
      const success = await imageLoad.promise;
      if (success) {
        setState?.({
          src: url,
          state: "complete",
        });
        return;
      }
      if (isReloaded) {
        isReloaded = false;
        return;
      }
    }
    setState?.({
      urls,
      state: "error",
    });
  };
  const useInstance = ({ ref }) => {
    [state, setState] = react$1.useState({
      src: "",
      state: "loading",
    });
    react$1.useEffect(() => {
      load();
    }, []);
    react$1.useEffect(() => {
      const target = ref?.current;
      if (target && observer) {
        observer.observe(target);
        return () => observer.unobserve(target);
      }
    }, [
      observer,
      ref.current,
    ]);
    return {
      key,
      ...state.src
        ? {
          src: state.src,
        }
        : {},
      onError: () => imageLoad.resolve(false),
      onLoad: () => imageLoad.resolve(true),
    };
  };
  return {
    get state() {
      return state;
    },
    reload: async () => {
      isReloaded = true;
      imageLoad.resolve(false);
      await load();
    },
    useInstance,
  };
};

const makeViewerController = ({ ref, navigator, rerender }) => {
  let options = {};
  let images = [];
  let status = "loading";
  let compactWidthIndex = 1;
  let downloader;
  let pages = [];
  const toggleFullscreen = () => {
    if (document.fullscreenElement) {
      document.exitFullscreen();
    } else {
      ref.current?.requestFullscreen();
    }
  };
  const loadImages = async (source) => {
    try {
      [images, downloader] = [
        [],
        undefined,
      ];
      if (!source) {
        status = "complete";
        return;
      }
      [status, pages] = [
        "loading",
        [],
      ];
      rerender();
      images = await source();
      if (!Array.isArray(images)) {
        throw new Error(`Invalid comic source type: ${typeof images}`);
      }
      status = "complete";
      downloader = makeDownloader(images);
      pages = images.map((x) =>
        makePageController({
          source: x,
          observer: navigator.observer,
        })
      );
    } catch (error) {
      status = "error";
      console.log(error);
      throw error;
    } finally {
      rerender();
    }
  };
  const reloadErrored = async () => {
    window.stop();
    for (const controller of pages) {
      if (controller.state.state !== "complete") {
        controller.reload();
      }
    }
  };
  return {
    get options() {
      return options;
    },
    get status() {
      return status;
    },
    get container() {
      return ref.current;
    },
    get compactWidthIndex() {
      return compactWidthIndex;
    },
    get downloader() {
      return downloader;
    },
    get download() {
      return downloader?.download ?? (() => Promise.resolve(new Uint8Array()));
    },
    get pages() {
      return pages;
    },
    set compactWidthIndex(value) {
      compactWidthIndex = value;
      rerender();
    },
    setOptions: async (value) => {
      const { source } = value;
      const isSourceChanged = source !== options.source;
      options = value;
      if (isSourceChanged) {
        await loadImages(source);
      }
    },
    goPrevious: navigator.goPrevious,
    goNext: navigator.goNext,
    toggleFullscreen,
    reloadErrored,
    unmount: () => reactDom.unmountComponentAtNode(ref.current),
  };
};
const useViewerController = ({ ref, scrollRef }) => {
  const rerender = useRerender();
  const navigator = usePageNavigator(scrollRef);
  const controller = react$1.useMemo(() =>
    makeViewerController({
      ref,
      navigator,
      rerender,
    }), [
    ref,
    navigator,
  ]);
  return controller;
};

const stretch = keyframes({
  "0%": {
    top: "8px",
    height: "64px",
  },
  "50%": {
    top: "24px",
    height: "32px",
  },
  "100%": {
    top: "24px",
    height: "32px",
  },
});
const SpinnerContainer = styled("div", {
  position: "absolute",
  left: "0",
  top: "0",
  right: "0",
  bottom: "0",
  margin: "auto",
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
  div: {
    display: "inline-block",
    width: "16px",
    margin: "0 4px",
    background: "#fff",
    animation: `${stretch} 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite`,
  },
  "div:nth-child(1)": {
    "animation-delay": "-0.24s",
  },
  "div:nth-child(2)": {
    "animation-delay": "-0.12s",
  },
  "div:nth-child(3)": {
    "animation-delay": "0",
  },
});
const Spinner = () =>
  /*#__PURE__*/ react$1.createElement(
    SpinnerContainer,
    null,
    /*#__PURE__*/ react$1.createElement("div", null),
    /*#__PURE__*/ react$1.createElement("div", null),
    /*#__PURE__*/ react$1.createElement("div", null),
  );
const Overlay = styled("div", {
  position: "relative",
  margin: "4px 0.5px",
  maxWidth: "100%",
  height: "100%",
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  "@media print": {
    margin: 0,
  },
  variants: {
    placeholder: {
      true: {
        width: "45%",
      },
    },
    fullWidth: {
      true: {
        width: "100%",
      },
    },
  },
});
const LinkColumn = styled("div", {
  display: "flex",
  flexFlow: "column nowrap",
  alignItems: "center",
  justifyContent: "center",
  cursor: "pointer",
  boxShadow: "1px 1px 3px",
  padding: "1rem 1.5rem",
  transition: "box-shadow 1s easeOutExpo",
  "&:hover": {
    boxShadow: "2px 2px 5px",
  },
  "&:active": {
    boxShadow: "0 0 2px",
  },
});
const Image1 = styled("img", {
  position: "relative",
  height: "100%",
  objectFit: "contain",
  maxWidth: "100%",
});

const Page = ({ fullWidth, controller, ...props }) => {
  const ref = react$1.useRef();
  const imageProps = controller.useInstance({
    ref,
  });
  const { state, src, urls } = controller.state;
  const reloadErrored = react$1.useCallback(async (event) => {
    event.stopPropagation();
    await controller.reload();
  }, []);
  return (/*#__PURE__*/ react$1.createElement(
    Overlay,
    {
      ref: ref,
      placeholder: state !== "complete",
      fullWidth: fullWidth,
    },
    state === "loading" && /*#__PURE__*/ react$1.createElement(Spinner, null),
    state === "error" && /*#__PURE__*/ react$1.createElement(
      LinkColumn,
      {
        onClick: reloadErrored,
      },
      /*#__PURE__*/ react$1.createElement(CircledX, null),
      /*#__PURE__*/ react$1.createElement("p", null, "이미지를 불러오지 못했습니다"),
      /*#__PURE__*/ react$1.createElement(
        "p",
        null,
        src ? src : urls?.join("\n"),
      ),
    ),
    /*#__PURE__*/ react$1.createElement(
      Image1,
      Object.assign({}, imageProps, props),
    ),
  ));
};

const maybeNotHotkey = (event) =>
  event.ctrlKey || event.altKey || isTyping(event);
const useDefault = ({ enable, controller }) => {
  const defaultKeyHandler = async (event) => {
    if (maybeNotHotkey(event)) {
      return;
    }
    switch (event.key) {
      case "j":
      case "ArrowDown":
        controller.goNext();
        break;
      case "k":
      case "ArrowUp":
        controller.goPrevious();
        break;
      case ";":
        await controller.downloader?.downloadWithProgress();
        break;
      case "/":
        controller.compactWidthIndex++;
        break;
      case "?":
        controller.compactWidthIndex--;
        break;
      case "'":
        controller.reloadErrored();
        break;
    }
  };
  const defaultGlobalKeyHandler = (event) => {
    if (maybeNotHotkey(event)) {
      return;
    }
    if (event.key === "i") {
      controller.toggleFullscreen();
    }
  };
  react$1.useEffect(() => {
    if (!controller || !enable) {
      return;
    }
    controller.container.addEventListener("keydown", defaultKeyHandler);
    window.addEventListener("keydown", defaultGlobalKeyHandler);
    return () => {
      controller.container.removeEventListener("keydown", defaultKeyHandler);
      window.removeEventListener("keydown", defaultGlobalKeyHandler);
    };
  }, [
    controller,
    enable,
  ]);
};

const Svg = styled("svg", {
  position: "absolute",
  bottom: "8px",
  left: "8px",
  cursor: "pointer",
  "&:hover": {
    filter: "hue-rotate(-145deg)",
  },
  variants: {
    error: {
      true: {
        filter: "hue-rotate(140deg)",
      },
    },
  },
});
const Circle = styled("circle", {
  transform: "rotate(-90deg)",
  transformOrigin: "50% 50%",
  stroke: "url(#aEObn)",
  fill: "#fff8",
});
const GradientDef = /*#__PURE__*/ react$1.createElement(
  "defs",
  null,
  /*#__PURE__*/ react$1.createElement(
    "linearGradient",
    {
      id: "aEObn",
      x1: "100%",
      y1: "0%",
      x2: "0%",
      y2: "100%",
    },
    /*#__PURE__*/ react$1.createElement("stop", {
      offset: "0%",
      style: {
        stopColor: "#53baff",
        stopOpacity: 1,
      },
    }),
    /*#__PURE__*/ react$1.createElement("stop", {
      offset: "100%",
      style: {
        stopColor: "#0067bb",
        stopOpacity: 1,
      },
    }),
  ),
);
const CenterText = styled("text", {
  dominantBaseline: "middle",
  textAnchor: "middle",
  fontSize: "30px",
  fontWeight: "bold",
  fill: "#004b9e",
});
const CircularProgress = (props) => {
  const { radius, strokeWidth, value, text, ...otherProps } = props;
  const circumference = 2 * Math.PI * radius;
  const strokeDashoffset = circumference - value * circumference;
  const center = radius + strokeWidth / 2;
  const side = center * 2;
  return (/*#__PURE__*/ react$1.createElement(
    Svg,
    Object.assign({
      height: side,
      width: side,
    }, otherProps),
    GradientDef,
    /*#__PURE__*/ react$1.createElement(
      Circle,
      Object.assign({}, {
        strokeWidth,
        strokeDasharray: `${circumference} ${circumference}`,
        strokeDashoffset,
        r: radius,
        cx: center,
        cy: center,
      }),
    ),
    /*#__PURE__*/ react$1.createElement(CenterText, {
      x: "50%",
      y: "50%",
    }, text || ""),
  ));
};

const DownloadIndicator = ({ downloader }) => {
  const { value, text, error } = downloader.progress ?? {};
  downloader.useInstance();
  return (/*#__PURE__*/ react$1.createElement(
    react$1.Fragment,
    null,
    text
      ? /*#__PURE__*/ react$1.createElement(CircularProgress, {
        radius: 50,
        strokeWidth: 10,
        value: value ?? 0,
        text: text,
        error: error,
        onClick: downloader.cancelDownload,
      })
      : /*#__PURE__*/ react$1.createElement(DownloadIcon, {
        onClick: downloader.downloadWithProgress,
      }),
  ));
};

const Viewer_ = (props, refHandle) => {
  const { useDefault: enableDefault, options: viewerOptions, ...otherProps } =
    props;
  const ref = react$1.useRef();
  const scrollRef = react$1.useRef();
  const fullscreenElement = useFullscreenElement();
  const controller = useViewerController({
    ref,
    scrollRef,
  });
  const {
    options,
    pages,
    status,
    downloader,
    toggleFullscreen,
    compactWidthIndex,
  } = controller;
  const navigate = react$1.useCallback((event) => {
    const height = ref.current?.clientHeight;
    if (!height || event.button !== 0) {
      return;
    }
    event.preventDefault();
    const isTop = event.clientY < height / 2;
    if (isTop) {
      controller.goPrevious();
    } else {
      controller.goNext();
    }
  }, [
    controller,
  ]);
  const blockSelection = react$1.useCallback((event) => {
    if (event.detail >= 2) {
      event.preventDefault();
    }
    if (event.buttons === 3) {
      controller.toggleFullscreen();
      event.preventDefault();
    }
  }, [
    controller,
  ]);
  useDefault({
    enable: props.useDefault,
    controller,
  });
  react$1.useImperativeHandle(refHandle, () => controller, [
    controller,
  ]);
  react$1.useEffect(() => {
    controller.setOptions(viewerOptions);
  }, [
    controller,
    viewerOptions,
  ]);
  react$1.useEffect(() => {
    if (ref.current && fullscreenElement === ref.current) {
      ref.current?.focus?.();
    }
  }, [
    ref.current,
    fullscreenElement,
  ]);
  return (/*#__PURE__*/ react$1.createElement(
    Container,
    {
      ref: ref,
      tabIndex: -1,
      className: "vim_comic_viewer",
    },
    /*#__PURE__*/ react$1.createElement(
      ScrollableLayout,
      Object.assign({
        ref: scrollRef,
        fullscreen: fullscreenElement === ref.current,
        onClick: navigate,
        onMouseDown: blockSelection,
      }, otherProps),
      status === "complete"
        ? pages?.map?.((controller, index) =>
          /*#__PURE__*/ react$1.createElement(
            Page,
            Object.assign({
              key: index,
              controller: controller,
              fullWidth: index < compactWidthIndex,
            }, options?.imageProps),
          )
        ) || false
        : /*#__PURE__*/ react$1.createElement(
          "p",
          null,
          status === "error" ? "에러가 발생했습니다" : "로딩 중...",
        ),
    ),
    /*#__PURE__*/ react$1.createElement(FullscreenIcon, {
      onClick: toggleFullscreen,
    }),
    downloader
      ? /*#__PURE__*/ react$1.createElement(DownloadIndicator, {
        downloader: downloader,
      })
      : false,
  ));
};
const Viewer = /*#__PURE__*/ react$1.forwardRef(Viewer_);

var types = /*#__PURE__*/ Object.freeze({
  __proto__: null,
});

/** @jsx createElement */
/// <reference lib="dom" />
const getDefaultRoot = () => {
  const div = document.createElement("div");
  div.setAttribute("style", "width: 0; height: 0; position: fixed;");
  document.body.append(div);
  return div;
};
const initialize = async (options) => {
  const ref = /*#__PURE__*/ react$1.createRef();
  reactDom.render(
    /*#__PURE__*/ react$1.createElement(Viewer, {
      ref: ref,
      options: options,
      useDefault: true,
    }),
    getDefaultRoot(),
  );
  return ref.current;
};

exports.Viewer = Viewer;
exports.download = download;
exports.initialize = initialize;
exports.transformToBlobUrl = transformToBlobUrl;
exports.types = types;
exports.utils = utils;