히토미 뷰어

i,j,k 키를 눌러보세요

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           히토미 뷰어
// @name:ko        히토미 뷰어
// @name:en        hitomi viewer
// @description    i,j,k 키를 눌러보세요
// @description:ko i,j,k 키를 눌러보세요
// @description:en press i to open
// @version        250524154359
// @match          https://hitomi.la/*
// @author         nanikit
// @namespace      https://greasyfork.org/ko/users/713014-nanikit
// @license        MIT
// @connect        self
// @grant          GM.addValueChangeListener
// @grant          GM.getResourceText
// @grant          GM.getValue
// @grant          GM.openInTab
// @grant          GM.removeValueChangeListener
// @grant          GM.setValue
// @grant          GM.xmlHttpRequest
// @grant          unsafeWindow
// @grant          window.close
// @require        https://cdn.jsdelivr.net/npm/[email protected]/require.js
// @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-cache             https://cdn.jsdelivr.net/npm/[email protected]/dist/cjs/atomWithCache.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: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-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:react/jsx-runtime       https://cdn.jsdelivr.net/npm/[email protected]/cjs/react-jsx-runtime.production.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       link:vim_comic_viewer        https://update.greasyfork.org/scripts/417893/1595153/vim%20comic%20viewer.js
// @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";

define("main", (require, exports, module) => {
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 __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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
	value: mod,
	enumerable: true
}) : target, mod));
const vim_comic_viewer = __toESM(require("vim_comic_viewer"));
const timeout = (millisecond) => new Promise((resolve) => setTimeout(resolve, millisecond));
const insertCss = (css) => {
	const style = document.createElement("style");
	style.innerHTML = css;
	document.head.append(style);
};
const observeOnce = (element, options) => {
	return new Promise((resolve) => {
		const observer = new MutationObserver((...args) => {
			observer.disconnect();
			resolve(args);
		});
		observer.observe(element, options);
	});
};
const defaultFocusCss = `
&& {
  background: aliceblue;
}`;
const selectItem = (div) => {
	div.classList.add("key-nav-focus");
	const { left, top, width, height } = div.getBoundingClientRect();
	const centerX = left + width / 2;
	const centerY = top + height / 2;
	const x = centerX - innerWidth / 2;
	const y = centerY - innerHeight / 2;
	scrollBy(x, y);
};
const getFocusedItem = () => document.querySelector(".key-nav-focus") || void 0;
function hookListPage$1(configuration) {
	const { navigatePage: navigatePage$1, getItems: getItems$1, enter: enter$1, onKeyDown } = configuration;
	const navigateItem = (forward$1) => {
		const items = getItems$1();
		const focus = getFocusedItem();
		if (!focus) {
			if (items[0]) selectItem(forward$1 ? items[0] : items[items.length - 1]);
			return;
		}
		const index = items.indexOf(focus);
		if (index === -1) return;
		focus.classList.remove("key-nav-focus");
		let next = index + (forward$1 ? 1 : -1);
		next = Math.max(0, Math.min(next, items.length - 1));
		selectItem(items[next]);
	};
	const forward = (event) => {
		if (onKeyDown) {
			const focus = getFocusedItem();
			onKeyDown(event, focus);
		}
	};
	const handlePageKeypress = (event) => {
		switch (event.key) {
			case "h":
				navigatePage$1(-1);
				break;
			case "l":
				navigatePage$1(1);
				break;
			default: {
				forward(event);
				break;
			}
		}
	};
	const handleKeyPress = (event) => {
		if (event.target.tagName === "INPUT") return;
		switch (event.key.toLowerCase()) {
			case "j":
				navigateItem(true);
				break;
			case "k":
				navigateItem(false);
				break;
			case "i": {
				const item = getFocusedItem();
				if (item) enter$1(item);
				break;
			}
			default:
				if (navigatePage$1) handlePageKeypress(event);
				else forward(event);
				break;
		}
	};
	const insertFocusCss = () => {
		const content = configuration.focusCss || defaultFocusCss;
		insertCss(content.replace(/&/g, ".key-nav-focus"));
	};
	addEventListener("keypress", handleKeyPress);
	insertFocusCss();
}
function hookListPage() {
	hookListPage$1({
		enter,
		getItems,
		navigatePage
	});
}
async function enter(element) {
	const anchor = element.querySelector?.("a");
	const fileName = anchor?.href?.match?.(/\d+\.html/)?.[0];
	if (fileName) await GM.openInTab(`${location.origin}/reader/${fileName}`);
}
function getItems() {
	return [...document.querySelectorAll(".gallery-content > div")];
}
function navigatePage(offset) {
	const link = getOffsetUrl(offset);
	if (link) location.href = link;
}
function getOffsetUrl(offset) {
	const page = getPageList();
	if (!page) return;
	const { index, links } = page;
	return links[index + offset];
}
function getPageList(href) {
	const url = href ?? location.href;
	const lastItem = document.querySelector(".page-container li:last-child");
	if (!lastItem?.textContent) return;
	const lastPage = parseInt(lastItem.textContent);
	const currentPage = parseInt(url.match(/\d+$/)?.[0] ?? "1");
	const anchor = document.querySelectorAll(".page-container li>a[href]")[1];
	if (!anchor) return {
		links: [url],
		index: 0
	};
	const prefix = anchor.href.replace(/\d+$/, "");
	const links = [];
	for (let i = 1; i <= lastPage; i++) links.push(`${prefix}${i}`);
	return {
		links,
		index: currentPage - 1
	};
}
const overrideCss = `
.vim_comic_viewer > :first-child ::-webkit-scrollbar {
  width: 12px !important;
}
::-webkit-scrollbar-thumb {
  background: #888;
}
`;
async function hookReaderPage() {
	const urls = await getUrls();
	const controller = await (0, vim_comic_viewer.initialize)({ source: throttleComicSource(urls) });
	controller.container.parentElement.className = "vim_comic_viewer";
	insertCss(overrideCss);
	addEventListener("keypress", onReaderKey);
}
function onReaderKey(event) {
	switch (event.key) {
		case "o":
			close();
			break;
	}
}
async function waitUnsafeObject(name) {
	while (true) {
		const target = unsafeWindow[name];
		if (target) {
			if (typeof target == "function") return target.bind(unsafeWindow);
			return target;
		}
		await timeout(100);
	}
}
function throttleComicSource(urls) {
	const urlCacheKey = "viewer_cached_urls";
	const cachedUrls = JSON.parse(sessionStorage.getItem(urlCacheKey) ?? "[]");
	const currentSource = [...urls.slice(0, 4), ...Array(Math.max(0, urls.length - 4)).fill(void 0)];
	for (const [i, url] of urls.entries()) if (cachedUrls.includes(url)) currentSource[i] = url;
	const remainingIndices = [...Array(urls.length).keys()].slice(4);
	const resolvers = new Map();
	setInterval(() => {
		const index = remainingIndices.shift();
		if (index === void 0) return;
		currentSource[index] = urls[index];
		resolvers.get(index)?.resolve();
		resolvers.delete(index);
		cachedUrls.push(urls[index]);
		sessionStorage.setItem(urlCacheKey, JSON.stringify(cachedUrls));
	}, 500);
	return async ({ cause, page }) => {
		if (cause === "download") return urls;
		if (cause === "error" && page !== void 0) {
			currentSource[page] = void 0;
			remainingIndices.push(page);
		}
		if (!page || currentSource[page] !== void 0) return currentSource;
		await getResolver(page).promise;
		return currentSource;
	};
	function getResolver(page) {
		let resolver = resolvers.get(page);
		if (resolver) return resolver;
		resolver = Promise.withResolvers();
		resolvers.set(page, resolver);
		return resolver;
	}
}
async function getUrls() {
	const info = await waitUnsafeObject("galleryinfo");
	prependIdToTitle(info);
	const gg = await waitUnsafeObject("gg");
	const guardless = `${gg.m}`.slice(14, -2).replace(/return 4;/g, "");
	unsafeWindow.gg.m = Function("g", guardless);
	const make_source_element = await waitUnsafeObject("make_source_element");
	exec(() => {
		const base$1 = `${make_source_element}`.match(/url_from_url_from_hash\(.*?'(.*?)'\)/)[1];
		Object.assign(window, { base: base$1 });
	});
	const base = unsafeWindow.base;
	const urlFromUrlFromHash = await waitUnsafeObject("url_from_url_from_hash");
	const urls = info.files.map((file) => urlFromUrlFromHash(info.id, file, file.hasavif ? "avif" : file.haswebp ? "webp" : "jpg", void 0, base));
	return urls;
}
async function prependIdToTitle(info) {
	const title = document.querySelector("title");
	for (let i = 0; i < 2; i++) {
		document.title = `${info.id} ${info.title}`;
		await observeOnce(title, { childList: true });
	}
}
function exec(fn) {
	const script = document.createElement("script");
	script.setAttribute("type", "application/javascript");
	script.textContent = "(" + fn + ")();";
	document.body.appendChild(script);
	document.body.removeChild(script);
}
async function initialize() {
	const { pathname } = location;
	if (pathname.startsWith("/reader")) await hookReaderPage();
	else if (!/^\/(manga|doujinshi|cg)\//.test(pathname)) await hookListPage();
}
initialize();


});

define("tampermonkey_grants", function() { Object.assign(this.window, { GM, unsafeWindow }); });
requirejs.config({ deps: ["tampermonkey_grants"] });
load()

async function load() {
  const links = GM.info.script.resources.filter(x => x.name.startsWith("link:"));
  await Promise.all(links.map(async ({ name }) => {
    const script = await GM.getResourceText(name)
    define(name.replace("link:", ""), Function("require", "exports", "module", script))
  }));
  require(["main"], () => {}, console.error);
}