vim comic viewer

Universal comic reader

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greatest.deepsurf.us/scripts/417893/1566357/vim%20comic%20viewer.js

// ==UserScript==
// @name           vim comic viewer
// @name:ko        vim comic viewer
// @description    Universal comic reader
// @description:ko 만화 뷰어 라이브러리
// @version        19.2.0
// @namespace      https://greatest.deepsurf.us/en/users/713014-nanikit
// @exclude        *
// @match          http://unused-field.space/
// @author         nanikit
// @license        MIT
// @grant          GM.addValueChangeListener
// @grant          GM.getValue
// @grant          GM.removeValueChangeListener
// @grant          GM.setValue
// @grant          GM.xmlHttpRequest
// @grant          unsafeWindow
// @resource       link:@headlessui/react       https://cdn.jsdelivr.net/npm/@headlessui/[email protected]/dist/headlessui.prod.cjs
// @resource       link:@stitches/react         https://cdn.jsdelivr.net/npm/@stitches/[email protected]/dist/index.cjs
// @resource       link:clsx                    https://cdn.jsdelivr.net/npm/[email protected]/dist/clsx.js
// @resource       link:fflate                  https://cdn.jsdelivr.net/npm/[email protected]/lib/browser.cjs
// @resource       link:jotai                   https://cdn.jsdelivr.net/npm/[email protected]/index.js
// @resource       link:jotai/react             https://cdn.jsdelivr.net/npm/[email protected]/react.js
// @resource       link:jotai/react/utils       https://cdn.jsdelivr.net/npm/[email protected]/react/utils.js
// @resource       link:jotai/utils             https://cdn.jsdelivr.net/npm/[email protected]/utils.js
// @resource       link:jotai/vanilla           https://cdn.jsdelivr.net/npm/[email protected]/vanilla.js
// @resource       link:jotai/vanilla/utils     https://cdn.jsdelivr.net/npm/[email protected]/vanilla/utils.js
// @resource       link:jotai-cache             https://cdn.jsdelivr.net/npm/[email protected]/dist/cjs/atomWithCache.js
// @resource       link:overlayscrollbars       https://cdn.jsdelivr.net/npm/[email protected]/overlayscrollbars.cjs
// @resource       link:overlayscrollbars-react https://cdn.jsdelivr.net/npm/[email protected]/overlayscrollbars-react.cjs.js
// @resource       link:react                   https://cdn.jsdelivr.net/npm/[email protected]/cjs/react.production.js
// @resource       link:react/jsx-runtime       https://cdn.jsdelivr.net/npm/[email protected]/cjs/react-jsx-runtime.production.js
// @resource       link:react-dom               https://cdn.jsdelivr.net/npm/[email protected]/cjs/react-dom.production.js
// @resource       link:react-dom/client        https://cdn.jsdelivr.net/npm/[email protected]/cjs/react-dom-client.production.js
// @resource       link:react-toastify          https://cdn.jsdelivr.net/npm/[email protected]/dist/react-toastify.js
// @resource       link:scheduler               https://cdn.jsdelivr.net/npm/[email protected]/cjs/scheduler.production.min.js
// @resource       link:vcv-inject-node-env     data:,unsafeWindow.process=%7Benv:%7BNODE_ENV:%22production%22%7D%7D
// @resource       overlayscrollbars-css        https://cdn.jsdelivr.net/npm/[email protected]/styles/overlayscrollbars.min.css
// @resource       react-toastify-css           https://cdn.jsdelivr.net/npm/[email protected]/dist/ReactToastify.css
// ==/UserScript==
"use strict";

