Sleazy Fork is available in English.

히토미 뷰어

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

질문, 리뷰하거나, 이 스크립트를 신고하세요.
  1. // ==UserScript==
  2. // @name 히토미 뷰어
  3. // @name:ko 히토미 뷰어
  4. // @name:en hitomi viewer
  5. // @description i,j,k 키를 눌러보세요
  6. // @description:ko i,j,k 키를 눌러보세요
  7. // @description:en press i to open
  8. // @version 250312193353
  9. // @match https://hitomi.la/*
  10. // @author nanikit
  11. // @namespace https://greasyfork.org/ko/users/713014-nanikit
  12. // @license MIT
  13. // @connect self
  14. // @grant GM.addValueChangeListener
  15. // @grant GM.getResourceText
  16. // @grant GM.getValue
  17. // @grant GM.openInTab
  18. // @grant GM.removeValueChangeListener
  19. // @grant GM.setValue
  20. // @grant GM.xmlHttpRequest
  21. // @grant unsafeWindow
  22. // @grant window.close
  23. // @require https://cdn.jsdelivr.net/npm/requirejs@2.3.6/require.js
  24. // @resource link:@headlessui/react https://cdn.jsdelivr.net/npm/@headlessui/react@2.1.8/dist/headlessui.prod.cjs
  25. // @resource link:@stitches/react https://cdn.jsdelivr.net/npm/@stitches/react@1.3.1-1/dist/index.cjs
  26. // @resource link:clsx https://cdn.jsdelivr.net/npm/clsx@2.1.1/dist/clsx.js
  27. // @resource link:fflate https://cdn.jsdelivr.net/npm/fflate@0.8.2/lib/browser.cjs
  28. // @resource link:jotai https://cdn.jsdelivr.net/npm/jotai@2.10.0/index.js
  29. // @resource link:jotai-cache https://cdn.jsdelivr.net/npm/jotai-cache@0.5.0/dist/cjs/atomWithCache.js
  30. // @resource link:jotai/react https://cdn.jsdelivr.net/npm/jotai@2.10.0/react.js
  31. // @resource link:jotai/react/utils https://cdn.jsdelivr.net/npm/jotai@2.10.0/react/utils.js
  32. // @resource link:jotai/utils https://cdn.jsdelivr.net/npm/jotai@2.10.0/utils.js
  33. // @resource link:jotai/vanilla https://cdn.jsdelivr.net/npm/jotai@2.10.0/vanilla.js
  34. // @resource link:jotai/vanilla/utils https://cdn.jsdelivr.net/npm/jotai@2.10.0/vanilla/utils.js
  35. // @resource link:overlayscrollbars https://cdn.jsdelivr.net/npm/overlayscrollbars@2.10.0/overlayscrollbars.cjs
  36. // @resource link:overlayscrollbars-react https://cdn.jsdelivr.net/npm/overlayscrollbars-react@0.5.6/overlayscrollbars-react.cjs.js
  37. // @resource link:react https://cdn.jsdelivr.net/npm/react@18.3.1/cjs/react.production.min.js
  38. // @resource link:react-dom https://cdn.jsdelivr.net/npm/react-dom@18.3.1/cjs/react-dom.production.min.js
  39. // @resource link:react-toastify https://cdn.jsdelivr.net/npm/react-toastify@10.0.5/dist/react-toastify.js
  40. // @resource link:scheduler https://cdn.jsdelivr.net/npm/scheduler@0.23.2/cjs/scheduler.production.min.js
  41. // @resource link:vcv-inject-node-env data:,unsafeWindow.process=%7Benv:%7BNODE_ENV:%22production%22%7D%7D
  42. // @resource link:vim_comic_viewer https://update.greasyfork.org/scripts/417893/1552356/vim%20comic%20viewer.js
  43. // @resource overlayscrollbars-css https://cdn.jsdelivr.net/npm/overlayscrollbars@2.10.0/styles/overlayscrollbars.min.css
  44. // @resource react-toastify-css https://cdn.jsdelivr.net/npm/react-toastify@10.0.5/dist/ReactToastify.css
  45. // ==/UserScript==
  46. "use strict";
  47.  
  48. define("main", (require, exports, module) => {
  49. var timeout = (millisecond) => new Promise((resolve) => setTimeout(resolve, millisecond));
  50. var insertCss = (css) => {
  51. const style = document.createElement("style");
  52. style.innerHTML = css;
  53. document.head.append(style);
  54. };
  55. var observeOnce = (element, options) => {
  56. return new Promise((resolve) => {
  57. const observer = new MutationObserver((...args) => {
  58. observer.disconnect();
  59. resolve(args);
  60. });
  61. observer.observe(element, options);
  62. });
  63. };
  64. var defaultFocusCss = `
  65. && {
  66. background: aliceblue;
  67. }`;
  68. var selectItem = (div) => {
  69. div.classList.add("key-nav-focus");
  70. const { left, top, width, height } = div.getBoundingClientRect();
  71. const centerX = left + width / 2;
  72. const centerY = top + height / 2;
  73. const x = centerX - innerWidth / 2;
  74. const y = centerY - innerHeight / 2;
  75. scrollBy(x, y);
  76. };
  77. var getFocusedItem = () => document.querySelector(".key-nav-focus") || void 0;
  78. function hookListPage(configuration) {
  79. const { navigatePage: navigatePage2, getItems: getItems2, enter: enter2, onKeyDown } = configuration;
  80. const navigateItem = (forward2) => {
  81. const items = getItems2();
  82. const focus = getFocusedItem();
  83. if (!focus) {
  84. if (items[0]) {
  85. selectItem(forward2 ? items[0] : items[items.length - 1]);
  86. }
  87. return;
  88. }
  89. const index = items.indexOf(focus);
  90. if (index === -1) {
  91. return;
  92. }
  93. focus.classList.remove("key-nav-focus");
  94. let next = index + (forward2 ? 1 : -1);
  95. next = Math.max(0, Math.min(next, items.length - 1));
  96. selectItem(items[next]);
  97. };
  98. const forward = (event) => {
  99. if (onKeyDown) {
  100. const focus = getFocusedItem();
  101. onKeyDown(event, focus);
  102. }
  103. };
  104. const handlePageKeypress = (event) => {
  105. switch (event.key) {
  106. case "h":
  107. navigatePage2(-1);
  108. break;
  109. case "l":
  110. navigatePage2(1);
  111. break;
  112. default: {
  113. forward(event);
  114. break;
  115. }
  116. }
  117. };
  118. const handleKeyPress = (event) => {
  119. if (event.target.tagName === "INPUT") {
  120. return;
  121. }
  122. switch (event.key.toLowerCase()) {
  123. case "j":
  124. navigateItem(true);
  125. break;
  126. case "k":
  127. navigateItem(false);
  128. break;
  129. case "i": {
  130. const item = getFocusedItem();
  131. if (item) {
  132. enter2(item);
  133. }
  134. break;
  135. }
  136. default:
  137. if (navigatePage2) {
  138. handlePageKeypress(event);
  139. } else {
  140. forward(event);
  141. }
  142. break;
  143. }
  144. };
  145. const insertFocusCss = () => {
  146. const content = configuration.focusCss || defaultFocusCss;
  147. insertCss(content.replace(/&/g, ".key-nav-focus"));
  148. };
  149. addEventListener("keypress", handleKeyPress);
  150. insertFocusCss();
  151. }
  152. function hookListPage2() {
  153. hookListPage({ enter, getItems, navigatePage });
  154. }
  155. async function enter(element) {
  156. const anchor = element.querySelector?.("a");
  157. const fileName = anchor?.href?.match?.(/\d+\.html/)?.[0];
  158. if (fileName) {
  159. await GM.openInTab(`${location.origin}/reader/${fileName}`);
  160. }
  161. }
  162. function getItems() {
  163. return [
  164. ...document.querySelectorAll(".gallery-content > div")
  165. ];
  166. }
  167. function navigatePage(offset) {
  168. const link = getOffsetUrl(offset);
  169. if (link) {
  170. location.href = link;
  171. }
  172. }
  173. function getOffsetUrl(offset) {
  174. const page = getPageList();
  175. if (!page) {
  176. return;
  177. }
  178. const { index, links } = page;
  179. return links[index + offset];
  180. }
  181. function getPageList(href) {
  182. const url = href ?? location.href;
  183. const lastItem = document.querySelector(".page-container li:last-child");
  184. if (!lastItem?.textContent) {
  185. return;
  186. }
  187. const lastPage = parseInt(lastItem.textContent);
  188. const currentPage = parseInt(url.match(/\d+$/)?.[0] ?? "1");
  189. const anchor = document.querySelectorAll(
  190. ".page-container li>a[href]"
  191. )[1];
  192. if (!anchor) {
  193. return { links: [url], index: 0 };
  194. }
  195. const prefix = anchor.href.replace(/\d+$/, "");
  196. const links = [];
  197. for (let i = 1; i <= lastPage; i++) {
  198. links.push(`${prefix}${i}`);
  199. }
  200. return { links, index: currentPage - 1 };
  201. }
  202. var import_vim_comic_viewer = require("vim_comic_viewer");
  203. var overrideCss = `
  204. .vim_comic_viewer > :first-child ::-webkit-scrollbar {
  205. width: 12px !important;
  206. }
  207. ::-webkit-scrollbar-thumb {
  208. background: #888;
  209. }
  210. `;
  211. async function hookReaderPage() {
  212. const urls = await getUrls();
  213. const controller = await (0, import_vim_comic_viewer.initialize)({ source: throttleComicSource(urls) });
  214. controller.container.parentElement.className = "vim_comic_viewer";
  215. insertCss(overrideCss);
  216. addEventListener("keypress", onReaderKey);
  217. }
  218. function onReaderKey(event) {
  219. switch (event.key) {
  220. case "o":
  221. close();
  222. break;
  223. }
  224. }
  225. async function waitUnsafeObject(name) {
  226. while (true) {
  227. const target = unsafeWindow[name];
  228. if (target) {
  229. if (typeof target == "function") {
  230. return target.bind(unsafeWindow);
  231. }
  232. return target;
  233. }
  234. await timeout(100);
  235. }
  236. }
  237. function throttleComicSource(urls) {
  238. const urlCacheKey = "viewer_cached_urls";
  239. const cachedUrls = JSON.parse(sessionStorage.getItem(urlCacheKey) ?? "[]");
  240. const currentSource = [
  241. ...urls.slice(0, 4),
  242. ...Array(Math.max(0, urls.length - 4)).fill(void 0)
  243. ];
  244. for (const [i, url] of urls.entries()) {
  245. if (cachedUrls.includes(url)) {
  246. currentSource[i] = url;
  247. }
  248. }
  249. const remainingIndices = [...Array(urls.length).keys()].slice(4);
  250. const resolvers = new Map();
  251. setInterval(() => {
  252. const index = remainingIndices.shift();
  253. if (index === void 0) {
  254. return;
  255. }
  256. currentSource[index] = urls[index];
  257. resolvers.get(index)?.resolve();
  258. resolvers.delete(index);
  259. cachedUrls.push(urls[index]);
  260. sessionStorage.setItem(urlCacheKey, JSON.stringify(cachedUrls));
  261. }, 500);
  262. return async ({ cause, page }) => {
  263. if (cause === "download") {
  264. return urls;
  265. }
  266. if (cause === "error" && page !== void 0) {
  267. currentSource[page] = void 0;
  268. remainingIndices.push(page);
  269. }
  270. if (!page || currentSource[page] !== void 0) {
  271. return currentSource;
  272. }
  273. await getResolver(page).promise;
  274. return currentSource;
  275. };
  276. function getResolver(page) {
  277. let resolver = resolvers.get(page);
  278. if (resolver) {
  279. return resolver;
  280. }
  281. resolver = Promise.withResolvers();
  282. resolvers.set(page, resolver);
  283. return resolver;
  284. }
  285. }
  286. async function getUrls() {
  287. const info = await waitUnsafeObject("galleryinfo");
  288. prependIdToTitle(info);
  289. const gg = await waitUnsafeObject("gg");
  290. const guardless = `${gg.m}`.slice(14, -2).replace(/return 4;/g, "");
  291. unsafeWindow.gg.m = Function(
  292. "g",
  293. guardless
  294. );
  295. const make_source_element = await waitUnsafeObject("make_source_element");
  296. exec(() => {
  297. const base2 = `${make_source_element}`.match(
  298. /url_from_url_from_hash\(.*?'(.*?)'\)/
  299. )[1];
  300. Object.assign(window, { base: base2 });
  301. });
  302. const base = unsafeWindow.base;
  303. const urlFromUrlFromHash = await waitUnsafeObject("url_from_url_from_hash");
  304. const urls = info.files.map(
  305. (file) => urlFromUrlFromHash(
  306. info.id,
  307. file,
  308. file.hasavif ? "avif" : file.haswebp ? "webp" : "jpg",
  309. void 0,
  310. base
  311. )
  312. );
  313. return urls;
  314. }
  315. async function prependIdToTitle(info) {
  316. const title = document.querySelector("title");
  317. for (let i = 0; i < 2; i++) {
  318. document.title = `${info.id} ${info.title}`;
  319. await observeOnce(title, { childList: true });
  320. }
  321. }
  322. function exec(fn) {
  323. const script = document.createElement("script");
  324. script.setAttribute("type", "application/javascript");
  325. script.textContent = "(" + fn + ")();";
  326. document.body.appendChild(script);
  327. document.body.removeChild(script);
  328. }
  329. async function initialize2() {
  330. const { pathname } = location;
  331. if (pathname.startsWith("/reader")) {
  332. await hookReaderPage();
  333. } else if (!/^\/(manga|doujinshi|cg)\//.test(pathname)) {
  334. await hookListPage2();
  335. }
  336. }
  337. initialize2();
  338.  
  339. });
  340.  
  341. define("tampermonkey_grants", function() { Object.assign(this.window, { GM, unsafeWindow }); });
  342. requirejs.config({ deps: ["tampermonkey_grants"] });
  343. load()
  344.  
  345. async function load() {
  346. const links = GM.info.script.resources.filter(x => x.name.startsWith("link:"));
  347. await Promise.all(links.map(async ({ name }) => {
  348. const script = await GM.getResourceText(name)
  349. define(name.replace("link:", ""), Function("require", "exports", "module", script))
  350. }));
  351. require(["main"], () => {}, console.error);
  352. }