您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Universal comic reader
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/417893/949417/vim%20comic%20viewer.js
- // ==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/fflate@0.7.1/lib/browser.cjs
- // @resource react https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js
- // @resource react-dom https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js
- // @resource @stitches/core https://cdn.jsdelivr.net/npm/@stitches/core@0.2.0/dist/index.cjs
- // @resource @stitches/react https://cdn.jsdelivr.net/npm/@stitches/react@0.2.0/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;