Greasy Fork

vim comic viewer

Universal comic reader

目前为 2020-12-05 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.icu/scripts/417893/877253/vim%20comic%20viewer.js

// ==UserScript==
// @name         vim comic viewer
// @description  Universal comic reader
// @version      1.2.0
// @namespace    https://greasyfork.org/en/users/713014-nanikit
// @exclude      *
// @match        http://unused-field.space/
// @author       nanikit
// @license      MIT
// ==/UserScript==

"use strict";

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

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

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

const usePageNavigator = () => {
  const [observer, setObserver] = react.useState();
  const [pages] = react.useState({
    entries: [],
    isSorted: true,
  });
  const sortAndGetAnchor = react.useCallback(() => {
    const first = pages.entries?.[0]?.target;
    if (!pages.isSorted && !!first) {
      const children = [
        ...first.parentElement.children,
      ];
      pages.entries.sort((a, b) => {
        const aRatio = Math.round(a.intersectionRatio * 10);
        const bRatio = Math.round(b.intersectionRatio * 10);
        const aIndex = children.indexOf(a.target);
        const bIndex = children.indexOf(b.target);
        return (bRatio - aRatio) * 10 + (bIndex - aIndex);
      });
      pages.isSorted = true;
    }
    return pages.entries?.[0]?.target;
  }, [
    pages,
  ]);
  const goNext = react.useCallback(() => {
    const anchor = sortAndGetAnchor();
    if (!anchor) {
      return;
    }
    let cursor = anchor;
    const originBound = cursor.getBoundingClientRect();
    while (cursor.nextElementSibling) {
      const next = cursor.nextElementSibling;
      const nextBound = next.getBoundingClientRect();
      if (originBound.bottom < nextBound.top) {
        next.scrollIntoView({
          block: "center",
        });
        break;
      }
      cursor = next;
    }
  }, [
    sortAndGetAnchor,
  ]);
  const goPrevious = react.useCallback(() => {
    const anchor = sortAndGetAnchor();
    if (!anchor) {
      return;
    }
    let cursor = anchor;
    const originBound = cursor.getBoundingClientRect();
    while (cursor.previousElementSibling) {
      const previous = cursor.previousElementSibling;
      const previousBound = previous.getBoundingClientRect();
      if (previousBound.bottom < originBound.top) {
        previous.scrollIntoView({
          block: "center",
        });
        break;
      }
      cursor = previous;
    }
  }, [
    sortAndGetAnchor,
  ]);
  const restore = react.useCallback(() => {
    const anchor = sortAndGetAnchor();
    if (!anchor) {
      return;
    }
    anchor.scrollIntoView({
      block: "center",
    });
  }, [
    pages,
    sortAndGetAnchor,
  ]);
  react.useEffect(() => {
    const newObserver = new IntersectionObserver((entries) => {
      let newIntersections = pages.entries;
      for (const entry of entries) {
        newIntersections = newIntersections.filter((item) =>
          item.target !== entry.target
        );
        if (entry.isIntersecting) {
          newIntersections.push(entry);
        }
      }
      pages.entries = newIntersections;
    }, {
      threshold: [
        0.01,
        0.5,
      ],
    });
    setObserver(newObserver);
    window.addEventListener("resize", restore);
    return () => {
      newObserver.disconnect();
      window.removeEventListener("resize", restore);
    };
  }, [
    pages,
  ]);
  return react.useMemo(() => ({
    goNext,
    goPrevious,
    restore,
    observer,
  }), [
    goNext,
    goPrevious,
    restore,
    observer,
  ]);
};

const { styled, css } = react$1.createStyled({});

const init = (source) => {
  if (typeof source === "string") {
    return {
      src: source,
      iterator: (function* () {
      })(),
    };
  }
  if (Array.isArray(source)) {
    return {
      src: source[0],
      iterator: (function* () {
        for (const url of source.slice(1)) {
          yield url;
        }
      })(),
    };
  }
  throw new Error("unknown image source");
};
const reducer = (state, action) => {
  if (action !== "next") {
    return init(action);
  }
  if (state.iterator == null) {
    return state;
  }
  const result = state.iterator.next();
  if (result.done === true) {
    return {};
  }
  return {
    ...state,
    src: result.value,
  };
};
const usePageReducer = (source) => {
  const [state, dispatch] = react.useReducer(reducer, source, init);
  const onError = react.useCallback(() => {
    dispatch("next");
  }, []);
  return {
    src: state.src,
    onError,
  };
};