var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
	for (var name in all) __defProp(target, name, {
		get: all[name],
		enumerable: true
	});
};
var __copyProps = (to, from, except, desc) => {
	if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
		key = keys[i];
		if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
			get: ((k) => from[k]).bind(null, key),
			enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
		});
	}
	return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
	value: mod,
	enumerable: true
}) : target, mod));
require("vcv-inject-node-env");
const __stitches_react = __toESM(require("@stitches/react"));
const jotai = __toESM(require("jotai"));
const jotai_cache = __toESM(require("jotai-cache"));
const jotai_utils = __toESM(require("jotai/utils"));
const __headlessui_react = __toESM(require("@headlessui/react"));
const react = __toESM(require("react"));
const react_dom_client = __toESM(require("react-dom/client"));
const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
const react_toastify = __toESM(require("react-toastify"));
const overlayscrollbars_react = __toESM(require("overlayscrollbars-react"));
function deferred() {
	let methods;
	let state = "pending";
	const promise = new Promise((resolve, reject) => {
		methods = {
			async resolve(value) {
				await value;
				state = "fulfilled";
				resolve(value);
			},
			reject(reason) {
				state = "rejected";
				reject(reason);
			}
		};
	});
	Object.defineProperty(promise, "state", { get: () => state });
	return Object.assign(promise, methods);
}
function throttle(fn, timeframe) {
	let lastExecution = NaN;
	let flush = null;
	const throttled = (...args) => {
		flush = () => {
			try {
				fn.call(throttled, ...args);
			} finally {
				lastExecution = Date.now();
				flush = null;
			}
		};
		if (throttled.throttling) {
			return;
		}
		flush?.();
	};
	throttled.clear = () => {
		lastExecution = NaN;
	};
	throttled.flush = () => {
		lastExecution = NaN;
		flush?.();
		throttled.clear();
	};
	Object.defineProperties(throttled, {
		throttling: { get: () => Date.now() - lastExecution <= timeframe },
		lastExecution: { get: () => lastExecution }
	});
	return throttled;
}
var deps_exports = {};
__export(deps_exports, {
	Dialog: () => __headlessui_react.Dialog,
	Fragment: () => react.Fragment,
	Provider: () => jotai.Provider,
	RESET: () => jotai_utils.RESET,
	Tab: () => __headlessui_react.Tab,
	atom: () => jotai.atom,
	atomWithCache: () => jotai_cache.atomWithCache,
	atomWithStorage: () => jotai_utils.atomWithStorage,
	createContext: () => react.createContext,
	createJSONStorage: () => jotai_utils.createJSONStorage,
	createRef: () => react.createRef,
	createRoot: () => react_dom_client.createRoot,
	createStitches: () => __stitches_react.createStitches,
	createStore: () => jotai.createStore,
	deferred: () => deferred,
	forwardRef: () => react.forwardRef,
	loadable: () => jotai_utils.loadable,
	selectAtom: () => jotai_utils.selectAtom,
	splitAtom: () => jotai_utils.splitAtom,
	throttle: () => throttle,
	useAtom: () => jotai.useAtom,
	useAtomValue: () => jotai.useAtomValue,
	useCallback: () => react.useCallback,
	useEffect: () => react.useEffect,
	useId: () => react.useId,
	useImperativeHandle: () => react.useImperativeHandle,
	useLayoutEffect: () => react.useLayoutEffect,
	useMemo: () => react.useMemo,
	useReducer: () => react.useReducer,
	useRef: () => react.useRef,
	useSetAtom: () => jotai.useSetAtom,
	useState: () => react.useState,
	useStore: () => jotai.useStore
});
__reExport(deps_exports, require("fflate"));
const rootAtom = (0, jotai.atom)(null);
const viewerOptionsAtom = (0, jotai.atom)({});
const viewerStatusAtom = (0, jotai.atom)("idle");
var ___locale$1 = "en";
var settings$1 = "Settings";
var help$1 = "Help";
var maxZoomOut$1 = "Maximum zoom out";
var maxZoomIn$1 = "Maximum zoom in";
var singlePageCount$1 = "single page count";
var backgroundColor$1 = "Background color";
var leftToRight$1 = "Left to right";
var reset$1 = "Reset";
var doYouReallyWantToReset$1 = "Do you really want to reset?";
var errorIsOccurred$1 = "Error is occurred.";
var failedToLoadImage$1 = "Failed to load image.";
var loading$1 = "Loading...";
var fullScreenRestorationGuide$1 = "Enter full screen yourself if you want to keep the viewer open in full screen.";
var useFullScreen$1 = "Use full screen";
var downloading$1 = "Downloading...";
var cancel$1 = "CANCEL";
var downloadComplete$1 = "Download complete.";
var errorOccurredWhileDownloading$1 = "Error occurred while downloading.";
var keyBindings$1 = "Key bindings";
var toggleViewer$1 = "Toggle viewer";
var toggleFullscreenSetting$1 = "Toggle fullscreen setting";
var nextPage$1 = "Next page";
var previousPage$1 = "Previous page";
var download$2 = "Download";
var refresh$1 = "Refresh";
var increaseSinglePageCount$1 = "Increase single page count";
var decreaseSinglePageCount$1 = "Decrease single page count";
var anchorSinglePageCount$1 = "Set single page view until before current page";
var en_default = {
	"@@locale": ___locale$1,
	settings: settings$1,
	help: help$1,
	maxZoomOut: maxZoomOut$1,
	maxZoomIn: maxZoomIn$1,
	singlePageCount: singlePageCount$1,
	backgroundColor: backgroundColor$1,
	leftToRight: leftToRight$1,
	reset: reset$1,
	doYouReallyWantToReset: doYouReallyWantToReset$1,
	errorIsOccurred: errorIsOccurred$1,
	failedToLoadImage: failedToLoadImage$1,
	loading: loading$1,
	fullScreenRestorationGuide: fullScreenRestorationGuide$1,
	useFullScreen: useFullScreen$1,
	downloading: downloading$1,
	cancel: cancel$1,
	downloadComplete: downloadComplete$1,
	errorOccurredWhileDownloading: errorOccurredWhileDownloading$1,
	keyBindings: keyBindings$1,
	toggleViewer: toggleViewer$1,
	toggleFullscreenSetting: toggleFullscreenSetting$1,
	nextPage: nextPage$1,
	previousPage: previousPage$1,
	download: download$2,
	refresh: refresh$1,
	increaseSinglePageCount: increaseSinglePageCount$1,
	decreaseSinglePageCount: decreaseSinglePageCount$1,
	anchorSinglePageCount: anchorSinglePageCount$1
};
var ___locale = "ko";
var settings = "설정";
var help = "도움말";
var maxZoomOut = "최대 축소";
var maxZoomIn = "최대 확대";
var singlePageCount = "한쪽 페이지 수";
var backgroundColor = "배경색";
var leftToRight = "왼쪽부터 보기";
var reset = "초기화";
var doYouReallyWantToReset = "정말 초기화하시겠어요?";
var errorIsOccurred = "에러가 발생했습니다.";
var failedToLoadImage = "이미지를 불러오지 못했습니다.";
var loading = "로딩 중...";
var fullScreenRestorationGuide = "뷰어 전체 화면을 유지하려면 직접 전체 화면을 켜 주세요 (F11).";
var useFullScreen = "전체 화면";
var downloading = "다운로드 중...";
var cancel = "취소";
var downloadComplete = "다운로드 완료";
var errorOccurredWhileDownloading = "다운로드 도중 오류가 발생했습니다";
var keyBindings = "단축키";
var toggleViewer = "뷰어 전환";
var toggleFullscreenSetting = "전체화면 설정 전환";
var nextPage = "다음 페이지";
var previousPage = "이전 페이지";
var download$1 = "다운로드";
var refresh = "새로고침";
var increaseSinglePageCount = "한쪽 페이지 수 늘리기";
var decreaseSinglePageCount = "한쪽 페이지 수 줄이기";
var anchorSinglePageCount = "현재 페이지 전까지 한쪽 페이지로 설정";
var ko_default = {
	"@@locale": ___locale,
	settings,
	help,
	maxZoomOut,
	maxZoomIn,
	singlePageCount,
	backgroundColor,
	leftToRight,
	reset,
	doYouReallyWantToReset,
	errorIsOccurred,
	failedToLoadImage,
	loading,
	fullScreenRestorationGuide,
	useFullScreen,
	downloading,
	cancel,
	downloadComplete,
	errorOccurredWhileDownloading,
	keyBindings,
	toggleViewer,
	toggleFullscreenSetting,
	nextPage,
	previousPage,
	download: download$1,
	refresh,
	increaseSinglePageCount,
	decreaseSinglePageCount,
	anchorSinglePageCount
};
const translations = {
	en: en_default,
	ko: ko_default
};
const i18nStringsAtom = (0, jotai.atom)(getLanguage());
const i18nAtom = (0, jotai.atom)((get) => get(i18nStringsAtom), (_get, set) => {
	set(i18nStringsAtom, getLanguage());
});
i18nAtom.onMount = (set) => {
	addEventListener("languagechange", set);
	return () => {
		removeEventListener("languagechange", set);
	};
};
function getLanguage() {
	for (const language of navigator.languages) {
		const locale = language.split("-")[0];
		if (!locale) {
			continue;
		}
		const translation = translations[locale];
		if (translation) {
			return translation;
		}
	}
	return en_default;
}
const { styled, css, keyframes } = (0, __stitches_react.createStitches)({});
function DownloadCancel({ onClick }) {
	const strings = (0, jotai.useAtomValue)(i18nAtom);
	return  (0, react_jsx_runtime.jsxs)(SpaceBetween, { children: [ (0, react_jsx_runtime.jsx)("p", { children: strings.downloading }),  (0, react_jsx_runtime.jsx)("button", {
		type: "button",
		onClick,
		children: strings.cancel
	})] });
}
const SpaceBetween = styled("div", {
	display: "flex",
	flexFlow: "row nowrap",
	justifyContent: "space-between"
});
const MAX_RETRY_COUNT = 6;
const MAX_SAME_URL_RETRY_COUNT = 2;
function isDelay(sourceOrDelay) {
	return sourceOrDelay === undefined || typeof sourceOrDelay !== "string" && !sourceOrDelay.src;
}
function toAdvancedObject(sourceOrDelay) {
	return isDelay(sourceOrDelay) ? { src: undefined } : toAdvancedSource(sourceOrDelay);
}
function toAdvancedSource(source) {
	return typeof source === "string" ? {
		type: "image",
		src: source
	} : source;
}
async function* getMediaIterable({ media, index, comic, maxSize }) {
	if (!isDelay(media)) {
		yield getUrl(media);
	}
	if (!comic) {
		return;
	}
	let previous;
	let retryCount = 0;
	let sameUrlRetryCount = 0;
	while (sameUrlRetryCount <= MAX_SAME_URL_RETRY_COUNT && retryCount <= MAX_RETRY_COUNT) {
		const hadError = media !== undefined || retryCount > 0;
		const medias = await comic({
			cause: hadError ? "error" : "load",
			page: index,
			maxSize
		});
		const next = medias[index];
		if (isDelay(next)) {
			continue;
		}
		const url = getUrl(next);
		yield url;
		retryCount++;
		if (previous === url) {
			sameUrlRetryCount++;
			continue;
		}
		previous = url;
	}
}
function getUrl(source) {
	return typeof source === "string" ? source : source.src;
}
const isGmFetchAvailable = typeof GM.xmlHttpRequest === "function";
async function gmFetch(url, init) {
	const method = init?.body ? "POST" : "GET";
	const response = await GM.xmlHttpRequest({
		method,
		url,
		headers: {
			referer: `${location.origin}/`,
			...init?.headers
		},
		responseType: init?.type === "text" ? undefined : init?.type,
		data: init?.body
	});
	return response;
}
async function download(comic, options) {
	const { onError, onProgress, signal } = options || {};
	let startedCount = 0;
	let resolvedCount = 0;
	let rejectedCount = 0;
	let status = "ongoing";
	const pages = await comic({
		cause: "download",
		maxSize: {
			width: Infinity,
			height: Infinity
		}
	});
	const digit = Math.floor(Math.log10(pages.length)) + 1;
	return archiveWithReport();
	async function archiveWithReport() {
		const result = await Promise.all(pages.map(downloadWithReport));
		if (signal?.aborted) {
			reportProgress({ transition: "cancelled" });
			signal.throwIfAborted();
		}
		const pairs = await Promise.all(result.map(toPair));
		const data = Object.assign({}, ...pairs);
		const value = deferred();
		const abort = (0, deps_exports.zip)(data, { level: 0 }, (error, array) => {
			if (error) {
				reportProgress({ transition: "error" });
				value.reject(error);
			} else {
				reportProgress({ transition: "complete" });
				value.resolve(array);
			}
		});
		signal?.addEventListener("abort", abort, { once: true });
		return value;
	}
	async function downloadWithReport(source, pageIndex) {
		const errors = [];
		startedCount++;
		reportProgress();
		for await (const event of downloadImage({
			media: source,
			pageIndex
		})) {
			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")])
		};
	}
	async function* downloadImage({ media, pageIndex }) {
		const maxSize = {
			width: Infinity,
			height: Infinity
		};
		const mediaParams = {
			media,
			index: pageIndex,
			comic,
			maxSize
		};
		for await (const url of getMediaIterable(mediaParams)) {
			if (signal?.aborted) {
				break;
			}
			try {
				const blob = await fetchBlobWithCacheIfPossible(url, signal);
				yield {
					url,
					blob
				};
			} catch (error) {
				yield await fetchBlobIgnoringCors(url, {
					signal,
					fetchError: error
				});
			}
		}
	}
	async function toPair({ url, blob }, index) {
		const array = new Uint8Array(await blob.arrayBuffer());
		const pad = `${index}`.padStart(digit, "0");
		const name = `${pad}${guessExtension(array) ?? getExtension(url)}`;
		return { [name]: array };
	}
	function reportProgress({ transition } = {}) {
		if (status !== "ongoing") {
			return;
		}
		if (transition) {
			status = transition;
		}
		onProgress?.({
			total: pages.length,
			started: startedCount,
			settled: resolvedCount + rejectedCount,
			rejected: rejectedCount,
			status
		});
	}
}
function getExtension(url) {
	if (!url) {
		return ".txt";
	}
	const extension = url.match(/\.[^/?#]{3,4}?(?=[?#]|$)/);
	return extension?.[0] || ".jpg";
}
function 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";
	}
}
async function fetchBlobWithCacheIfPossible(url, signal) {
	const response = await fetch(url, { signal });
	return await response.blob();
}
async function fetchBlobIgnoringCors(url, { signal, fetchError }) {
	if (isCrossOrigin(url) && !isGmFetchAvailable) {
		return { error: new Error("It could be a CORS issue but cannot use GM.xmlhttpRequest", { cause: fetchError }) };
	}
	try {
		const response = await gmFetch(url, {
			signal,
			type: "blob"
		});
		if (response.status >= 400) {
			const body = await response.response.text();
			const message = `failed to load ${url} with HTTP ${response.status} ${response.statusText}\n${body}`;
			return { error: new Error(message) };
		}
		return {
			url,
			blob: response.response
		};
	} catch (error) {
		if (isGmCancelled(error)) {
			return { error: new Error("download aborted") };
		} else {
			return { error: fetchError };
		}
	}
}
function isCrossOrigin(url) {
	return new URL(url).origin !== location.origin;
}
function isGmCancelled(error) {
	return error instanceof Function;
}
var utils_exports = {};
__export(utils_exports, {
	getSafeFileName: () => getSafeFileName,
	insertCss: () => insertCss,
	isTyping: () => isTyping,
	save: () => save,
	saveAs: () => saveAs,
	timeout: () => timeout,
	waitDomContent: () => waitDomContent
});
const timeout = (millisecond) => new Promise((resolve) => setTimeout(resolve, millisecond));
const waitDomContent = (document$1) => document$1.readyState === "loading" ? new Promise((r) => document$1.addEventListener("readystatechange", r, { once: true })) : true;
const insertCss = (css$1) => {
	const style = document.createElement("style");
	style.innerHTML = css$1;
	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(4e4);
	URL.revokeObjectURL(a.href);
};
const getSafeFileName = (str) => {
	return str.replace(/[<>:"/\\|?*\x00-\x1f]+/gi, "").trim() || "download";
};
const save = (blob) => {
	return saveAs(blob, `${getSafeFileName(document.title)}.zip`);
};
GM.getResourceText("react-toastify-css").then(insertCss);
const aborterAtom = (0, jotai.atom)(null);
const cancelDownloadAtom = (0, jotai.atom)(null, (get) => {
	get(aborterAtom)?.abort();
});
const startDownloadAtom = (0, jotai.atom)(null, async (get, set, options) => {
	const aborter = new AbortController();
	set(aborterAtom, (previous) => {
		previous?.abort();
		return aborter;
	});
	const viewerOptions = get(viewerOptionsAtom);
	const source = options?.source ?? viewerOptions.source;
	if (!source) {
		return;
	}
	let toastId = null;
	addEventListener("beforeunload", confirmDownloadAbort);
	try {
		toastId = (0, react_toastify.toast)( (0, react_jsx_runtime.jsx)(DownloadCancel, { onClick: aborter.abort }), {
			autoClose: false,
			progress: 0
		});
		return await download(source, {
			onProgress: reportProgress,
			onError: logIfNotAborted,
			signal: aborter.signal
		});
	} finally {
		removeEventListener("beforeunload", confirmDownloadAbort);
	}
	async function reportProgress(event) {
		if (!toastId) {
			return;
		}
		const { total, started, settled, rejected, status } = event;
		const value = started / total * .1 + settled / total * .89;
		switch (status) {
			case "ongoing":
				react_toastify.toast.update(toastId, {
					type: rejected > 0 ? "warning" : "default",
					progress: value
				});
				break;
			case "complete":
				react_toastify.toast.update(toastId, {
					type: "success",
					render: get(i18nAtom).downloadComplete,
					progress: .9999
				});
				await timeout(1e3);
				react_toastify.toast.done(toastId);
				break;
			case "error":
				react_toastify.toast.update(toastId, {
					type: "error",
					render: get(i18nAtom).errorOccurredWhileDownloading,
					progress: 0
				});
				break;
			case "cancelled":
				react_toastify.toast.done(toastId);
				break;
		}
	}
});
const downloadAndSaveAtom = (0, jotai.atom)(null, async (_get, set, options) => {
	const zip$1 = await set(startDownloadAtom, options);
	if (zip$1) {
		await save(new Blob([zip$1]));
	}
});
function logIfNotAborted(error) {
	if (isNotAbort(error)) {
		console.error(error);
	}
}
function isNotAbort(error) {
	return !/aborted/i.test(`${error}`);
}
function confirmDownloadAbort(event) {
	event.preventDefault();
	event.returnValue = "";
}
const gmStorage = {
	getItem: GM.getValue,
	setItem: GM.setValue,
	removeItem: (key) => GM.deleteValue(key),
	subscribe: (key, callback) => {
		const idPromise = GM.addValueChangeListener(key, (_key, _oldValue, newValue) => callback(newValue));
		return async () => {
			const id = await idPromise;
			await GM.removeValueChangeListener(id);
		};
	}
};
function atomWithGmValue(key, defaultValue) {
	return (0, jotai_utils.atomWithStorage)(key, defaultValue, gmStorage, { getOnInit: true });
}
const jsonSessionStorage = (0, jotai_utils.createJSONStorage)(() => sessionStorage);
function atomWithSession(key, defaultValue) {
	return (0, jotai_utils.atomWithStorage)(key, defaultValue, jsonSessionStorage, { getOnInit: true });
}
const defaultPreferences = {
	backgroundColor: "#eeeeee",
	singlePageCount: 1,
	maxZoomOutExponent: 3,
	maxZoomInExponent: 3,
	pageDirection: "rightToLeft",
	isFullscreenPreferred: false,
	fullscreenNoticeCount: 0
};
const scriptPreferencesAtom = (0, jotai.atom)({});
const preferencesPresetAtom = (0, jotai.atom)("default");
const [backgroundColorAtom] = atomWithPreferences("backgroundColor");
const [singlePageCountStorageAtom] = atomWithPreferences("singlePageCount");
const [maxZoomOutExponentAtom] = atomWithPreferences("maxZoomOutExponent");
const [maxZoomInExponentAtom] = atomWithPreferences("maxZoomInExponent");
const [pageDirectionAtom] = atomWithPreferences("pageDirection");
const [isFullscreenPreferredAtom, isFullscreenPreferredPromiseAtom] = atomWithPreferences("isFullscreenPreferred");
const [fullscreenNoticeCountAtom, fullscreenNoticeCountPromiseAtom] = atomWithPreferences("fullscreenNoticeCount");
const wasImmersiveAtom = atomWithSession("vim_comic_viewer.was_immersive", false);
function atomWithPreferences(key) {
	const asyncAtomAtom = (0, jotai.atom)((get) => {
		const preset = get(preferencesPresetAtom);
		const qualifiedKey = `vim_comic_viewer.preferences.${preset}.${key}`;
		return atomWithGmValue(qualifiedKey, undefined);
	});
	const cacheAtom = (0, jotai_cache.atomWithCache)((get) => get(get(asyncAtomAtom)));
	const manualAtom = (0, jotai.atom)((get) => get(cacheAtom), updater);
	const loadableAtom = (0, jotai_utils.loadable)(manualAtom);
	const effectiveAtom = (0, jotai.atom)((get) => {
		const value = get(loadableAtom);
		if (value.state === "hasData" && value.data !== undefined) {
			return value.data;
		}
		return get(scriptPreferencesAtom)[key] ?? defaultPreferences[key];
	}, updater);
	return [effectiveAtom, manualAtom];
	function updater(get, set, update) {
		return set(get(asyncAtomAtom), (value) => typeof update === "function" ? Promise.resolve(value).then(update) : update);
	}
}
const globalCss = document.createElement("style");
globalCss.innerHTML = `html, body {
  overflow: hidden;
}`;
function hideBodyScrollBar(doHide) {
	if (doHide) {
		document.head.append(globalCss);
	} else {
		globalCss.remove();
	}
}
async function setFullscreenElement(element) {
	if (element) {
		await element.requestFullscreen?.();
	} else {
		await document.exitFullscreen?.();
	}
}
function focusWithoutScroll(element) {
	element?.focus({ preventScroll: true });
}
function isUserGesturePermissionError(error) {
	return error?.message === "Permissions check failed";
}
function isDocumentNotActiveError(error) {
	const message = error?.message;
	return message?.match(/Failed to execute '.*?' on 'Document': Document not active/) ?? false;
}
const fullscreenElementAtom = (0, jotai.atom)(null);
const viewerElementAtom = (0, jotai.atom)(null);
const isViewerFullscreenAtom = (0, jotai.atom)((get) => {
	const viewerElement = get(viewerElementAtom);
	return !!viewerElement && viewerElement === get(fullscreenElementAtom);
});
const isImmersiveAtom = (0, jotai.atom)(false);
const isViewerImmersiveAtom = (0, jotai.atom)((get) => get(isImmersiveAtom));
const scrollBarStyleFactorAtom = (0, jotai.atom)((get) => ({
	fullscreenElement: get(fullscreenElementAtom),
	viewerElement: get(viewerElementAtom)
}), (get, set, factors) => {
	const { fullscreenElement, viewerElement, isImmersive } = factors;
	if (fullscreenElement !== undefined) {
		set(fullscreenElementAtom, fullscreenElement);
	}
	if (viewerElement !== undefined) {
		set(viewerElementAtom, viewerElement);
	}
	if (isImmersive !== undefined) {
		set(wasImmersiveAtom, isImmersive);
		set(isImmersiveAtom, isImmersive);
	}
	const canScrollBarDuplicate = !get(isViewerFullscreenAtom) && get(isImmersiveAtom);
	hideBodyScrollBar(canScrollBarDuplicate);
});
const viewerFullscreenAtom = (0, jotai.atom)((get) => {
	get(isFullscreenPreferredAtom);
	return get(isViewerFullscreenAtom);
}, async (get, _set, value) => {
	const element = value ? get(viewerElementAtom) : null;
	const { fullscreenElement } = get(scrollBarStyleFactorAtom);
	if (element === fullscreenElement) {
		return true;
	}
	const fullscreenChange = new Promise((resolve) => {
		addEventListener("fullscreenchange", resolve, { once: true });
	});
	try {
		await setFullscreenElement(element);
		await fullscreenChange;
		return true;
	} catch (error) {
		if (isUserGesturePermissionError(error)) {
			return false;
		}
		throw error;
	}
});
const transitionDeferredAtom = (0, jotai.atom)({});
const transitionLockAtom = (0, jotai.atom)(null, async (get, set) => {
	const { deferred: previousLock } = get(transitionDeferredAtom);
	const lock = deferred();
	set(transitionDeferredAtom, { deferred: lock });
	await previousLock;
	return { deferred: lock };
});
const isFullscreenPreferredSettingsAtom = (0, jotai.atom)((get) => get(isFullscreenPreferredAtom), async (get, set, value) => {
	const promise = set(isFullscreenPreferredAtom, value);
	const appliedValue = value === jotai_utils.RESET ? (await promise, get(isFullscreenPreferredAtom)) : value;
	const lock = await set(transitionLockAtom);
	try {
		const wasImmersive = get(wasImmersiveAtom);
		const shouldEnterFullscreen = appliedValue && wasImmersive;
		await set(viewerFullscreenAtom, shouldEnterFullscreen);
	} finally {
		lock.deferred.resolve();
	}
});
const beforeRepaintAtom = (0, jotai.atom)({});
const useBeforeRepaint = () => {
	const { task } = (0, jotai.useAtomValue)(beforeRepaintAtom);
	(0, react.useLayoutEffect)(() => {
		task?.();
	}, [task]);
};
function getCurrentRow({ elements, viewportHeight }) {
	if (!elements.length) {
		return;
	}
	const scrollCenter = viewportHeight / 2;
	const pages = elements.map((page) => ({
		page,
		rect: page.getBoundingClientRect()
	}));
	return pages.filter(isCenterCrossing);
	function isCenterCrossing({ rect: { y, height } }) {
		return y <= scrollCenter && y + height >= scrollCenter;
	}
}
function isVisible(element) {
	if ("checkVisibility" in element) {
		return element.checkVisibility();
	}
	const { x, y, width, height } = element.getBoundingClientRect();
	const elements = document.elementsFromPoint(x + width / 2, y + height / 2);
	return elements.includes(element);
}
function hasNoticeableDifference(middle, lastMiddle) {
	return Math.abs(middle - lastMiddle) > .01;
}
function getInPageRatio({ page, viewportHeight }) {
	const scrollCenter = viewportHeight / 2;
	const { y, height } = page.rect;
	return 1 - (y + height - scrollCenter) / height;
}
function getScrollPage(middle, container) {
	const element = getPagesFromScrollElement(container)?.item(Math.floor(middle));
	return element instanceof HTMLElement ? element : null;
}
function getCurrentMiddleFromScrollElement({ scrollElement, previousMiddle }) {
	const elements = getPagesFromScrollElement(scrollElement);
	if (!elements || !scrollElement) {
		return null;
	}
	return getPageScroll({
		elements: [...elements],
		viewportHeight: scrollElement.getBoundingClientRect().height,
		previousMiddle
	});
}
function getNewSizeIfResized({ scrollElement, previousSize }) {
	if (!scrollElement) {
		return;
	}
	const { width, height } = scrollElement.getBoundingClientRect();
	const scrollHeight = scrollElement.scrollHeight;
	const { width: previousWidth, height: previousHeight, scrollHeight: previousScrollHeight } = previousSize;
	const needsScrollRestoration = previousWidth === 0 || previousHeight === 0 || previousWidth !== width || previousHeight !== height || previousScrollHeight !== scrollHeight;
	return needsScrollRestoration ? {
		width,
		height,
		scrollHeight
	} : undefined;
}
function navigateByPointer(scrollElement, event) {
	const height = scrollElement?.clientHeight;
	if (!height || event.button !== 0) {
		return;
	}
	event.preventDefault();
	const isTop = event.clientY < height / 2;
	if (isTop) {
		goToPreviousArea(scrollElement);
	} else {
		goToNextArea(scrollElement);
	}
}
function goToPreviousArea(scrollElement) {
	const page = getCurrentPageFromScrollElement({
		scrollElement,
		previousMiddle: Infinity
	});
	if (!page || !scrollElement) {
		return;
	}
	const { height: viewerHeight, top: viewerTop } = scrollElement.getBoundingClientRect();
	const ignorableHeight = viewerHeight * .05;
	const { top: pageTop } = page.getBoundingClientRect();
	const remainingHeight = viewerTop - pageTop;
	const needsPartialScroll = remainingHeight > ignorableHeight;
	if (needsPartialScroll) {
		const divisor = Math.ceil(remainingHeight / viewerHeight);
		const yDiff = -Math.ceil(remainingHeight / divisor);
		scrollElement.scrollBy({ top: yDiff });
	} else {
		goToPreviousRow(page);
	}
}
function goToNextArea(scrollElement) {
	const page = getCurrentPageFromScrollElement({
		scrollElement,
		previousMiddle: 0
	});
	if (!page || !scrollElement) {
		return;
	}
	const { height: viewerHeight, bottom: viewerBottom } = scrollElement.getBoundingClientRect();
	const ignorableHeight = viewerHeight * .05;
	const { bottom: pageBottom } = page.getBoundingClientRect();
	const remainingHeight = pageBottom - viewerBottom;
	const needsPartialScroll = remainingHeight > ignorableHeight;
	if (needsPartialScroll) {
		const divisor = Math.ceil(remainingHeight / viewerHeight);
		const yDiff = Math.ceil(remainingHeight / divisor);
		scrollElement.scrollBy({ top: yDiff });
	} else {
		goToNextRow(page);
	}
}
function toWindowScroll({ middle, lastMiddle, noSyncScroll, forFullscreen, scrollElement }) {
	if (noSyncScroll || !forFullscreen && !hasNoticeableDifference(middle, lastMiddle)) {
		return;
	}
	const page = getScrollPage(middle, scrollElement);
	const src = page?.querySelector("img[src], video[src]")?.src;
	if (!src) {
		return;
	}
	const original = findOriginElement(src, page);
	if (!original) {
		return;
	}
	const rect = original.getBoundingClientRect();
	const ratio = middle - Math.floor(middle);
	const top = scrollY + rect.y + rect.height * ratio - innerHeight / 2;
	return top;
}
function getYDifferenceFromPrevious({ scrollable, middle }) {
	const page = getScrollPage(middle, scrollable);
	if (!page || !scrollable || scrollable.clientHeight < 1) {
		return;
	}
	const { height: scrollableHeight } = scrollable.getBoundingClientRect();
	const { y: pageY, height: pageHeight } = page.getBoundingClientRect();
	const ratio = middle - Math.floor(middle);
	const restoredYDiff = pageY + pageHeight * ratio - scrollableHeight / 2;
	return restoredYDiff;
}
function getAbovePageIndex(scrollElement) {
	const children = getPagesFromScrollElement(scrollElement);
	if (!children || !scrollElement) {
		return;
	}
	const elements = [...children];
	const currentRow = getCurrentRow({
		elements,
		viewportHeight: scrollElement.clientHeight
	});
	const firstPage = currentRow?.[0]?.page;
	return firstPage ? elements.indexOf(firstPage) : undefined;
}
function findOriginElement(src, page) {
	const fileName = src.split("/").pop()?.split("?")[0];
	const candidates = document.querySelectorAll(`img[src*="${fileName}"], video[src*="${fileName}"]`);
	const originals = [...candidates].filter((media) => media.src === src && media.parentElement !== page && isVisible(media));
	if (originals.length === 1) {
		return originals[0];
	}
	const links = document.querySelectorAll(`a[href*="${fileName}"`);
	const visibleLinks = [...links].filter(isVisible);
	if (visibleLinks.length === 1) {
		return visibleLinks[0];
	}
}
function goToNextRow(currentPage) {
	const epsilon = .01;
	const currentPageBottom = currentPage.getBoundingClientRect().bottom - epsilon;
	let page = currentPage;
	while (page.nextElementSibling) {
		page = page.nextElementSibling;
		const pageTop = page.getBoundingClientRect().top;
		const isNextPage = currentPageBottom <= pageTop;
		if (isNextPage) {
			page.scrollIntoView({
				behavior: "instant",
				block: "start"
			});
			return;
		}
	}
	page.scrollIntoView({
		behavior: "instant",
		block: "end"
	});
}
function goToPreviousRow(currentPage) {
	const epsilon = .01;
	const currentPageTop = currentPage.getBoundingClientRect().top + epsilon;
	let page = currentPage;
	while (page.previousElementSibling) {
		page = page.previousElementSibling;
		const pageBottom = page.getBoundingClientRect().bottom;
		const isPreviousPage = pageBottom <= currentPageTop;
		if (isPreviousPage) {
			page.scrollIntoView({
				behavior: "instant",
				block: "end"
			});
			return;
		}
	}
	page.scrollIntoView({
		behavior: "instant",
		block: "start"
	});
}
function getCurrentPageFromScrollElement({ scrollElement, previousMiddle }) {
	const middle = getCurrentMiddleFromScrollElement({
		scrollElement,
		previousMiddle
	});
	if (!middle || !scrollElement) {
		return null;
	}
	return getScrollPage(middle, scrollElement);
}
function getPageScroll(params) {
	const currentPage = getCurrentPageFromElements(params);
	return currentPage ? getMiddle(currentPage) : undefined;
	function getMiddle(page) {
		const { viewportHeight, elements } = params;
		const ratio = getInPageRatio({
			page,
			viewportHeight
		});
		return elements.indexOf(page.page) + ratio;
	}
}
function getCurrentPageFromElements({ elements, viewportHeight, previousMiddle }) {
	const currentRow = getCurrentRow({
		elements,
		viewportHeight
	});
	if (!currentRow) {
		return;
	}
	return selectColumn(currentRow);
	function selectColumn(row) {
		const firstPage = row.find(({ page: page$1 }) => page$1 === elements[0]);
		if (firstPage) {
			return firstPage;
		}
		const lastPage = row.find(({ page: page$1 }) => page$1 === elements.at(-1));
		if (lastPage) {
			return lastPage;
		}
		const half = Math.floor(row.length / 2);
		if (row.length % 2 === 1) {
			return row[half];
		}
		const page = row[half]?.page;
		if (!page) {
			return;
		}
		const centerNextTop = elements.indexOf(page);
		const previousMiddlePage = previousMiddle < centerNextTop ? row[half - 1] : row[half];
		return previousMiddlePage;
	}
}
function getPagesFromScrollElement(scrollElement) {
	return scrollElement?.firstElementChild?.children;
}
function toViewerScroll({ scrollable, lastWindowToViewerMiddle, noSyncScroll }) {
	if (!scrollable || noSyncScroll) {
		return;
	}
	const viewerMedia = [...scrollable.querySelectorAll("img[src], video[src]")];
	const urlToViewerPages = new Map();
	for (const media$1 of viewerMedia) {
		urlToViewerPages.set(media$1.src, media$1);
	}
	const urls = [...urlToViewerPages.keys()];
	const media = getUrlMedia(urls);
	const siteMedia = media.filter((medium) => !viewerMedia.includes(medium));
	const visibleMedia = siteMedia.filter(isVisible);
	const viewportHeight = visualViewport?.height ?? innerHeight;
	const currentRow = getCurrentRow({
		elements: visibleMedia,
		viewportHeight
	});
	if (!currentRow) {
		return;
	}
	const indexed = currentRow.map((sized) => [sized, getUrlIndex(sized.page, urls)]);
	const last = lastWindowToViewerMiddle - .5;
	const sorted = indexed.sort((a, b) => Math.abs(a[1] - last) - Math.abs(b[1] - last));
	const [page, index] = sorted[0] ?? [];
	if (!page || index === undefined) {
		return;
	}
	const pageRatio = getInPageRatio({
		page,
		viewportHeight
	});
	const snappedRatio = Math.abs(pageRatio - .5) < .1 ? .5 : pageRatio;
	if (!hasNoticeableDifference(index + snappedRatio, lastWindowToViewerMiddle)) {
		return;
	}
	return index + snappedRatio;
}
function getUrlMedia(urls) {
	const media = document.querySelectorAll("img, video");
	return [...media].filter((medium) => getUrlIndex(medium, urls) !== -1);
}
function getUrlIndex(medium, urls) {
	if (medium instanceof HTMLImageElement) {
		const img = medium;
		const parent = img.parentElement;
		const imgUrlIndex = urls.findIndex((x) => x === img.src);
		const pictureUrlIndex = parent instanceof HTMLPictureElement ? getUrlIndexFromSrcset(parent, urls) : -1;
		return imgUrlIndex === -1 ? pictureUrlIndex : imgUrlIndex;
	} else if (medium instanceof HTMLVideoElement) {
		const video = medium;
		const videoUrlIndex = urls.findIndex((x) => x === video.src);
		const srcsetUrlIndex = getUrlIndexFromSrcset(video, urls);
		return videoUrlIndex === -1 ? srcsetUrlIndex : videoUrlIndex;
	}
	return -1;
}
function getUrlIndexFromSrcset(media, urls) {
	for (const url of getUrlsFromSources(media)) {
		const index = urls.findIndex((x) => x === url);
		if (index !== -1) {
			return index;
		}
	}
	return -1;
}
function getUrlsFromSources(picture) {
	const sources = [...picture.querySelectorAll("source")];
	return sources.flatMap((x) => getSrcFromSrcset(x.srcset));
}
function getSrcFromSrcset(srcset) {
	return srcset.split(",").map((x) => x.split(/\s+/)[0]).filter((x) => x !== undefined);
}
const scrollElementStateAtom = (0, jotai.atom)(null);
const scrollElementAtom = (0, jotai.atom)((get) => get(scrollElementStateAtom)?.div ?? null);
const scrollElementSizeAtom = (0, jotai.atom)({
	width: 0,
	height: 0,
	scrollHeight: 0
});
const pageScrollMiddleAtom = (0, jotai.atom)(.5);
const lastViewerToWindowMiddleAtom = (0, jotai.atom)(-1);
const lastWindowToViewerMiddleAtom = (0, jotai.atom)(-1);
const transferWindowScrollToViewerAtom = (0, jotai.atom)(null, (get, set) => {
	const scrollable = get(scrollElementAtom);
	const lastWindowToViewerMiddle = get(lastWindowToViewerMiddleAtom);
	const noSyncScroll = get(viewerOptionsAtom).noSyncScroll ?? false;
	const middle = toViewerScroll({
		scrollable,
		lastWindowToViewerMiddle,
		noSyncScroll
	});
	if (!middle) {
		return;
	}
	set(pageScrollMiddleAtom, middle);
	set(lastWindowToViewerMiddleAtom, middle);
});
const transferViewerScrollToWindowAtom = (0, jotai.atom)(null, (get, set, { forFullscreen } = {}) => {
	const middle = get(pageScrollMiddleAtom);
	const scrollElement = get(scrollElementAtom);
	const lastMiddle = get(lastViewerToWindowMiddleAtom);
	const noSyncScroll = get(viewerOptionsAtom).noSyncScroll ?? false;
	const top = toWindowScroll({
		middle,
		lastMiddle,
		scrollElement,
		noSyncScroll,
		forFullscreen
	});
	if (top !== undefined) {
		set(lastViewerToWindowMiddleAtom, middle);
		scroll({
			behavior: "instant",
			top
		});
	}
});
const synchronizeScrollAtom = (0, jotai.atom)(null, (get, set) => {
	const scrollElement = get(scrollElementAtom);
	if (!scrollElement) {
		return;
	}
	if (set(correctScrollAtom)) {
		return;
	}
	const middle = getCurrentMiddleFromScrollElement({
		scrollElement,
		previousMiddle: get(pageScrollMiddleAtom)
	});
	if (middle) {
		set(pageScrollMiddleAtom, middle);
		set(transferViewerScrollToWindowAtom);
	}
});
const correctScrollAtom = (0, jotai.atom)(null, (get, set) => {
	const scrollElement = get(scrollElementAtom);
	const previousSize = get(scrollElementSizeAtom);
	const newSize = getNewSizeIfResized({
		scrollElement,
		previousSize
	});
	if (!newSize) {
		return false;
	}
	set(scrollElementSizeAtom, newSize);
	set(restoreScrollAtom);
	return true;
});
const restoreScrollAtom = (0, jotai.atom)(null, (get, set) => {
	const middle = get(pageScrollMiddleAtom);
	const scrollable = get(scrollElementAtom);
	const restored = getYDifferenceFromPrevious({
		scrollable,
		middle
	});
	if (restored != null) {
		scrollable?.scrollBy({ top: restored });
		set(beforeRepaintAtom, { task: () => set(correctScrollAtom) });
	}
});
const goNextAtom = (0, jotai.atom)(null, (get) => {
	goToNextArea(get(scrollElementAtom));
});
const goPreviousAtom = (0, jotai.atom)(null, (get) => {
	goToPreviousArea(get(scrollElementAtom));
});
const navigateAtom = (0, jotai.atom)(null, (get, _set, event) => {
	navigateByPointer(get(scrollElementAtom), event);
});
const singlePageCountAtom = (0, jotai.atom)((get) => get(singlePageCountStorageAtom), async (get, set, value) => {
	const clampedValue = typeof value === "number" ? Math.max(0, value) : value;
	const middle = get(pageScrollMiddleAtom);
	const scrollElement = get(scrollElementAtom);
	await set(singlePageCountStorageAtom, clampedValue);
	set(beforeRepaintAtom, { task: () => {
		const yDifference = getYDifferenceFromPrevious({
			scrollable: scrollElement,
			middle
		});
		if (yDifference != null) {
			scrollElement?.scrollBy({ top: yDifference });
		}
		set(pageScrollMiddleAtom, middle);
	} });
});
const anchorSinglePageCountAtom = (0, jotai.atom)(null, (get, set) => {
	const scrollElement = get(scrollElementAtom);
	const abovePageIndex = getAbovePageIndex(scrollElement);
	if (abovePageIndex !== undefined) {
		set(singlePageCountAtom, abovePageIndex);
	}
});

const maxSizeStateAtom = (0, jotai.atom)({
	width: screen.width,
	height: screen.height
});
const maxSizeAtom = (0, jotai.atom)((get) => get(maxSizeStateAtom), (get, set, size) => {
	const current = get(maxSizeStateAtom);
	if (size.width <= current.width && size.height <= current.height) {
		return;
	}
	set(maxSizeStateAtom, {
		width: Math.max(size.width, current.width),
		height: Math.max(size.height, current.height)
	});
});
const mediaSourcesAtom = (0, jotai.atom)([]);
const pageAtomsAtom = (0, jotai.atom)([]);
const refreshMediaSourceAtom = (0, jotai.atom)(null, async (get, set, params) => {
	const { source } = get(viewerOptionsAtom);
	if (!source) {
		return;
	}
	const medias = await source({
		...params,
		maxSize: get(maxSizeAtom)
	});
	if (source !== get(viewerOptionsAtom).source) {
		return;
	}
	if (!Array.isArray(medias)) {
		throw new Error(`Invalid comic source type: ${typeof medias}`);
	}
	set(mediaSourcesAtom, medias);
	if (params.cause === "load" && params.page === undefined) {
		set(pageAtomsAtom, medias.map((media, index) => createPageAtom({
			initialSource: media,
			index,
			set
		})));
	}
	if (params.page !== undefined) {
		return medias[params.page];
	}
});
function createPageAtom(params) {
	const { initialSource, index, set } = params;
	const triedUrls = [];
	let div = null;
	const stateAtom = (0, jotai.atom)({
		status: "loading",
		source: initialSource ? toAdvancedObject(initialSource) : { src: undefined }
	});
	const loadAtom = (0, jotai.atom)(null, async (get, set$1, cause) => {
		switch (cause) {
			case "load":
				triedUrls.length = 0;
				break;
			case "error": break;
		}
		if (isComplete()) {
			return;
		}
		let newSource;
		try {
			newSource = await set$1(refreshMediaSourceAtom, {
				cause,
				page: index
			});
		} catch (error) {
			console.error(error);
			set$1(stateAtom, (previous) => ({
				...previous,
				status: "error",
				urls: Array.from(triedUrls)
			}));
			return;
		}
		if (isComplete()) {
			return;
		}
		if (isDelay(newSource)) {
			set$1(stateAtom, {
				status: "error",
				urls: [],
				source: { src: undefined }
			});
			return;
		}
		const source = toAdvancedSource(newSource);
		triedUrls.push(source.src);
		set$1(stateAtom, {
			status: "loading",
			source
		});
		function isComplete() {
			return get(stateAtom).status === "complete";
		}
	});
	const aggregateAtom = (0, jotai.atom)((get) => {
		get(loadAtom);
		const state = get(stateAtom);
		const scrollElementSize = get(scrollElementSizeAtom);
		const compactWidthIndex = get(singlePageCountAtom);
		const maxZoomInExponent = get(maxZoomInExponentAtom);
		const maxZoomOutExponent = get(maxZoomOutExponentAtom);
		const { src, width, height } = state.source ?? {};
		const ratio = getImageToViewerSizeRatio({
			viewerSize: scrollElementSize,
			imgSize: {
				width,
				height
			}
		});
		const shouldBeOriginalSize = shouldMediaBeOriginalSize({
			maxZoomInExponent,
			maxZoomOutExponent,
			mediaRatio: ratio
		});
		const isLarge = ratio > 1;
		const canMessUpRow = shouldBeOriginalSize && isLarge;
		const mediaProps = {
			src,
			onError: reload
		};
		const divCss = {
			...shouldBeOriginalSize ? {
				minHeight: scrollElementSize.height,
				height: "auto"
			} : { height: scrollElementSize.height },
			...state.status !== "complete" ? { aspectRatio: width && height ? `${width} / ${height}` : "3 / 4" } : {}
		};
		const page = {
			index,
			state,
			div,
			setDiv: (newDiv) => {
				div = newDiv;
			},
			reloadAtom: loadAtom,
			fullWidth: index < compactWidthIndex || canMessUpRow,
			shouldBeOriginalSize,
			divCss,
			imageProps: state.source && state.source.type !== "video" ? {
				...mediaProps,
				onLoad: setCompleteState
			} : undefined,
			videoProps: state.source?.type === "video" ? {
				...mediaProps,
				controls: true,
				autoPlay: true,
				loop: true,
				muted: true,
				onLoadedMetadata: setCompleteState
			} : undefined
		};
		return page;
	});
	async function reload() {
		const isOverMaxRetry = triedUrls.length > MAX_RETRY_COUNT;
		const urlCountMap = triedUrls.reduce((acc, url) => {
			acc[url] = (acc[url] ?? 0) + 1;
			return acc;
		}, {});
		const isOverSameUrlRetry = Object.values(urlCountMap).some((count) => count > MAX_SAME_URL_RETRY_COUNT);
		if (isOverMaxRetry || isOverSameUrlRetry) {
			set(stateAtom, (previous) => ({
				...previous,
				status: "error",
				urls: [...new Set(triedUrls)]
			}));
			return;
		}
		set(stateAtom, (previous) => ({
			status: "loading",
			source: {
				...previous.source,
				src: undefined
			}
		}));
		await set(loadAtom, "error");
	}
	function setCompleteState(event) {
		const element = event.currentTarget;
		set(stateAtom, {
			status: "complete",
			source: {
				src: element.src,
				...element instanceof HTMLImageElement ? {
					type: "image",
					width: element.naturalWidth,
					height: element.naturalHeight
				} : {
					type: "video",
					width: element.videoWidth,
					height: element.videoHeight
				}
			}
		});
	}
	if (isDelay(initialSource)) {
		set(loadAtom, "load");
	}
	return aggregateAtom;
}
function getImageToViewerSizeRatio({ viewerSize, imgSize }) {
	if (!imgSize.height && !imgSize.width) {
		return 1;
	}
	return Math.max((imgSize.height ?? 0) / viewerSize.height, (imgSize.width ?? 0) / viewerSize.width);
}
function shouldMediaBeOriginalSize({ maxZoomOutExponent, maxZoomInExponent, mediaRatio }) {
	const minZoomRatio = Math.sqrt(2) ** maxZoomOutExponent;
	const maxZoomRatio = Math.sqrt(2) ** maxZoomInExponent;
	const isOver = minZoomRatio < mediaRatio || mediaRatio < 1 / maxZoomRatio;
	return isOver;
}
const externalFocusElementAtom = (0, jotai.atom)(null);
const setViewerImmersiveAtom = (0, jotai.atom)(null, async (get, set, value) => {
	const lock = await set(transitionLockAtom);
	try {
		await transactImmersive(get, set, value);
	} finally {
		lock.deferred.resolve();
	}
});
async function transactImmersive(get, set, value) {
	if (get(isViewerImmersiveAtom) === value) {
		return;
	}
	if (value) {
		set(externalFocusElementAtom, (previous) => previous ? previous : document.activeElement);
		set(transferWindowScrollToViewerAtom);
	}
	const scrollable = get(scrollElementAtom);
	if (!scrollable) {
		return;
	}
	const { fullscreenElement } = get(scrollBarStyleFactorAtom);
	try {
		if (get(isFullscreenPreferredAtom)) {
			const isAccepted = await set(viewerFullscreenAtom, value);
			if (!isAccepted) {
				const noticeCount = await get(fullscreenNoticeCountPromiseAtom) ?? 0;
				if (shouldShowF11Guide({ noticeCount })) {
					showF11Guide();
					return;
				}
			}
		}
	} finally {
		set(scrollBarStyleFactorAtom, { isImmersive: value });
		if (value) {
			focusWithoutScroll(scrollable);
		} else {
			if (fullscreenElement) {
				set(transferViewerScrollToWindowAtom, { forFullscreen: true });
			}
			const externalFocusElement = get(externalFocusElementAtom);
			focusWithoutScroll(externalFocusElement);
		}
	}
	function showF11Guide() {
		(0, react_toastify.toast)(get(i18nAtom).fullScreenRestorationGuide, {
			type: "info",
			onClose: () => {
				set(fullscreenNoticeCountPromiseAtom, (count) => (count ?? 0) + 1);
			}
		});
	}
}
const isBeforeUnloadAtom = (0, jotai.atom)(false);
const beforeUnloadAtom = (0, jotai.atom)(null, async (_get, set) => {
	set(isBeforeUnloadAtom, true);
	await waitUnloadFinishRoughly();
	set(isBeforeUnloadAtom, false);
});
beforeUnloadAtom.onMount = (set) => {
	addEventListener("beforeunload", set);
	return () => removeEventListener("beforeunload", set);
};
const fullscreenSynchronizationAtom = (0, jotai.atom)((get) => {
	get(isBeforeUnloadAtom);
	return get(scrollBarStyleFactorAtom).fullscreenElement;
}, (get, set, element) => {
	const isFullscreenPreferred = get(isFullscreenPreferredAtom);
	const isFullscreen = element === get(scrollBarStyleFactorAtom).viewerElement;
	const wasImmersive = get(isViewerImmersiveAtom);
	const isViewerFullscreenExit = wasImmersive && !isFullscreen;
	const isNavigationExit = get(isBeforeUnloadAtom);
	const shouldExitImmersive = isFullscreenPreferred && isViewerFullscreenExit && !isNavigationExit;
	set(scrollBarStyleFactorAtom, {
		fullscreenElement: element,
		isImmersive: shouldExitImmersive ? false : undefined
	});
});
fullscreenSynchronizationAtom.onMount = (set) => {
	const notify = () => set(document.fullscreenElement ?? null);
	document.addEventListener("fullscreenchange", notify);
	return () => document.removeEventListener("fullscreenchange", notify);
};
const setViewerElementAtom = (0, jotai.atom)(null, (_get, set, element) => {
	set(scrollBarStyleFactorAtom, { viewerElement: element });
});
const viewerModeAtom = (0, jotai.atom)((get) => {
	const isFullscreen = get(viewerFullscreenAtom);
	const isImmersive = get(isViewerImmersiveAtom);
	return isFullscreen ? "fullscreen" : isImmersive ? "window" : "normal";
});
const setViewerOptionsAtom = (0, jotai.atom)(null, async (get, set, options) => {
	try {
		const { source } = options;
		const previousOptions = get(viewerOptionsAtom);
		const shouldLoadSource = source && source !== previousOptions.source;
		if (!shouldLoadSource) {
			return;
		}
		set(viewerStatusAtom, (previous) => previous === "complete" ? "complete" : "loading");
		set(viewerOptionsAtom, options);
		await set(refreshMediaSourceAtom, { cause: "load" });
		set(viewerStatusAtom, "complete");
	} catch (error) {
		set(viewerStatusAtom, "error");
		throw error;
	}
});
const reloadErroredAtom = (0, jotai.atom)(null, (get, set) => {
	stop();
	for (const page of get(pageAtomsAtom).map(get)) {
		if (page.state.status !== "complete") {
			set(page.reloadAtom, "load");
		}
	}
});
const toggleImmersiveAtom = (0, jotai.atom)(null, async (get, set) => {
	const hasPermissionIssue = get(viewerModeAtom) === "window" && get(isFullscreenPreferredAtom);
	if (hasPermissionIssue) {
		await set(viewerFullscreenAtom, true);
		return;
	}
	await set(setViewerImmersiveAtom, !get(isViewerImmersiveAtom));
});
const toggleFullscreenAtom = (0, jotai.atom)(null, async (get, set) => {
	set(isFullscreenPreferredSettingsAtom, !get(isFullscreenPreferredSettingsAtom));
	if (get(viewerModeAtom) === "normal") {
		await set(setViewerImmersiveAtom, true);
	}
});
const blockSelectionAtom = (0, jotai.atom)(null, (_get, set, event) => {
	if (event.detail >= 2) {
		event.preventDefault();
	}
	if (event.buttons === 3) {
		set(toggleImmersiveAtom);
		event.preventDefault();
	}
});

async function waitUnloadFinishRoughly() {
	for (let i = 0; i < 5; i++) {
		await timeout(100);
	}
}
function shouldShowF11Guide({ noticeCount }) {
	const isUserFullscreen = innerHeight === screen.height || innerWidth === screen.width;
	return noticeCount < 3 && !isUserFullscreen;
}
const controllerPrimitiveAtom = (0, jotai.atom)(null);
const controllerAtom = (0, jotai.atom)((get) => get(controllerPrimitiveAtom), (get, set) => {
	const existing = get(controllerPrimitiveAtom);
	if (existing) {
		return existing;
	}
	const controller = new Controller(get, set);
	set(controllerPrimitiveAtom, controller);
	return controller;
});
controllerAtom.onMount = (set) => void set();
const effectivePreferencesAtom = (0, jotai.atom)((get) => ({
	backgroundColor: get(backgroundColorAtom),
	singlePageCount: get(singlePageCountStorageAtom),
	maxZoomOutExponent: get(maxZoomOutExponentAtom),
	maxZoomInExponent: get(maxZoomInExponentAtom),
	pageDirection: get(pageDirectionAtom),
	isFullscreenPreferred: get(isFullscreenPreferredAtom),
	fullscreenNoticeCount: get(fullscreenNoticeCountAtom)
}), (get, set, update) => {
	if (typeof update === "function") {
		const preferences = get(effectivePreferencesAtom);
		const newPreferences = update(preferences);
		return updatePreferences(newPreferences);
	}
	return updatePreferences(update);
	function updatePreferences(preferences) {
		return Promise.all([
			updateIfDefined(backgroundColorAtom, preferences.backgroundColor),
			updateIfDefined(singlePageCountAtom, preferences.singlePageCount),
			updateIfDefined(maxZoomOutExponentAtom, preferences.maxZoomOutExponent),
			updateIfDefined(maxZoomInExponentAtom, preferences.maxZoomInExponent),
			updateIfDefined(pageDirectionAtom, preferences.pageDirection),
			updateIfDefined(isFullscreenPreferredAtom, preferences.isFullscreenPreferred),
			updateIfDefined(fullscreenNoticeCountAtom, preferences.fullscreenNoticeCount)
		]);
	}
	function updateIfDefined(atom$2, value) {
		return value !== undefined ? set(atom$2, value) : Promise.resolve();
	}
});
var Controller = class {
	currentElementKeyHandler = null;
	constructor(get, set) {
		this.get = get;
		this.set = set;
		addEventListener("keydown", this.defaultGlobalKeyHandler);
		this.elementKeyHandler = this.defaultElementKeyHandler;
	}
	get options() {
		return this.get(viewerOptionsAtom);
	}
	get status() {
		return this.get(viewerStatusAtom);
	}
	get container() {
		return this.get(scrollBarStyleFactorAtom).viewerElement;
	}
	downloader = {
		download: (options) => this.set(startDownloadAtom, options),
		downloadAndSave: (options) => this.set(downloadAndSaveAtom, options),
		cancel: () => this.set(cancelDownloadAtom)
	};
	get pages() {
		return this.get(pageAtomsAtom).map(this.get);
	}
	get viewerMode() {
		return this.get(viewerModeAtom);
	}
	get effectivePreferences() {
		return this.get(effectivePreferencesAtom);
	}
	set elementKeyHandler(handler) {
		const { currentElementKeyHandler, container } = this;
		const scrollable = this.container?.querySelector("div[data-overlayscrollbars-viewport]");
		if (currentElementKeyHandler) {
			container?.removeEventListener("keydown", currentElementKeyHandler);
			scrollable?.removeEventListener("keydown", currentElementKeyHandler);
		}
		if (handler) {
			container?.addEventListener("keydown", handler);
			scrollable?.addEventListener("keydown", handler);
		}
	}
	setOptions = (value) => {
		return this.set(setViewerOptionsAtom, value);
	};
	goPrevious = () => {
		this.set(goPreviousAtom);
	};
	goNext = () => {
		this.set(goNextAtom);
	};
	setManualPreferences = (value) => {
		return this.set(effectivePreferencesAtom, value);
	};
	setScriptPreferences = ({ manualPreset, preferences }) => {
		if (manualPreset) {
			this.set(preferencesPresetAtom, manualPreset);
		}
		if (preferences) {
			this.set(scriptPreferencesAtom, preferences);
		}
	};
	setImmersive = (value) => {
		return this.set(setViewerImmersiveAtom, value);
	};
	setIsFullscreenPreferred = (value) => {
		return this.set(isFullscreenPreferredSettingsAtom, value);
	};
	toggleImmersive = () => {
		this.set(toggleImmersiveAtom);
	};
	toggleFullscreen = () => {
		this.set(toggleFullscreenAtom);
	};
	reloadErrored = () => {
		this.set(reloadErroredAtom);
	};
	unmount = () => {
		return this.get(rootAtom)?.unmount();
	};
	defaultElementKeyHandler = (event) => {
		if (maybeNotHotkey(event)) {
			return false;
		}
		const isHandled = this.handleElementKey(event);
		if (isHandled) {
			event.stopPropagation();
			event.preventDefault();
		}
		return isHandled;
	};
	defaultGlobalKeyHandler = (event) => {
		if (maybeNotHotkey(event)) {
			return false;
		}
		if ([
			"KeyI",
			"Numpad0",
			"Enter"
		].includes(event.code)) {
			if (event.shiftKey) {
				this.toggleFullscreen();
			} else {
				this.toggleImmersive();
			}
			return true;
		}
		return false;
	};
	handleElementKey(event) {
		switch (event.code) {
			case "KeyJ":
			case "ArrowDown":
			case "KeyQ":
				this.goNext();
				return true;
			case "KeyK":
			case "ArrowUp":
				this.goPrevious();
				return true;
			case "KeyH":
			case "ArrowLeft":
				if (this.options.onPreviousSeries) {
					this.options.onPreviousSeries();
					return true;
				}
				return false;
			case "KeyL":
			case "ArrowRight":
			case "KeyW":
				if (this.options.onNextSeries) {
					this.options.onNextSeries();
					return true;
				}
				return false;
			case "Semicolon":
				this.downloader?.downloadAndSave();
				return true;
			case "Comma":
				void this.addSinglePageCount(-1);
				return true;
			case "Period":
				void this.addSinglePageCount(1);
				return true;
			case "Slash":
				this.set(anchorSinglePageCountAtom);
				return true;
			case "Quote":
				this.reloadErrored();
				return true;
			default: return false;
		}
	}
	async addSinglePageCount(diff) {
		await this.setManualPreferences((preferences) => ({
			...preferences,
			singlePageCount: this.effectivePreferences.singlePageCount + diff
		}));
	}
};
function maybeNotHotkey(event) {
	const { ctrlKey, altKey, metaKey } = event;
	return ctrlKey || altKey || metaKey || isTyping(event);
}
const setScrollElementAtom = (0, jotai.atom)(null, async (get, set, div) => {
	const previous = get(scrollElementStateAtom);
	if (previous?.div === div) {
		return;
	}
	previous?.resizeObserver.disconnect();
	if (div === null) {
		set(scrollElementStateAtom, null);
		return;
	}
	const setScrollElementSize = () => {
		const size = div.getBoundingClientRect();
		set(maxSizeAtom, size);
		set(correctScrollAtom);
	};
	const resizeObserver = new ResizeObserver(setScrollElementSize);
	resizeObserver.observe(div);
	resizeObserver.observe(div.firstElementChild);
	div.addEventListener("wheel", navigateWithWheel);
	function navigateWithWheel(event) {
		const unit = event.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? 10 : 1;
		const diff = event.deltaY / unit;
		if (diff >= 1) {
			set(goNextAtom);
		} else if (diff <= -1) {
			set(goPreviousAtom);
		}
		event.preventDefault();
		event.stopPropagation();
	}
	set(scrollElementStateAtom, {
		div,
		resizeObserver
	});
	setScrollElementSize();
	await get(isFullscreenPreferredPromiseAtom);
	await set(setViewerImmersiveAtom, get(wasImmersiveAtom));
	return () => {
		div.removeEventListener("wheel", navigateWithWheel);
	};
});
const Svg = styled("svg", {
	opacity: "50%",
	filter: "drop-shadow(0 0 1px white) drop-shadow(0 0 1px white)",
	color: "black"
});
const downloadCss = { width: "40px" };
const fullscreenCss = {
	position: "absolute",
	right: "1%",
	bottom: "1%"
};
const IconButton = styled("button", {
	display: "flex",
	padding: 0,
	border: "none",
	background: "transparent",
	cursor: "pointer",
	"& > svg": { pointerEvents: "none" },
	"&:hover > svg": {
		opacity: "100%",
		transform: "scale(1.1)"
	},
	"&:focus > svg": { opacity: "100%" }
});
const DownloadButton = (props) =>  (0, react_jsx_runtime.jsx)(IconButton, {
	...props,
	children:  (0, react_jsx_runtime.jsx)(Svg, {
		version: "1.1",
		xmlns: "http://www.w3.org/2000/svg",
		x: "0px",
		y: "0px",
		viewBox: "0 -34.51 122.88 122.87",
		css: downloadCss,
		children:  (0, react_jsx_runtime.jsx)("g", { children:  (0, react_jsx_runtime.jsx)("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 FullscreenButton = (props) =>  (0, react_jsx_runtime.jsx)(IconButton, {
	css: fullscreenCss,
	...props,
	children:  (0, react_jsx_runtime.jsx)(Svg, {
		version: "1.1",
		xmlns: "http://www.w3.org/2000/svg",
		x: "0px",
		y: "0px",
		viewBox: "0 0 122.88 122.87",
		width: "40px",
		children:  (0, react_jsx_runtime.jsx)("g", { children:  (0, react_jsx_runtime.jsx)("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  (0, react_jsx_runtime.jsx)(ErrorIcon, {
		x: "0px",
		y: "0px",
		viewBox: "0 0 122.881 122.88",
		"enable-background": "new 0 0 122.881 122.88",
		...props,
		children:  (0, react_jsx_runtime.jsx)("g", { children:  (0, react_jsx_runtime.jsx)("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 SettingsButton = (props) => {
	return  (0, react_jsx_runtime.jsx)(IconButton, {
		...props,
		children:  (0, react_jsx_runtime.jsxs)(Svg, {
			fill: "none",
			stroke: "currentColor",
			strokeLinecap: "round",
			strokeLinejoin: "round",
			strokeWidth: 2,
			viewBox: "0 0 24 24",
			height: "40px",
			width: "40px",
			children: [ (0, react_jsx_runtime.jsx)("path", { d: "M15 12 A3 3 0 0 1 12 15 A3 3 0 0 1 9 12 A3 3 0 0 1 15 12 z" }),  (0, react_jsx_runtime.jsx)("path", { d: "M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" })]
		})
	});
};
const RightArrow = (props) => {
	return  (0, react_jsx_runtime.jsx)(Svg, {
		viewBox: "0 0 330 330",
		...props,
		children:  (0, react_jsx_runtime.jsx)("path", { d: "M250.606,154.389l-150-149.996c-5.857-5.858-15.355-5.858-21.213,0.001\n	c-5.857,5.858-5.857,15.355,0.001,21.213l139.393,139.39L79.393,304.394c-5.857,5.858-5.857,15.355,0.001,21.213\n	C82.322,328.536,86.161,330,90,330s7.678-1.464,10.607-4.394l149.999-150.004c2.814-2.813,4.394-6.628,4.394-10.606\n	C255,161.018,253.42,157.202,250.606,154.389z" })
	});
};
const LeftArrow = (props) => {
	return  (0, react_jsx_runtime.jsx)(RightArrow, {
		...props,
		transform: "rotate(180)"
	});
};
const Container = styled("div", {
	position: "relative",
	height: "100%",
	overflow: "hidden",
	userSelect: "none",
	fontFamily: "Pretendard, NanumGothic, sans-serif",
	fontSize: "16px",
	color: "black",
	"& *:focus-visible": { outline: "none" },
	variants: { immersive: { true: {
		position: "fixed",
		top: 0,
		bottom: 0,
		left: 0,
		right: 0
	} } }
});
const OverlayScroller = styled("div", {
	position: "relative",
	width: "100%",
	height: "100%",
	"& .os-scrollbar": { zIndex: 1 },
	"& .os-scrollbar-handle": {
		backdropFilter: "brightness(0.5)",
		background: "none",
		border: "#fff8 1px solid"
	},
	variants: { fullscreen: { true: {
		position: "fixed",
		top: 0,
		bottom: 0,
		overflow: "auto"
	} } }
});
GM.getResourceText("overlayscrollbars-css").then(insertCss);
const Backdrop = styled("div", {
	position: "absolute",
	top: 0,
	left: 0,
	width: "100%",
	height: "100%",
	display: "flex",
	alignItems: "center",
	justifyContent: "center",
	background: "rgba(0, 0, 0, 0.5)",
	transition: "0.2s",
	variants: { isOpen: {
		true: {
			opacity: 1,
			pointerEvents: "auto"
		},
		false: {
			opacity: 0,
			pointerEvents: "none"
		}
	} }
});
const CenterDialog = styled("div", {
	minWidth: "20em",
	minHeight: "20em",
	transition: "0.2s",
	background: "white",
	padding: "20px",
	borderRadius: "10px",
	boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.2)"
});
function BackdropDialog({ onClose,...props }) {
	const [isOpen, setIsOpen] = (0, react.useState)(false);
	const close = async () => {
		setIsOpen(false);
		await timeout(200);
		onClose();
	};
	const closeIfEnter = (event) => {
		if (event.key === "Enter") {
			close();
			event.stopPropagation();
		}
	};
	(0, react.useEffect)(() => {
		setIsOpen(true);
	}, []);
	return  (0, react_jsx_runtime.jsx)(Backdrop, {
		isOpen,
		onClick: close,
		onKeyDown: closeIfEnter,
		children:  (0, react_jsx_runtime.jsx)(CenterDialog, {
			onClick: (event) => event.stopPropagation(),
			...props
		})
	});
}
const keyBindingsAtom = (0, jotai.atom)((get) => {
	const strings = get(i18nAtom);
	return [
		[strings.toggleViewer,  (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
			 (0, react_jsx_runtime.jsx)("kbd", { children: "i" }),
			", ",
			 (0, react_jsx_runtime.jsx)("kbd", { children: "Enter⏎" }),
			", ",
			 (0, react_jsx_runtime.jsx)("kbd", { children: "NumPad0" })
		] })],
		[strings.toggleFullscreenSetting,  (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
			 (0, react_jsx_runtime.jsx)("kbd", { children: "⇧Shift" }),
			"+(",
			 (0, react_jsx_runtime.jsx)("kbd", { children: "i" }),
			", ",
			 (0, react_jsx_runtime.jsx)("kbd", { children: "Enter⏎" }),
			", ",
			 (0, react_jsx_runtime.jsx)("kbd", { children: "NumPad0" }),
			")"
		] })],
		[strings.nextPage,  (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
			 (0, react_jsx_runtime.jsx)("kbd", { children: "j" }),
			", ",
			 (0, react_jsx_runtime.jsx)("kbd", { children: "↓" }),
			", ",
			 (0, react_jsx_runtime.jsx)("kbd", { children: "q" })
		] })],
		[strings.previousPage,  (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
			 (0, react_jsx_runtime.jsx)("kbd", { children: "k" }),
			", ",
			 (0, react_jsx_runtime.jsx)("kbd", { children: "↑" })
		] })],
		[strings.download,  (0, react_jsx_runtime.jsx)("kbd", { children: ";" })],
		[strings.refresh,  (0, react_jsx_runtime.jsx)("kbd", { children: "'" })],
		[strings.decreaseSinglePageCount,  (0, react_jsx_runtime.jsx)("kbd", { children: "," })],
		[strings.increaseSinglePageCount,  (0, react_jsx_runtime.jsx)("kbd", { children: "." })],
		[strings.anchorSinglePageCount,  (0, react_jsx_runtime.jsx)("kbd", { children: "/" })]
	];
});
const ActionName = styled("td", { paddingRight: "1em" });
function HelpTab() {
	const keyBindings$2 = (0, jotai.useAtomValue)(keyBindingsAtom);
	const strings = (0, jotai.useAtomValue)(i18nAtom);
	return  (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [ (0, react_jsx_runtime.jsx)("p", { children: strings.keyBindings }),  (0, react_jsx_runtime.jsx)("table", { children: keyBindings$2.map(([action, keyBinding]) =>  (0, react_jsx_runtime.jsxs)("tr", { children: [ (0, react_jsx_runtime.jsx)(ActionName, { children: action }),  (0, react_jsx_runtime.jsx)("td", { children: keyBinding })] }, action)) })] });
}
function SettingsTab() {
	const [maxZoomOutExponent, setMaxZoomOutExponent] = (0, jotai.useAtom)(maxZoomOutExponentAtom);
	const [maxZoomInExponent, setMaxZoomInExponent] = (0, jotai.useAtom)(maxZoomInExponentAtom);
	const [singlePageCount$2, setSinglePageCount] = (0, jotai.useAtom)(singlePageCountAtom);
	const [backgroundColor$2, setBackgroundColor] = (0, jotai.useAtom)(backgroundColorAtom);
	const [pageDirection, setPageDirection] = (0, jotai.useAtom)(pageDirectionAtom);
	const [isFullscreenPreferred, setIsFullscreenPreferred] = (0, jotai.useAtom)(isFullscreenPreferredSettingsAtom);
	const zoomOutExponentInputId = (0, react.useId)();
	const zoomInExponentInputId = (0, react.useId)();
	const singlePageCountInputId = (0, react.useId)();
	const colorInputId = (0, react.useId)();
	const pageDirectionInputId = (0, react.useId)();
	const fullscreenInputId = (0, react.useId)();
	const strings = (0, jotai.useAtomValue)(i18nAtom);
	const [isResetConfirming, setResetConfirming] = (0, react.useState)(false);
	const maxZoomOut$2 = formatMultiplier(maxZoomOutExponent);
	const maxZoomIn$2 = formatMultiplier(maxZoomInExponent);
	function tryReset() {
		if (!isResetConfirming) {
			setResetConfirming(true);
			return;
		}
		setMaxZoomInExponent(jotai_utils.RESET);
		setMaxZoomOutExponent(jotai_utils.RESET);
		setSinglePageCount(jotai_utils.RESET);
		setBackgroundColor(jotai_utils.RESET);
		setPageDirection(jotai_utils.RESET);
		setIsFullscreenPreferred(jotai_utils.RESET);
		setResetConfirming(false);
	}
	return  (0, react_jsx_runtime.jsxs)(ConfigSheet, { children: [
		 (0, react_jsx_runtime.jsxs)(ConfigRow, { children: [ (0, react_jsx_runtime.jsxs)(ConfigLabel, {
			htmlFor: zoomOutExponentInputId,
			children: [
				strings.maxZoomOut,
				": ",
				maxZoomOut$2
			]
		}),  (0, react_jsx_runtime.jsx)("input", {
			type: "number",
			min: 0,
			step: .1,
			id: zoomOutExponentInputId,
			value: maxZoomOutExponent,
			onChange: (event) => {
				setMaxZoomOutExponent(event.currentTarget.valueAsNumber || 0);
			}
		})] }),
		 (0, react_jsx_runtime.jsxs)(ConfigRow, { children: [ (0, react_jsx_runtime.jsxs)(ConfigLabel, {
			htmlFor: zoomInExponentInputId,
			children: [
				strings.maxZoomIn,
				": ",
				maxZoomIn$2
			]
		}),  (0, react_jsx_runtime.jsx)("input", {
			type: "number",
			min: 0,
			step: .1,
			id: zoomInExponentInputId,
			value: maxZoomInExponent,
			onChange: (event) => {
				setMaxZoomInExponent(event.currentTarget.valueAsNumber || 0);
			}
		})] }),
		 (0, react_jsx_runtime.jsxs)(ConfigRow, { children: [ (0, react_jsx_runtime.jsx)(ConfigLabel, {
			htmlFor: singlePageCountInputId,
			children: strings.singlePageCount
		}),  (0, react_jsx_runtime.jsx)("input", {
			type: "number",
			min: 0,
			step: 1,
			id: singlePageCountInputId,
			value: singlePageCount$2,
			onChange: (event) => {
				setSinglePageCount(event.currentTarget.valueAsNumber || 0);
			}
		})] }),
		 (0, react_jsx_runtime.jsxs)(ConfigRow, { children: [ (0, react_jsx_runtime.jsx)(ConfigLabel, {
			htmlFor: colorInputId,
			children: strings.backgroundColor
		}),  (0, react_jsx_runtime.jsx)(ColorInput, {
			type: "color",
			id: colorInputId,
			value: backgroundColor$2,
			onChange: (event) => {
				setBackgroundColor(event.currentTarget.value);
			}
		})] }),
		 (0, react_jsx_runtime.jsxs)(ConfigRow, { children: [ (0, react_jsx_runtime.jsx)("p", { children: strings.useFullScreen }),  (0, react_jsx_runtime.jsxs)(Toggle, { children: [ (0, react_jsx_runtime.jsx)(HiddenInput, {
			type: "checkbox",
			id: fullscreenInputId,
			checked: isFullscreenPreferred,
			onChange: (event) => {
				setIsFullscreenPreferred(event.currentTarget.checked);
			}
		}),  (0, react_jsx_runtime.jsx)("label", {
			htmlFor: fullscreenInputId,
			children: strings.useFullScreen
		})] })] }),
		 (0, react_jsx_runtime.jsxs)(ConfigRow, { children: [ (0, react_jsx_runtime.jsx)("p", { children: strings.leftToRight }),  (0, react_jsx_runtime.jsxs)(Toggle, { children: [ (0, react_jsx_runtime.jsx)(HiddenInput, {
			type: "checkbox",
			id: pageDirectionInputId,
			checked: pageDirection === "leftToRight",
			onChange: (event) => {
				setPageDirection(event.currentTarget.checked ? "leftToRight" : "rightToLeft");
			}
		}),  (0, react_jsx_runtime.jsx)("label", {
			htmlFor: pageDirectionInputId,
			children: strings.leftToRight
		})] })] }),
		 (0, react_jsx_runtime.jsx)(ResetButton, {
			onClick: tryReset,
			children: isResetConfirming ? strings.doYouReallyWantToReset : strings.reset
		})
	] });
}
function formatMultiplier(maxZoomOutExponent) {
	return Math.sqrt(2) ** maxZoomOutExponent === Infinity ? "∞" : `${(Math.sqrt(2) ** maxZoomOutExponent).toPrecision(2)}x`;
}
const ConfigLabel = styled("label", { margin: 0 });
const ResetButton = styled("button", {
	padding: "0.2em 0.5em",
	background: "none",
	border: "red 1px solid",
	borderRadius: "0.2em",
	color: "red",
	cursor: "pointer",
	transition: "0.3s",
	"&:hover": { background: "#ffe0e0" }
});
const ColorInput = styled("input", { height: "1.5em" });
const ConfigRow = styled("div", {
	display: "flex",
	alignItems: "center",
	justifyContent: "space-between",
	gap: "10%",
	"&& > *": {
		fontSize: "1em",
		fontWeight: "medium",
		minWidth: 0
	},
	"& > input": {
		appearance: "meter",
		border: "gray 1px solid",
		borderRadius: "0.2em",
		textAlign: "center"
	},
	":first-child": { flex: "2 1 0" },
	":nth-child(2)": { flex: "1 1 0" }
});
const HiddenInput = styled("input", {
	opacity: 0,
	width: 0,
	height: 0
});
const Toggle = styled("span", {
	"--width": "60px",
	"label": {
		position: "relative",
		display: "inline-flex",
		margin: 0,
		width: "var(--width)",
		height: "calc(var(--width) / 2)",
		borderRadius: "calc(var(--width) / 2)",
		cursor: "pointer",
		textIndent: "-9999px",
		background: "grey"
	},
	"label:after": {
		position: "absolute",
		top: "calc(var(--width) * 0.025)",
		left: "calc(var(--width) * 0.025)",
		width: "calc(var(--width) * 0.45)",
		height: "calc(var(--width) * 0.45)",
		borderRadius: "calc(var(--width) * 0.45)",
		content: "",
		background: "#fff",
		transition: "0.3s"
	},
	"input:checked + label": { background: "#bada55" },
	"input:checked + label:after": {
		left: "calc(var(--width) * 0.975)",
		transform: "translateX(-100%)"
	},
	"label:active:after": { width: "calc(var(--width) * 0.65)" }
});
const ConfigSheet = styled("div", {
	display: "flex",
	flexFlow: "column nowrap",
	alignItems: "stretch",
	gap: "0.8em"
});
function ViewerDialog({ onClose }) {
	const strings = (0, jotai.useAtomValue)(i18nAtom);
	return  (0, react_jsx_runtime.jsx)(BackdropDialog, {
		onClose,
		children:  (0, react_jsx_runtime.jsxs)(__headlessui_react.TabGroup, { children: [ (0, react_jsx_runtime.jsxs)(__headlessui_react.TabList, {
			as: StyledTabList,
			children: [ (0, react_jsx_runtime.jsx)(__headlessui_react.Tab, {
				as: PlainTab,
				children: strings.settings
			}),  (0, react_jsx_runtime.jsx)(__headlessui_react.Tab, {
				as: PlainTab,
				children: strings.help
			})]
		}),  (0, react_jsx_runtime.jsxs)(__headlessui_react.TabPanels, {
			as: StyledTabPanels,
			children: [ (0, react_jsx_runtime.jsx)(__headlessui_react.TabPanel, { children:  (0, react_jsx_runtime.jsx)(SettingsTab, {}) }),  (0, react_jsx_runtime.jsx)(__headlessui_react.TabPanel, { children:  (0, react_jsx_runtime.jsx)(HelpTab, {}) })]
		})] })
	});
}
const PlainTab = styled("button", {
	flex: 1,
	padding: "0.5em 1em",
	background: "transparent",
	border: "none",
	borderRadius: "0.5em",
	color: "#888",
	cursor: "pointer",
	fontSize: "1.2em",
	fontWeight: "bold",
	textAlign: "center",
	"&[data-headlessui-state=\"selected\"]": {
		border: "1px solid black",
		color: "black"
	},
	"&:hover": { color: "black" }
});
const StyledTabList = styled("div", {
	display: "flex",
	flexFlow: "row nowrap",
	gap: "0.5em"
});
const StyledTabPanels = styled("div", { marginTop: "1em" });
const LeftBottomFloat = styled("div", {
	position: "absolute",
	bottom: "1%",
	left: "1%",
	display: "flex",
	flexFlow: "column"
});
const MenuActions = styled("div", {
	display: "flex",
	flexFlow: "column nowrap",
	alignItems: "center",
	gap: "16px"
});
function LeftBottomControl() {
	const downloadAndSave = (0, jotai.useSetAtom)(downloadAndSaveAtom);
	const [isOpen, setIsOpen] = (0, react.useState)(false);
	const scrollable = (0, jotai.useAtomValue)(scrollElementAtom);
	const closeDialog = () => {
		setIsOpen(false);
		scrollable?.focus();
	};
	return  (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [ (0, react_jsx_runtime.jsx)(LeftBottomFloat, { children:  (0, react_jsx_runtime.jsxs)(MenuActions, { children: [ (0, react_jsx_runtime.jsx)(SettingsButton, { onClick: () => setIsOpen((value) => !value) }),  (0, react_jsx_runtime.jsx)(DownloadButton, { onClick: () => downloadAndSave() })] }) }), isOpen &&  (0, react_jsx_runtime.jsx)(ViewerDialog, { onClose: closeDialog })] });
}
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 = () =>  (0, react_jsx_runtime.jsxs)(SpinnerContainer, { children: [
	 (0, react_jsx_runtime.jsx)("div", {}),
	 (0, react_jsx_runtime.jsx)("div", {}),
	 (0, react_jsx_runtime.jsx)("div", {})
] });
const Overlay = styled("div", {
	position: "relative",
	maxWidth: "100%",
	height: "100vh",
	display: "flex",
	alignItems: "center",
	justifyContent: "center",
	"@media print": { margin: 0 },
	variants: { 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",
	lineBreak: "anywhere",
	"&:hover": { boxShadow: "2px 2px 5px" },
	"&:active": { boxShadow: "0 0 2px" }
});
const Image = styled("img", {
	position: "relative",
	height: "100%",
	maxWidth: "100%",
	objectFit: "contain",
	variants: { originalSize: { true: { height: "auto" } } }
});
const Video = styled("video", {
	position: "relative",
	height: "100%",
	maxWidth: "100%",
	objectFit: "contain",
	variants: { originalSize: { true: { height: "auto" } } }
});
const Page = ({ atom: atom$2,...props }) => {
	const { imageProps, videoProps, fullWidth, reloadAtom, shouldBeOriginalSize, divCss, state: pageState, setDiv } = (0, jotai.useAtomValue)(atom$2);
	const strings = (0, jotai.useAtomValue)(i18nAtom);
	const reload = (0, jotai.useSetAtom)(reloadAtom);
	const { status } = pageState;
	const reloadErrored = async (event) => {
		event.stopPropagation();
		await reload("load");
	};
	return  (0, react_jsx_runtime.jsxs)(Overlay, {
		ref: setDiv,
		css: divCss,
		fullWidth,
		children: [
			status === "loading" &&  (0, react_jsx_runtime.jsx)(Spinner, {}),
			status === "error" &&  (0, react_jsx_runtime.jsxs)(LinkColumn, {
				onClick: reloadErrored,
				children: [
					 (0, react_jsx_runtime.jsx)(CircledX, {}),
					 (0, react_jsx_runtime.jsx)("p", { children: strings.failedToLoadImage }),
					 (0, react_jsx_runtime.jsx)("p", { children: pageState.urls?.join("\n") })
				]
			}),
			videoProps &&  (0, react_jsx_runtime.jsx)(Video, {
				...videoProps,
				originalSize: shouldBeOriginalSize,
				...props
			}),
			imageProps &&  (0, react_jsx_runtime.jsx)(Image, {
				...imageProps,
				originalSize: shouldBeOriginalSize,
				...props
			})
		]
	});
};
function useHorizontalSwipe({ element, onPrevious, onNext }) {
	const [swipeRatio, setSwipeRatio] = (0, react.useState)(0);
	(0, react.useEffect)(() => {
		if (!element || !onPrevious && !onNext) return;
		let lastX = null;
		let lastRatio = 0;
		let startTouch = null;
		const addTouchIfClean = (event) => {
			const newTouch = event.touches[0];
			if (startTouch !== null || !newTouch) return;
			startTouch = {
				identifier: newTouch.identifier,
				x: newTouch.clientX,
				y: newTouch.clientY,
				scrollTop: element.scrollTop
			};
			lastX = newTouch.clientX;
		};
		const throttledSetSwipeRatio = throttle(setSwipeRatio, 1e3 / 60);
		const updateSwipeRatio = (event) => {
			const continuedTouch = [...event.changedTouches].find((touch) => touch.identifier === startTouch?.identifier);
			if (!continuedTouch || !startTouch || !lastX) return;
			const isVerticalScroll = element.scrollTop !== startTouch.scrollTop;
			if (isVerticalScroll) {
				resetTouch();
				return;
			}
			const ratioDelta = (continuedTouch.clientX - lastX) / 200;
			lastRatio = Math.max(-1, Math.min(lastRatio + ratioDelta, 1));
			throttledSetSwipeRatio(lastRatio);
			lastX = continuedTouch.clientX;
			const horizontalOffset = Math.abs(continuedTouch.clientX - startTouch.x);
			const verticalOffset = Math.abs(continuedTouch.clientY - startTouch.y);
			if (horizontalOffset > verticalOffset) {
				event.preventDefault();
			}
		};
		const resetSwipeRatioIfReleased = (event) => {
			const continuedTouch = [...event.touches].find((touch) => touch.identifier === startTouch?.identifier);
			if (continuedTouch) return;
			if (Math.abs(lastRatio) < .7) {
				resetTouch();
				return;
			}
			if (lastRatio > 0) {
				onPrevious?.();
			} else {
				onNext?.();
			}
			resetTouch();
		};
		function resetTouch() {
			startTouch = null;
			lastX = null;
			lastRatio = 0;
			throttledSetSwipeRatio(0);
			throttledSetSwipeRatio.flush();
		}
		element.addEventListener("touchend", resetSwipeRatioIfReleased);
		element.addEventListener("touchcancel", resetSwipeRatioIfReleased);
		element.addEventListener("touchmove", updateSwipeRatio, { passive: false });
		element.addEventListener("touchstart", addTouchIfClean, { passive: true });
		return () => {
			element.removeEventListener("touchstart", addTouchIfClean);
			element.removeEventListener("touchmove", updateSwipeRatio);
			element.removeEventListener("touchcancel", resetSwipeRatioIfReleased);
			element.removeEventListener("touchend", resetSwipeRatioIfReleased);
		};
	}, [element]);
	return swipeRatio;
}
const sideButtonCss = {
	position: "absolute",
	top: 0,
	bottom: "60px",
	width: "10%",
	height: "100%",
	border: "none",
	backgroundColor: "transparent",
	"& > *": { transition: "transform 0.2s ease-in-out" },
	variants: { touchDevice: { true: {
		transition: "unset",
		pointerEvents: "none"
	} } }
};
const LeftSideHiddenButton = styled("button", {
	...sideButtonCss,
	left: 0,
	"&:not(:hover) > *": { transform: "translateX(-60%)" },
	"&:hover > *, &:focus > *, &:focus-visible > *": { transform: "translateX(-20%)" }
});
const RightSideHiddenButton = styled("button", {
	...sideButtonCss,
	right: 0,
	"&:not(:hover) > *": { transform: "translateX(+60%)" },
	"&:hover > *, &:focus > *, &:focus-visible > *": { transform: "translateX(+20%)" }
});
const FlexCenter = styled("div", {
	display: "flex",
	justifyContent: "center",
	alignItems: "center",
	width: "100%",
	height: "100%"
});
function SideSeriesButtons() {
	const { onNextSeries, onPreviousSeries } = (0, jotai.useAtomValue)(viewerOptionsAtom);
	const scrollElement = (0, jotai.useAtomValue)(scrollElementAtom);
	const swipeRatio = useHorizontalSwipe({
		element: scrollElement,
		onPrevious: onPreviousSeries,
		onNext: onNextSeries
	});
	const isTouchDevice = navigator.maxTouchPoints > 0;
	function forwardWheelEvent(event) {
		scrollElement?.scrollBy({ top: event.deltaY });
	}
	return  (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [onPreviousSeries &&  (0, react_jsx_runtime.jsx)(LeftSideHiddenButton, {
		onClick: onPreviousSeries,
		onWheel: forwardWheelEvent,
		touchDevice: isTouchDevice,
		children:  (0, react_jsx_runtime.jsx)(FlexCenter, {
			style: swipeRatio <= 0 ? {} : { transform: `translateX(${swipeRatio * 40 - 60}%)` },
			children:  (0, react_jsx_runtime.jsx)(LeftArrow, {
				height: "3vmin",
				width: "3vmin"
			})
		})
	}), onNextSeries &&  (0, react_jsx_runtime.jsx)(RightSideHiddenButton, {
		onClick: onNextSeries,
		onWheel: forwardWheelEvent,
		touchDevice: isTouchDevice,
		children:  (0, react_jsx_runtime.jsx)(FlexCenter, {
			style: swipeRatio >= 0 ? {} : { transform: `translateX(${swipeRatio * 40 + 60}%)` },
			children:  (0, react_jsx_runtime.jsx)(RightArrow, {
				height: "3vmin",
				width: "3vmin"
			})
		})
	})] });
}
const Pages = styled("div", {
	display: "flex",
	justifyContent: "center",
	alignItems: "center",
	flexFlow: "row-reverse wrap",
	overflowY: "auto",
	variants: { ltr: { true: { flexFlow: "row wrap" } } }
});
const CenterText = styled("p", {
	position: "absolute",
	top: "50%",
	left: "50%",
	transform: "translate(-50%, -50%)",
	fontSize: "2em"
});
function InnerViewer(props) {
	const { options, onInitialized,...otherProps } = props;
	const isFullscreen = (0, jotai.useAtomValue)(viewerFullscreenAtom);
	const backgroundColor$2 = (0, jotai.useAtomValue)(backgroundColorAtom);
	const status = (0, jotai.useAtomValue)(viewerStatusAtom);
	const viewerOptions = (0, jotai.useAtomValue)(viewerOptionsAtom);
	const pageDirection = (0, jotai.useAtomValue)(pageDirectionAtom);
	const strings = (0, jotai.useAtomValue)(i18nAtom);
	const mode = (0, jotai.useAtomValue)(viewerModeAtom);
	const controller = (0, jotai.useAtomValue)(controllerAtom);
	const virtualContainerRef = (0, react.useRef)(null);
	const virtualContainer = virtualContainerRef.current;
	const setScrollElement = (0, jotai.useSetAtom)(setScrollElementAtom);
	const setViewerOptions = (0, jotai.useSetAtom)(setViewerOptionsAtom);
	const pageAtoms = (0, jotai.useAtomValue)(pageAtomsAtom);
	const [initialize$1] = (0, overlayscrollbars_react.useOverlayScrollbars)({
		defer: true,
		events: {
			scroll: (0, jotai.useSetAtom)(synchronizeScrollAtom),
			initialized: setupScroll
		}
	});
	(0, jotai.useAtomValue)(fullscreenSynchronizationAtom);
	useBeforeRepaint();
	async function setupScroll() {
		const selector = "div[data-overlayscrollbars-viewport]";
		await setScrollElement(virtualContainerRef.current?.querySelector(selector));
	}
	(0, react.useEffect)(() => {
		if (controller) {
			onInitialized?.(controller);
		}
	}, [controller, onInitialized]);
	(0, react.useEffect)(() => {
		setViewerOptions(options);
	}, [options]);
	(0, react.useEffect)(() => {
		if (virtualContainer) {
			initialize$1(virtualContainer);
		}
	}, [initialize$1, virtualContainer]);
	return  (0, react_jsx_runtime.jsxs)(Container, {
		ref: (0, jotai.useSetAtom)(setViewerElementAtom),
		css: { backgroundColor: backgroundColor$2 },
		immersive: mode === "window",
		children: [
			 (0, react_jsx_runtime.jsx)(OverlayScroller, {
				tabIndex: 0,
				ref: virtualContainerRef,
				fullscreen: isFullscreen,
				onClick: (0, jotai.useSetAtom)(navigateAtom),
				onMouseDown: (0, jotai.useSetAtom)(blockSelectionAtom),
				...otherProps,
				children:  (0, react_jsx_runtime.jsx)(Pages, {
					ltr: pageDirection === "leftToRight",
					children: pageAtoms.map((atom$2) =>  (0, react_jsx_runtime.jsx)(Page, {
						atom: atom$2,
						...viewerOptions.mediaProps
					}, `${atom$2}`))
				})
			}),
			 (0, react_jsx_runtime.jsx)(SideSeriesButtons, {}),
			status === "loading" &&  (0, react_jsx_runtime.jsx)(CenterText, { children: strings.loading }),
			status === "error" &&  (0, react_jsx_runtime.jsx)(CenterText, { children: strings.errorIsOccurred }),
			status === "complete" &&  (0, react_jsx_runtime.jsx)(LeftBottomControl, {}),
			 (0, react_jsx_runtime.jsx)(FullscreenButton, { onClick: (0, jotai.useSetAtom)(toggleImmersiveAtom) }),
			 (0, react_jsx_runtime.jsx)(react_toastify.ToastContainer, {})
		]
	});
}
function initialize(options) {
	const store = (0, jotai.createStore)();
	const root = (0, react_dom_client.createRoot)(getDefaultRoot());
	store.set(rootAtom, root);
	return new Promise((resolve) => root.render( (0, react_jsx_runtime.jsx)(jotai.Provider, {
		store,
		children:  (0, react_jsx_runtime.jsx)(InnerViewer, {
			options,
			onInitialized: resolve
		})
	})));
}
const Viewer = (0, react.forwardRef)(({ options, onInitialized }) => {
	const store = (0, react.useMemo)(jotai.createStore, []);
	return  (0, react_jsx_runtime.jsx)(jotai.Provider, {
		store,
		children:  (0, react_jsx_runtime.jsx)(InnerViewer, {
			options,
			onInitialized
		})
	});
});
function getDefaultRoot() {
	const div = document.createElement("div");
	div.setAttribute("style", "width: 0; height: 0; z-index: 9999999; position: fixed;");
	document.body.append(div);
	return div;
}
exports.Viewer = Viewer
exports.download = download
exports.initialize = initialize
Object.defineProperty(exports, 'utils', {
  enumerable: true,
  get: function () {
    return utils_exports;
  }
});