// ==UserScript==
// @name 토끼 뷰어
// @name:ko 토끼 뷰어
// @name:en toki viewer
// @description i,j,k 키를 눌러보세요
// @description:ko i,j,k 키를 눌러보세요
// @description:en press i to open
// @version 250406112833
// @match https://*.net/bbs/*
// @match https://*.net/comic/*
// @match https://*.com/webtoon/*
// @match https://*.com/novel/*
// @author nanikit
// @namespace https://greasyfork.dpdns.org/ko/users/713014-nanikit
// @license MIT
// @connect *
// @grant GM.addValueChangeListener
// @grant GM.getResourceText
// @grant GM.getValue
// @grant GM.removeValueChangeListener
// @grant GM.setValue
// @grant GM.xmlHttpRequest
// @grant unsafeWindow
// @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.dpdns.org/scripts/417893/1566357/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"));
async function main() {
const origin = getOrigin();
if (origin === "unknown") return;
markVisitedLinks();
registerEpisodeNavigator();
const buttons = duplicateViewerButton();
const source = await comicSource();
const controller = await (0, vim_comic_viewer.initialize)({
source: () => source,
onPreviousSeries: goPreviousEpisode,
onNextSeries: goNextEpisode
});
controller.setScriptPreferences({
manualPreset: origin,
preferences: { pageDirection: origin === "manatoki" ? "rightToLeft" : "leftToRight" }
});
for (const button of buttons) button.addEventListener("click", async () => {
await controller.setImmersive(true);
});
}
function getOrigin() {
const allowedOrigins = [
"manatoki",
"newtoki",
"booktoki"
];
return allowedOrigins.find(originIncludes) ?? "unknown";
}
function originIncludes(str) {
return location.origin.includes(str);
}
function duplicateViewerButton() {
const template = document.createElement("template");
template.innerHTML = `<a class="show_viewer" alt="뷰어로 보기">
<i class="ion-ios-book at-tip" aria-hidden="true" style="color: blue;"></i>
</a>`;
const templateButton = template.content.firstElementChild;
const buttons = [];
const divs = document.querySelectorAll(".toon-nav");
for (const div of divs) {
const button = templateButton.cloneNode(true);
div.prepend(button);
buttons.push(button);
}
return buttons;
}
async function comicSource() {
while (true) {
const urls = getUrls();
if (urls.length) return urls;
await vim_comic_viewer.utils.timeout(200);
}
}
function goPreviousEpisode() {
document.getElementById("goPrevBtn")?.click?.();
}
function goNextEpisode() {
document.getElementById("goNextBtn")?.click?.();
}
function registerEpisodeNavigator() {
addEventListener("keydown", (event) => {
const { ctrlKey, shiftKey, altKey } = event;
if (ctrlKey || shiftKey || altKey || vim_comic_viewer.utils.isTyping(event)) return;
switch (event.key) {
case "t":
document.getElementById("sticky-wrapper")?.scrollIntoView({ block: "center" });
break;
case "m":
document.querySelector(".view-good")?.scrollIntoView({ block: "center" });
break;
}
});
}
function getUrls() {
const imgs = document.querySelectorAll("div.view-padding img");
return [...imgs].flatMap(getUrl);
}
function getUrl(image) {
if (image.offsetParent === null) return [];
const data = Object.values(image.dataset);
return data.length ? data : [image.src];
}
async function markVisitedLinks() {
const links = document.querySelectorAll(".post-row a");
const visitedLinks = new Set(await GM.getValue("visitedPaths", []));
for (const link of links) {
const url = link.getAttribute("href");
if (!url) return;
const path = new URL(url).pathname;
if (visitedLinks.has(path)) link.style.color = "#e2e2e2";
link.addEventListener("click", async () => {
visitedLinks.add(path);
await GM.setValue("visitedPaths", [...visitedLinks]);
});
}
}
main();
});
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);
}