Greasy Fork

vim comic viewer

Universal comic reader

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

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

// ==UserScript==
// @name         vim comic viewer
// @description  Universal comic reader
// @version      1.0.1
// @namespace    https://openuserjs.org/users/keut
// @exclude      *
// @match        http://unused-field.space/
// @author       keut
// @license      MIT
// ==/UserScript==

'use strict';

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

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

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,
});

const waitFullscreenChange = () =>
  new Promise((resolve) => {
    document.addEventListener(
      'fullscreenchange',
      () => resolve(document.fullscreenElement),
      {
        once: true,
      },
    );
  });
const useFullscreen = (ref, { onEnter, onExit }) => {
  const toggle = react.useCallback(async () => {
    if (document.fullscreenElement) {
      return document.exitFullscreen();
    } else {
      await ref.current.requestFullscreen();
      await onEnter?.();
      while (await waitFullscreenChange());
      await onExit?.();
    }
  }, [onEnter, onExit, ref.current]);
  return toggle;
};

const usePageNavigator = () => {
  const [observer, setObserver] = react.useState();
  const [captures] = react.useState([
    {
      entries: [],
      isSorted: true,
    },
  ]);
  const sortAndGetAnchor = react.useCallback(
    (pages) => {
      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;
    },
    [captures],
  );
  const goNext = react.useCallback(() => {
    const anchor = sortAndGetAnchor(captures[0]);
    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(captures[0]);
    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(captures[1]);
    if (!anchor) {
      return;
    }
    anchor.scrollIntoView({
      block: 'center',
    });
  }, [captures, sortAndGetAnchor]);
  react.useEffect(() => {
    const newObserver = new IntersectionObserver(
      (entries) => {
        let newIntersections = captures[0].entries;
        for (const entry of entries) {
          newIntersections = newIntersections.filter(
            (item) => item.target !== entry.target,
          );
          if (entry.isIntersecting) {
            newIntersections.push(entry);
          }
        }
        captures.unshift({
          entries: newIntersections,
          isSorted: false,
        });
        captures.splice(2);
        console.log(captures[0].entries);
      },
      {
        threshold: [0.01, 0.5],
      },
    );
    setObserver(newObserver);
    return () => newObserver.disconnect();
  }, [captures]);
  return react.useMemo(
    () => ({
      goNext,
      goPrevious,
      restore,
      observer,
    }),
    [goNext, goPrevious, restore, observer],
  );
};

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 { styled, css } = react$1.createStyled({});

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 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]);
  const onEnterFullscreen = react.useCallback(async () => {
    ref.current?.focus?.();
    await timeout(102);
    navigator.restore();
  }, [ref.current]);
  const onExitFullscreen = react.useCallback(async () => {
    await timeout(102);
    navigator.restore();
  }, [navigator]);
  const toggleFullscreen = useFullscreen(ref, {
    onEnter: onEnterFullscreen,
    onExit: onExitFullscreen,
  });
  react.useEffect(() => {
    const globalKeyHandler = async (event) => {
      if (event.key === 'i') {
        toggleFullscreen();
      }
    };
    window.addEventListener('keydown', globalKeyHandler);
    return () => {
      window.removeEventListener('keydown', globalKeyHandler);
    };
  }, [navigator]);
  react.useEffect(() => {
    ref.current?.focus?.();
  }, [ref.current]);
  react.useEffect(() => {
    fetchSource();
  }, [fetchSource]);
  return react.createElement(
    ImageContainer,
    Object.assign(
      {
        ref: ref,
        className: 'vim_comic_viewer',
        css: {
          '&&:fullscreen': {
            display: 'flex',
          },
        },
        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 initializeViewer = (root, source) => {
  reactDom.render(
    react.createElement(Viewer, {
      source: source,
    }),
    root,
  );
};

const initializeWithSource = async (source) => {
  const root = document.createElement('div');
  while (true) {
    if (document.body) {
      document.body.append(root);
      initializeViewer(root, source);
      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.utils = utils;