const Image = styled("img", {
  height: "100vh",
  maxWidth: "100vw",
  objectFit: "contain",
  margin: "4px 1px",
});
const Page = ({ source, observer, ...props }) => {
  const { src, onError } = usePageReducer(source);
  const ref = react.useRef();
  react.useEffect(() => {
    const target = ref.current;
    if (target && observer) {
      observer.observe(target);
      return () => observer.unobserve(target);
    }
  }, [
    observer,
    ref.current,
  ]);
  return react.createElement(
    Image,
    Object.assign({
      ref: ref,
      src: src,
      onError: onError,
      loading: "lazy",
    }, props),
  );
};

const ImageContainer = styled("div", {
  backgroundColor: "#eee",
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
  flexFlow: "row-reverse wrap",
  overflowY: "auto",
});
const Viewer = ({ source, ...props }) => {
  const [images, setImages] = react.useState();
  const [status, setStatus] = react.useState("loading");
  const navigator = usePageNavigator();
  const ref = react.useRef();
  const fullscreenElement = useFullscreenElement();
  const handleNavigation = react.useCallback((event) => {
    switch (event.key) {
      case "j":
        navigator.goNext();
        break;
      case "k":
        navigator.goPrevious();
        break;
      case "o":
        window.close();
        break;
    }
  }, [
    navigator,
  ]);
  const fetchSource = react.useCallback(async () => {
    try {
      setImages(await source());
      setStatus("complete");
    } catch (error) {
      setStatus("error");
      console.log(error);
      throw error;
    }
  }, [
    source,
  ]);
  react.useEffect(() => {
    const globalKeyHandler = async (event) => {
      if (event.key === "i") {
        if (document.fullscreenElement) {
          await document.exitFullscreen();
        } else {
          await ref?.current?.requestFullscreen?.();
        }
      }
    };
    window.addEventListener("keydown", globalKeyHandler);
    return () => window.removeEventListener("keydown", globalKeyHandler);
  }, [
    navigator,
    ref.current,
  ]);
  react.useEffect(() => {
    ref.current?.focus?.();
  }, [
    ref.current,
  ]);
  react.useEffect(() => {
    if (!ref.current) {
      return;
    }
    const style = ref.current.style;
    const fullscreenStyle = {
      display: "flex",
      position: "fixed",
      top: 0,
      bottom: 0,
      overflow: "auto",
    };
    if (fullscreenElement && style.position !== "fixed") {
      Object.assign(style, fullscreenStyle);
      navigator.restore();
      ref.current.focus();
    } else if (!fullscreenElement && style.position === "fixed") {
      for (const property of Object.keys(fullscreenStyle)) {
        style.removeProperty(property);
      }
      navigator.restore();
    }
  }, [
    ref.current,
    fullscreenElement,
    navigator,
  ]);
  react.useEffect(() => {
    fetchSource();
  }, [
    fetchSource,
  ]);
  return react.createElement(
    ImageContainer,
    Object.assign({
      ref: ref,
      className: "vim_comic_viewer",
      tabIndex: -1,
      onKeyDown: handleNavigation,
    }, props),
    status === "complete"
      ? images?.map?.((image, index) =>
        react.createElement(Page, {
          key: index,
          source: image,
          observer: navigator.observer,
        })
      ) || false
      : react.createElement(
        "p",
        null,
        status === "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);
};

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

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

/** @jsx createElement */
/// <reference lib="dom" />
const initializeWithSource = async (source) => {
  const root = document.createElement("div");
  while (true) {
    if (document.body) {
      document.body.append(root);
      reactDom.render(
        react.createElement(Viewer, {
          source: source,
        }),
        root,
      );
      break;
    }
    await timeout(1);
  }
};
const initialize = async (sourceOrSources) => {
  if (Array.isArray(sourceOrSources)) {
    const source = sourceOrSources.find((x) => x.isApplicable());
    if (source) {
      await initializeWithSource(source.comicSource);
    }
  } else {
    await initializeWithSource(sourceOrSources.comicSource);
  }
};

exports.initialize = initialize;
exports.types = types;
exports.utils = utils;