토끼 뷰어

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

  1. // ==UserScript==
  2. // @name 토끼 뷰어
  3. // @name:ko 토끼 뷰어
  4. // @name:en toki viewer
  5. // @description i,j,k 키를 눌러보세요
  6. // @description:ko i,j,k 키를 눌러보세요
  7. // @description:en press i to open
  8. // @version 250406112833
  9. // @match https://*.net/bbs/*
  10. // @match https://*.net/comic/*
  11. // @match https://*.com/webtoon/*
  12. // @match https://*.com/novel/*
  13. // @author nanikit
  14. // @namespace https://greasyfork.org/ko/users/713014-nanikit
  15. // @license MIT
  16. // @connect *
  17. // @grant GM.addValueChangeListener
  18. // @grant GM.getResourceText
  19. // @grant GM.getValue
  20. // @grant GM.removeValueChangeListener
  21. // @grant GM.setValue
  22. // @grant GM.xmlHttpRequest
  23. // @grant unsafeWindow
  24. // @require https://cdn.jsdelivr.net/npm/requirejs@2.3.6/require.js
  25. // @resource link:@headlessui/react https://cdn.jsdelivr.net/npm/@headlessui/react@2.2.1/dist/headlessui.prod.cjs
  26. // @resource link:@stitches/react https://cdn.jsdelivr.net/npm/@stitches/react@1.3.1-1/dist/index.cjs
  27. // @resource link:clsx https://cdn.jsdelivr.net/npm/clsx@2.1.1/dist/clsx.js
  28. // @resource link:fflate https://cdn.jsdelivr.net/npm/fflate@0.8.2/lib/browser.cjs
  29. // @resource link:jotai https://cdn.jsdelivr.net/npm/jotai@2.10.0/index.js
  30. // @resource link:jotai-cache https://cdn.jsdelivr.net/npm/jotai-cache@0.5.0/dist/cjs/atomWithCache.js
  31. // @resource link:jotai/react https://cdn.jsdelivr.net/npm/jotai@2.10.0/react.js
  32. // @resource link:jotai/react/utils https://cdn.jsdelivr.net/npm/jotai@2.10.0/react/utils.js
  33. // @resource link:jotai/utils https://cdn.jsdelivr.net/npm/jotai@2.10.0/utils.js
  34. // @resource link:jotai/vanilla https://cdn.jsdelivr.net/npm/jotai@2.10.0/vanilla.js
  35. // @resource link:jotai/vanilla/utils https://cdn.jsdelivr.net/npm/jotai@2.10.0/vanilla/utils.js
  36. // @resource link:overlayscrollbars https://cdn.jsdelivr.net/npm/overlayscrollbars@2.10.0/overlayscrollbars.cjs
  37. // @resource link:overlayscrollbars-react https://cdn.jsdelivr.net/npm/overlayscrollbars-react@0.5.6/overlayscrollbars-react.cjs.js
  38. // @resource link:react https://cdn.jsdelivr.net/npm/react@19.0.0/cjs/react.production.js
  39. // @resource link:react-dom https://cdn.jsdelivr.net/npm/react-dom@19.0.0/cjs/react-dom.production.js
  40. // @resource link:react-dom/client https://cdn.jsdelivr.net/npm/react-dom@19.0.0/cjs/react-dom-client.production.js
  41. // @resource link:react-toastify https://cdn.jsdelivr.net/npm/react-toastify@10.0.5/dist/react-toastify.js
  42. // @resource link:react/jsx-runtime https://cdn.jsdelivr.net/npm/react@19.0.0/cjs/react-jsx-runtime.production.js
  43. // @resource link:scheduler https://cdn.jsdelivr.net/npm/scheduler@0.23.2/cjs/scheduler.production.min.js
  44. // @resource link:vcv-inject-node-env data:,unsafeWindow.process=%7Benv:%7BNODE_ENV:%22production%22%7D%7D
  45. // @resource link:vim_comic_viewer https://update.greasyfork.org/scripts/417893/1566357/vim%20comic%20viewer.js
  46. // @resource overlayscrollbars-css https://cdn.jsdelivr.net/npm/overlayscrollbars@2.10.0/styles/overlayscrollbars.min.css
  47. // @resource react-toastify-css https://cdn.jsdelivr.net/npm/react-toastify@10.0.5/dist/ReactToastify.css
  48. // ==/UserScript==
  49. "use strict";
  50.  
  51. define("main", (require, exports, module) => {
  52. var __create = Object.create;
  53. var __defProp = Object.defineProperty;
  54. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  55. var __getOwnPropNames = Object.getOwnPropertyNames;
  56. var __getProtoOf = Object.getPrototypeOf;
  57. var __hasOwnProp = Object.prototype.hasOwnProperty;
  58. var __copyProps = (to, from, except, desc) => {
  59. if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
  60. key = keys[i];
  61. if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
  62. get: ((k) => from[k]).bind(null, key),
  63. enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
  64. });
  65. }
  66. return to;
  67. };
  68. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
  69. value: mod,
  70. enumerable: true
  71. }) : target, mod));
  72. const vim_comic_viewer = __toESM(require("vim_comic_viewer"));
  73. async function main() {
  74. const origin = getOrigin();
  75. if (origin === "unknown") return;
  76. markVisitedLinks();
  77. registerEpisodeNavigator();
  78. const buttons = duplicateViewerButton();
  79. const source = await comicSource();
  80. const controller = await (0, vim_comic_viewer.initialize)({
  81. source: () => source,
  82. onPreviousSeries: goPreviousEpisode,
  83. onNextSeries: goNextEpisode
  84. });
  85. controller.setScriptPreferences({
  86. manualPreset: origin,
  87. preferences: { pageDirection: origin === "manatoki" ? "rightToLeft" : "leftToRight" }
  88. });
  89. for (const button of buttons) button.addEventListener("click", async () => {
  90. await controller.setImmersive(true);
  91. });
  92. }
  93. function getOrigin() {
  94. const allowedOrigins = [
  95. "manatoki",
  96. "newtoki",
  97. "booktoki"
  98. ];
  99. return allowedOrigins.find(originIncludes) ?? "unknown";
  100. }
  101. function originIncludes(str) {
  102. return location.origin.includes(str);
  103. }
  104. function duplicateViewerButton() {
  105. const template = document.createElement("template");
  106. template.innerHTML = `<a class="show_viewer" alt="뷰어로 보기">
  107. <i class="ion-ios-book at-tip" aria-hidden="true" style="color: blue;"></i>
  108. </a>`;
  109. const templateButton = template.content.firstElementChild;
  110. const buttons = [];
  111. const divs = document.querySelectorAll(".toon-nav");
  112. for (const div of divs) {
  113. const button = templateButton.cloneNode(true);
  114. div.prepend(button);
  115. buttons.push(button);
  116. }
  117. return buttons;
  118. }
  119. async function comicSource() {
  120. while (true) {
  121. const urls = getUrls();
  122. if (urls.length) return urls;
  123. await vim_comic_viewer.utils.timeout(200);
  124. }
  125. }
  126. function goPreviousEpisode() {
  127. document.getElementById("goPrevBtn")?.click?.();
  128. }
  129. function goNextEpisode() {
  130. document.getElementById("goNextBtn")?.click?.();
  131. }
  132. function registerEpisodeNavigator() {
  133. addEventListener("keydown", (event) => {
  134. const { ctrlKey, shiftKey, altKey } = event;
  135. if (ctrlKey || shiftKey || altKey || vim_comic_viewer.utils.isTyping(event)) return;
  136. switch (event.key) {
  137. case "t":
  138. document.getElementById("sticky-wrapper")?.scrollIntoView({ block: "center" });
  139. break;
  140. case "m":
  141. document.querySelector(".view-good")?.scrollIntoView({ block: "center" });
  142. break;
  143. }
  144. });
  145. }
  146. function getUrls() {
  147. const imgs = document.querySelectorAll("div.view-padding img");
  148. return [...imgs].flatMap(getUrl);
  149. }
  150. function getUrl(image) {
  151. if (image.offsetParent === null) return [];
  152. const data = Object.values(image.dataset);
  153. return data.length ? data : [image.src];
  154. }
  155. async function markVisitedLinks() {
  156. const links = document.querySelectorAll(".post-row a");
  157. const visitedLinks = new Set(await GM.getValue("visitedPaths", []));
  158. for (const link of links) {
  159. const url = link.getAttribute("href");
  160. if (!url) return;
  161. const path = new URL(url).pathname;
  162. if (visitedLinks.has(path)) link.style.color = "#e2e2e2";
  163. link.addEventListener("click", async () => {
  164. visitedLinks.add(path);
  165. await GM.setValue("visitedPaths", [...visitedLinks]);
  166. });
  167. }
  168. }
  169. main();
  170.  
  171.  
  172. });
  173.  
  174. define("tampermonkey_grants", function() { Object.assign(this.window, { GM, unsafeWindow }); });
  175. requirejs.config({ deps: ["tampermonkey_grants"] });
  176. load()
  177.  
  178. async function load() {
  179. const links = GM.info.script.resources.filter(x => x.name.startsWith("link:"));
  180. await Promise.all(links.map(async ({ name }) => {
  181. const script = await GM.getResourceText(name)
  182. define(name.replace("link:", ""), Function("require", "exports", "module", script))
  183. }));
  184. require(["main"], () => {}, console.error);
  185. }