您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
i,j,k 키를 눌러보세요
// ==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); }