// ==UserScript==
// @name vim comic viewer
// @name:ko vim comic viewer
// @description Universal comic reader
// @description:ko 만화 뷰어 라이브러리
// @version 8.0.1
// @namespace https://greasyfork.org/en/users/713014-nanikit
// @exclude *
// @match http://unused-field.space/
// @author nanikit
// @license MIT
// @resource @stitches/react https://cdn.jsdelivr.net/npm/@stitches/[email protected]/dist/index.cjs
// @resource fflate https://cdn.jsdelivr.net/npm/[email protected]/lib/browser.cjs
// @resource object-assign https://cdn.jsdelivr.net/npm/[email protected]/index.js
// @resource react https://cdn.jsdelivr.net/npm/[email protected]/cjs/react.production.min.js
// @resource react-dom https://cdn.jsdelivr.net/npm/[email protected]/cjs/react-dom.production.min.js
// @resource scheduler https://cdn.jsdelivr.net/npm/[email protected]/cjs/scheduler.production.min.js
// ==/UserScript==
// deno-fmt-ignore-file
// deno-lint-ignore-file
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 (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[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));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/mod.tsx
var mod_exports = {};
__export(mod_exports, {
Viewer: () => Viewer,
download: () => download,
initialize: () => initialize,
setGmXhr: () => setGmXhr,
transformToBlobUrl: () => transformToBlobUrl,
types: () => types_exports,
utils: () => utils_exports
});
module.exports = __toCommonJS(mod_exports);
// src/react_shim.ts
var React = __toESM(require("react"));
// src/deps.ts
var deps_exports = {};
__export(deps_exports, {
Fragment: () => import_react.Fragment,
createRef: () => import_react.createRef,
forwardRef: () => import_react.forwardRef,
useCallback: () => import_react.useCallback,
useEffect: () => import_react.useEffect,
useImperativeHandle: () => import_react.useImperativeHandle,
useMemo: () => import_react.useMemo,
useReducer: () => import_react.useReducer,
useRef: () => import_react.useRef,
useState: () => import_react.useState
});
__reExport(deps_exports, require("@stitches/react"));
__reExport(deps_exports, require("fflate"));
var import_react = require("react");
__reExport(deps_exports, require("react-dom"));
// src/vendors/stitches.ts
var { styled, css, keyframes } = (0, deps_exports.createStitches)({});
// src/components/icons.tsx
var Svg = 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)"
}
});
var downloadCss = { left: "8px" };
var fullscreenCss = { right: "24px" };
var DownloadIcon = (props) => /* @__PURE__ */ React.createElement(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,
...props
}, /* @__PURE__ */ React.createElement("g", null, /* @__PURE__ */ React.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"
})));
var FullscreenIcon = (props) => /* @__PURE__ */ React.createElement(Svg, {
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.createElement("g", null, /* @__PURE__ */ React.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"
})));
var ErrorIcon = styled("svg", {
width: "10vmin",
height: "10vmin",
fill: "hsl(0, 50%, 20%)",
margin: "2rem"
});
var CircledX = (props) => {
return /* @__PURE__ */ React.createElement(ErrorIcon, {
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.createElement("g", null, /* @__PURE__ */ React.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"
})));
};
// src/components/scrollable_layout.ts
var defaultScrollbar = {
"scrollbarWidth": "initial",
"scrollbarColor": "initial",
"&::-webkit-scrollbar": { all: "initial" },
"&::-webkit-scrollbar-thumb": { all: "initial", background: "gray" },
"&::-webkit-scrollbar-track": { all: "initial" }
};
var Container = styled("div", {
position: "relative",
height: "100%",
...defaultScrollbar
});
var ScrollableLayout = styled("div", {
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"
}
}
}
});
// src/utils.ts
var utils_exports = {};
__export(utils_exports, {
defer: () => defer,
getSafeFileName: () => getSafeFileName,
insertCss: () => insertCss,
isTyping: () => isTyping,
save: () => save,
saveAs: () => saveAs,
timeout: () => timeout,
waitDomContent: () => waitDomContent
});
var timeout = (millisecond) => new Promise((resolve) => setTimeout(resolve, millisecond));
var waitDomContent = (document2) => document2.readyState === "loading" ? new Promise((r) => document2.addEventListener("readystatechange", r, { once: true })) : true;
var insertCss = (css2) => {
const style = document.createElement("style");
style.innerHTML = css2;
document.head.append(style);
};
var isTyping = (event) => {
var _a, _b, _c, _d;
return ((_c = (_b = (_a = event.target) == null ? void 0 : _a.tagName) == null ? void 0 : _b.match) == null ? void 0 : _c.call(_b, /INPUT|TEXTAREA/)) || ((_d = event.target) == null ? void 0 : _d.isContentEditable);
};
var 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);
};
var getSafeFileName = (str) => {
return str.replace(/[<>:"/\\|?*\x00-\x1f]+/gi, "").trim() || "download";
};
var save = (blob) => {
return saveAs(blob, `${getSafeFileName(document.title)}.zip`);
};
var defer = () => {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
// src/hooks/use_default.ts
var maybeNotHotkey = (event) => event.ctrlKey || event.altKey || isTyping(event);
var useDefault = ({ enable, controller }) => {
const defaultKeyHandler = async (event) => {
var _a;
if (maybeNotHotkey(event)) {
return;
}
switch (event.key) {
case "j":
case "ArrowDown":
controller.goNext();
break;
case "k":
case "ArrowUp":
controller.goPrevious();
break;
case ";":
await ((_a = controller.downloader) == null ? void 0 : _a.downloadWithProgress());
break;
case "/":
controller.compactWidthIndex++;
break;
case "?":
controller.compactWidthIndex--;
break;
case "'":
controller.reloadErrored();
break;
default:
break;
}
};
const defaultGlobalKeyHandler = (event) => {
if (maybeNotHotkey(event)) {
return;
}
if (event.key === "i") {
controller.toggleFullscreen();
}
};
(0, import_react.useEffect)(() => {
if (!controller || !enable) {
return;
}
controller.container.addEventListener("keydown", defaultKeyHandler);
addEventListener("keydown", defaultGlobalKeyHandler);
return () => {
controller.container.removeEventListener("keydown", defaultKeyHandler);
removeEventListener("keydown", defaultGlobalKeyHandler);
};
}, [controller, enable]);
};
// src/hooks/use_fullscreen_element.ts
var useFullscreenElement = () => {
const [element, setElement] = (0, import_react.useState)(document.fullscreenElement || void 0);
(0, import_react.useEffect)(() => {
const notify = () => setElement(document.fullscreenElement || void 0);
document.addEventListener("fullscreenchange", notify);
return () => document.removeEventListener("fullscreenchange", notify);
}, []);
return element;
};
// src/services/tampermonkey.ts
var setGmXhr = (xmlhttpRequest) => {
gmXhr = xmlhttpRequest;
};
var gmXhr;
// src/services/gm_fetch.ts
var gmFetch = (resource, init) => {
const method = (init == null ? void 0 : init.body) ? "POST" : "GET";
const xhr = (type) => {
return new Promise((resolve, reject) => {
var _a;
const request = gmXhr({
method,
url: resource,
headers: init == null ? void 0 : init.headers,
responseType: type === "text" ? void 0 : type,
data: init == null ? void 0 : init.body,
onload: (response) => {
if (type === "text") {
resolve(response.responseText);
} else {
resolve(response.response);
}
},
onerror: reject,
onabort: reject
});
(_a = init == null ? void 0 : init.signal) == null ? void 0 : _a.addEventListener("abort", () => {
request.abort();
}, { once: true });
});
};
return {
blob: () => xhr("blob"),
json: () => xhr("json"),
text: () => xhr("text")
};
};
var fetchBlob = async (url, init) => {
var _a;
try {
const response = await fetch(url, init);
return await response.blob();
} catch (error) {
if ((_a = init == null ? void 0 : init.signal) == null ? void 0 : _a.aborted) {
throw error;
}
const isOriginDifferent = new URL(url).origin !== location.origin;
if (isOriginDifferent && gmXhr) {
return await gmFetch(url, init).blob();
} else {
throw new Error("CORS blocked and cannot use GM_xmlhttpRequest", {
cause: error
});
}
}
};
// src/services/user_utils.ts
var 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();
}
};
var 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);
}
}
});
};
// src/services/downloader.ts
var isGmCancelled = (error) => {
return error instanceof Function;
};
async function* downloadImage({ source, signal }) {
for await (const url of imageSourceToIterable(source)) {
if (signal == null ? void 0 : 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 };
}
}
}
}
var getExtension = (url) => {
if (!url) {
return ".txt";
}
const extension = url.match(/\.[^/?#]{3,4}?(?=[?#]|$)/);
return (extension == null ? void 0 : extension[0]) || ".jpg";
};
var 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";
}
};
var download = (images, options) => {
const { onError, onProgress, signal } = options || {};
let startedCount = 0;
let resolvedCount = 0;
let rejectedCount = 0;
let hasCancelled = false;
const reportProgress = ({ isCancelled, isComplete } = {}) => {
if (hasCancelled) {
return;
}
if (isCancelled) {
hasCancelled = true;
}
const total = images.length;
const settled = resolvedCount + rejectedCount;
onProgress == null ? void 0 : onProgress({
total,
started: startedCount,
settled,
rejected: rejectedCount,
isCancelled: hasCancelled,
isComplete
});
};
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 == null ? void 0 : 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) => {
var _a;
const array = new Uint8Array(await blob.arrayBuffer());
const pad = `${index}`.padStart(cipher, "0");
const name = `${pad}${(_a = guessExtension(array)) != null ? _a : getExtension(url)}`;
return { [name]: array };
};
const archiveWithReport = async (sources) => {
const result = await Promise.all(sources.map(downloadWithReport));
if (signal == null ? void 0 : 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 = (0, deps_exports.zip)(data, { level: 0 }, (error, array) => {
if (error) {
value.reject(error);
} else {
reportProgress({ isComplete: true });
value.resolve(array);
}
});
signal == null ? void 0 : signal.addEventListener("abort", abort, { once: true });
return value.promise;
};
return archiveWithReport(images);
};
// src/hooks/use_rerender.ts
var useRerender = () => {
const [, rerender] = (0, import_react.useReducer)(() => ({}), {});
return rerender;
};
// src/hooks/make_downloader.ts
var isNotAbort = (error) => !/aborted/i.test(`${error}`);
var logIfNotAborted = (error) => {
if (isNotAbort(error)) {
console.error(error);
}
};
var makeDownloader = (images) => {
let aborter = new AbortController();
let rerender;
let progress = {
value: 0,
text: "",
error: false
};
const startDownload = (options) => {
aborter = new AbortController();
return download(images, { ...options, signal: aborter.signal });
};
const downloadAndSave = async (options) => {
const zip2 = await startDownload(options);
if (zip2) {
await save(new Blob([zip2]));
}
};
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 == null ? void 0 : rerender();
} else if (text !== progress.text) {
progress = { value, text, error };
rerender == null ? void 0 : rerender();
}
};
const downloadWithProgress = async () => {
try {
await downloadAndSave({
onProgress: reportProgress,
onError: logIfNotAborted
});
} catch (error) {
if (isNotAbort(error)) {
throw error;
}
}
};
const guard = (event) => {
event.preventDefault();
};
const useInstance = () => {
const { error, text } = progress;
rerender = useRerender();
(0, import_react.useEffect)(() => {
if (error || !text) {
return;
}
addEventListener("beforeunload", guard);
return () => removeEventListener("beforeunload", guard);
}, [error || !text]);
};
return {
get progress() {
return progress;
},
download: startDownload,
downloadAndSave,
downloadWithProgress,
cancelDownload: () => aborter.abort(),
useInstance
};
};
// src/hooks/make_page_controller.ts
var 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 == null ? void 0 : setState({ src: url, state: "loading" });
const success = await imageLoad.promise;
if (success) {
setState == null ? void 0 : setState({ src: url, state: "complete" });
return;
}
if (isReloaded) {
isReloaded = false;
return;
}
}
setState == null ? void 0 : setState({ urls, state: "error" });
};
const useInstance = ({ ref }) => {
[state, setState] = (0, import_react.useState)({ src: "", state: "loading" });
(0, import_react.useEffect)(() => {
load();
}, []);
(0, import_react.useEffect)(() => {
const target = ref == null ? void 0 : 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
};
};
// src/hooks/use_intersection.ts
var useIntersectionObserver = (callback, options) => {
const [observer, setObserver] = (0, import_react.useState)();
(0, import_react.useEffect)(() => {
const newObserver = new IntersectionObserver(callback, options);
setObserver(newObserver);
return () => newObserver.disconnect();
}, [callback, options]);
return observer;
};
var useIntersection = (callback, options) => {
const memo = (0, import_react.useRef)(/* @__PURE__ */ new Map());
const filterIntersections = (0, import_react.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);
};
// src/hooks/use_page_navigator.ts
var useResize = (target, transformer) => {
const [value, setValue] = (0, import_react.useState)(() => transformer(void 0));
const callbackRef = (0, import_react.useRef)(transformer);
callbackRef.current = transformer;
(0, import_react.useEffect)(() => {
if (!target) {
return;
}
const observer = new ResizeObserver((entries) => {
setValue(callbackRef.current(entries[0]));
});
observer.observe(target);
return () => observer.disconnect();
}, [target, callbackRef]);
return value;
};
var getCurrentPage = (container, entries) => {
if (!entries.length) {
return container.firstElementChild || void 0;
}
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) * 1e4 + (index.a - index.b);
})[0].target;
};
var makePageNavigator = (ref) => {
let currentPage;
let ratio;
let ignoreIntersection = false;
const resetAnchor = (entries) => {
const container = ref.current;
if (!(container == null ? void 0 : 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 === void 0 || currentPage === void 0) {
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
};
};
var usePageNavigator = (ref) => {
const navigator = (0, import_react.useMemo)(() => makePageNavigator(ref), [ref]);
navigator.useInstance();
return navigator;
};
// src/hooks/use_viewer_controller.ts
var makeViewerController = ({ ref, navigator, rerender }) => {
let options = {};
let images = [];
let status = "loading";
let compactWidthIndex = 1;
let downloader;
let pages = [];
const toggleFullscreen = () => {
var _a;
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
(_a = ref.current) == null ? void 0 : _a.requestFullscreen();
}
};
const loadImages = async (source) => {
try {
[images, downloader] = [[], void 0];
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 = () => {
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() {
var _a;
return (_a = downloader == null ? void 0 : downloader.download) != null ? _a : () => 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: () => (0, deps_exports.unmountComponentAtNode)(ref.current)
};
};
var useViewerController = ({ ref, scrollRef }) => {
const rerender = useRerender();
const navigator = usePageNavigator(scrollRef);
const controller = (0, import_react.useMemo)(() => makeViewerController({ ref, navigator, rerender }), [ref, navigator]);
return controller;
};
// src/components/circular_progress.tsx
var Svg2 = styled("svg", {
position: "absolute",
bottom: "8px",
left: "8px",
cursor: "pointer",
"&:hover": {
filter: "hue-rotate(-145deg)"
},
variants: {
error: {
true: {
filter: "hue-rotate(140deg)"
}
}
}
});
var Circle = styled("circle", {
transform: "rotate(-90deg)",
transformOrigin: "50% 50%",
stroke: "url(#aEObn)",
fill: "#fff8"
});
var GradientDef = /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("linearGradient", {
id: "aEObn",
x1: "100%",
y1: "0%",
x2: "0%",
y2: "100%"
}, /* @__PURE__ */ React.createElement("stop", {
offset: "0%",
style: { stopColor: "#53baff", stopOpacity: 1 }
}), /* @__PURE__ */ React.createElement("stop", {
offset: "100%",
style: { stopColor: "#0067bb", stopOpacity: 1 }
})));
var CenterText = styled("text", {
dominantBaseline: "middle",
textAnchor: "middle",
fontSize: "30px",
fontWeight: "bold",
fill: "#004b9e"
});
var 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.createElement(Svg2, {
height: side,
width: side,
...otherProps
}, GradientDef, /* @__PURE__ */ React.createElement(Circle, {
...{
strokeWidth,
strokeDasharray: `${circumference} ${circumference}`,
strokeDashoffset,
r: radius,
cx: center,
cy: center
}
}), /* @__PURE__ */ React.createElement(CenterText, {
x: "50%",
y: "50%"
}, text || ""));
};
// src/containers/download_indicator.tsx
var DownloadIndicator = ({ downloader }) => {
var _a;
const { value, text, error } = (_a = downloader.progress) != null ? _a : {};
downloader.useInstance();
return /* @__PURE__ */ React.createElement(React.Fragment, null, text ? /* @__PURE__ */ React.createElement(CircularProgress, {
radius: 50,
strokeWidth: 10,
value: value != null ? value : 0,
text,
error,
onClick: downloader.cancelDownload
}) : /* @__PURE__ */ React.createElement(DownloadIcon, {
onClick: downloader.downloadWithProgress
}));
};
// src/components/spinner.tsx
var stretch = keyframes({
"0%": {
top: "8px",
height: "64px"
},
"50%": {
top: "24px",
height: "32px"
},
"100%": {
top: "24px",
height: "32px"
}
});
var 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"
}
});
var Spinner = () => /* @__PURE__ */ React.createElement(SpinnerContainer, null, /* @__PURE__ */ React.createElement("div", null), /* @__PURE__ */ React.createElement("div", null), /* @__PURE__ */ React.createElement("div", null));
var 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%" }
}
}
});
var 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"
}
});
var Image = styled("img", {
position: "relative",
height: "100%",
objectFit: "contain",
maxWidth: "100%"
});
// src/containers/page.tsx
var Page = ({
fullWidth,
controller,
...props
}) => {
const ref = (0, import_react.useRef)(null);
const imageProps = controller.useInstance({ ref });
const { state, src, urls } = controller.state;
const reloadErrored = (0, import_react.useCallback)(async (event) => {
event.stopPropagation();
await controller.reload();
}, []);
return /* @__PURE__ */ React.createElement(Overlay, {
ref,
placeholder: state !== "complete",
fullWidth
}, state === "loading" && /* @__PURE__ */ React.createElement(Spinner, null), state === "error" && /* @__PURE__ */ React.createElement(LinkColumn, {
onClick: reloadErrored
}, /* @__PURE__ */ React.createElement(CircledX, null), /* @__PURE__ */ React.createElement("p", null, "이미지를 불러오지 못했습니다"), /* @__PURE__ */ React.createElement("p", null, src ? src : urls == null ? void 0 : urls.join("\n"))), /* @__PURE__ */ React.createElement(Image, {
...imageProps,
...props
}));
};
// src/containers/viewer.tsx
var Viewer = (0, import_react.forwardRef)((props, refHandle) => {
const { useDefault: enableDefault, options: viewerOptions, ...otherProps } = props;
const ref = (0, import_react.useRef)();
const scrollRef = (0, import_react.useRef)();
const fullscreenElement = useFullscreenElement();
const controller = useViewerController({ ref, scrollRef });
const {
options,
pages,
status,
downloader,
toggleFullscreen,
compactWidthIndex
} = controller;
const navigate = (0, import_react.useCallback)((event) => {
var _a;
const height = (_a = ref.current) == null ? void 0 : _a.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 = (0, import_react.useCallback)((event) => {
if (event.detail >= 2) {
event.preventDefault();
}
if (event.buttons === 3) {
controller.toggleFullscreen();
event.preventDefault();
}
}, [controller]);
useDefault({ enable: props.useDefault, controller });
(0, import_react.useImperativeHandle)(refHandle, () => controller, [controller]);
(0, import_react.useEffect)(() => {
controller.setOptions(viewerOptions);
}, [controller, viewerOptions]);
(0, import_react.useEffect)(() => {
var _a, _b;
if (ref.current && fullscreenElement === ref.current) {
(_b = (_a = ref.current) == null ? void 0 : _a.focus) == null ? void 0 : _b.call(_a);
}
}, [ref.current, fullscreenElement]);
return /* @__PURE__ */ React.createElement(Container, {
ref,
tabIndex: -1,
className: "vim_comic_viewer"
}, /* @__PURE__ */ React.createElement(ScrollableLayout, {
ref: scrollRef,
fullscreen: fullscreenElement === ref.current,
onClick: navigate,
onMouseDown: blockSelection,
children: status === "complete" ? pages.map((controller2, index) => /* @__PURE__ */ React.createElement(Page, {
key: index,
controller: controller2,
fullWidth: index < compactWidthIndex,
...options == null ? void 0 : options.imageProps
})) : /* @__PURE__ */ React.createElement("p", null, status === "error" ? "에러가 발생했습니다" : "로딩 중..."),
...otherProps
}), /* @__PURE__ */ React.createElement(FullscreenIcon, {
onClick: toggleFullscreen
}), downloader ? /* @__PURE__ */ React.createElement(DownloadIndicator, {
downloader
}) : false);
});
// src/types.ts
var types_exports = {};
// src/mod.tsx
var getDefaultRoot = () => {
const div = document.createElement("div");
div.setAttribute("style", "width: 0; height: 0; position: fixed;");
document.body.append(div);
return div;
};
var initialize = (options) => {
const ref = (0, import_react.createRef)();
(0, deps_exports.render)(/* @__PURE__ */ React.createElement(Viewer, {
ref,
options,
useDefault: true
}), getDefaultRoot());
return Promise.resolve(ref.current);
};