// ==UserScript==
// @name Modal Image in rule(rule34图片查看器 水果玉米版)
// @namespace http://tampermonkey.net/
// @version 20241127
// @description Add a modal image and thumbnails vertical-carrousel to rule34, add functions of drag img and resize img, rule34图片查看器, 修改版
// @author falaz, hzx
// @match https://rule34.xxx/index.php?page=po*
// @icon https://www.google.com/s2/favicons?domain=rule34.xxx
// @grant GM_download
// @license MIT
// ==/UserScript==
/*jshint esversion: 6 */
// @ts-check
// original author is falaz, hzx just add functios orinal https://sleazyfork.org/en/scripts/390625-modal-image-in-rule\
function getFileNameFromURL(url) {
// 检查是否包含问号并去掉?及其后面的内容
let cleanedUrl = url.includes('?') ? url.split('?')[0] : url;
// 获取最后一个斜杠后的内容
let fileName = cleanedUrl.substring(cleanedUrl.lastIndexOf('/') + 1);
return fileName;
}
class Falaz {
/**
* Like querySelector, but small
* @param {String} selector
* @param {Element|Document} element
* @returns {HTMLElement}
*/
q(selector, element = document) {
return element.querySelector(selector);
}
/**
* Like querySelectorAll, but small
* @param {String} selector
* @param {Element|Document} element
* @returns
*/
qa(selector, element = document) {
return element.querySelectorAll(selector);
}
}
class Modal {
constructor(document, dp) {
this.pointer = 0;
this.dp = dp;
this.infinityScroll = new InfinityScroll(document, dp);
/**
* @property {Media[]} medias
*/
this.medias = this.infinityScroll.getMedias(document, 0);
this.thumbsGallery = new ThumbsGallery(this.medias);
this.createModalNode(document, this.thumbsGallery.render());
this.bodyParent = F.q('body');
this.visible = false;
}
createConfigBar(){
return `
`
}
createModalNode(document, extraDiv=null) {
const div = document.createElement("div");
const css = document.createElement("style");
div.innerHTML =
`${extraDiv?extraDiv:''}${this.createButtons()}
`;
css.innerHTML =
".content {display: flex; flex-wrap: wrap; gap: 5px;}" +
"span[data-nosnippet] {display: none;}" +
"#modal-container{background: #000000a8;width: 100%;height: 100%;position: fixed;z-index: 10;}" +
"#modal{height: 90%;width: 80%;background: transparent;padding: 0% 5%;margin: 2% 5% 2% 0;position: fixed}" +
"#modal img{width: auto;border: none;vertical-align: middle;height: 100%;margin: 0 auto;display:block;}" +
"#modal video{width: 100%;height:100%}"+
"#modal-container #thumbGallery{float:right;overflow-y: scroll;height: 900px;} #modal-container #thumbGallery ul{list-style:none}"+
"#modal-container .gallery-item{cursor: pointer;}"+
'#navigationModal{margin-bottom: 0px;font-size: 20px;color: white;display: flex;justify-content: center;bottom: 0px;width: 100%; position:absolute}'+
'#navigationModal svg:hover g g {fill: white;}'+
'.fluid_video_wrapper video{cursor:default!important}'+
'@media (max-width: 1024px) {#thumbGallery {display: none;} #modal{width:100%;padding:0%;overflow-x:scroll}}'+
'#modal-container button {border-radius: 25px; border: none; background: #1abc9c; margin-left: 10px}';
if(debug){
css.innerHTML+= "img,video{filter:blur(30px)}";
}
document.body.prepend(div);
document.head.prepend(css);
this.modalContainer = F.q("#modal-container");
this.modal = F.q("#modal");
this.infinityScroll.nextPage =this.infinityScroll.getNextPageHref(document);
}
createButtons(){
return ``
}
/**
* @param {Media} media
*/
async render(media) {
if (!this.modalContainer) {
this.createModalNode(document,this.thumbsGallery.render());
}
if (media.src == "Retry") {
await this.reloadMediaSrc(media);
}
if (media.type == "video") {
this.modal.innerHTML = ``;
F.q('#modalVideo').volume = this.getCurrentVolume();
F.q('#modalVideo').onvolumechange = e=>this.saveCurrentVolume(e.target.volume);
fluidPlayer(document.querySelector('#modal video'),{
layoutControls:{
autoPlay:true,
allowDownload:true,
loop:true,
fillToContainer: true
}
})
F.q('#modal video').loop = true
} else {
this.modal.innerHTML = `
`;
// hzx修改===========
this.innerImg = F.q("img", this.modal );
let img = this.innerImg;
let scale = 1;
let originX = 0;
let originY = 0;
let isDragging = false;
let startX, startY;
img.addEventListener('wheel', (event) => {
event.preventDefault();
const rect = img.getBoundingClientRect();
const offsetX = event.clientX - rect.left;
const offsetY = event.clientY - rect.top;
const delta = Math.sign(event.deltaY) * -0.1;
const newScale = Math.min(Math.max(0.5, scale + delta), 3);
originX = (originX - offsetX) * (newScale / scale) + offsetX;
originY = (originY - offsetY) * (newScale / scale) + offsetY;
scale = newScale;
img.style.transform = `translate(${originX}px, ${originY}px) scale(${scale})`;
});
img.addEventListener('mousedown', (event) => {
event.preventDefault();
isDragging = true;
startX = event.clientX;
startY = event.clientY;
img.style.cursor = 'grabbing';
});
window.addEventListener('mouseup', () => {
event.preventDefault();
isDragging = false;
img.style.cursor = 'grab';
});
window.addEventListener('mousemove', (event) => {
event.preventDefault();
if (!isDragging) return;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
originX += dx;
originY += dy;
startX = event.clientX;
startY = event.clientY;
img.style.transform = `translate(${originX}px, ${originY}px) scale(${scale})`;
});
this.resetImage = function() {
scale = 1;
originX = 0;
originY = 0;
img.style.transform = `translate(0, 0) scale(${scale})`;
}
this.downloadImage = function() {
GM_download({url:img.src, name: getFileNameFromURL(img.src)});
}
// hzx修改结束
}
this.modalContainer.style.display = "block";
this.visible = true;
this.toggleBodyScroll(false);
modalObj.scrollIntoCurrentMedia();
}
toggleBodyScroll(visible){
this.bodyParent.style.overflow = visible?'inherit':'hidden';
}
getCurrentVolume(){
return localStorage.volume? localStorage.volume: 0.0;
}
/**
* Attach to event video.onvolumechange
* @param {number} value
*/
saveCurrentVolume(value){
localStorage.volume = value;
}
async getNextPage() {
if (!this.infinityScroll.nextSended) {
this.infinityScroll.nextSended = true;
const docResponse = await this.infinityScroll.getNextPage();
if (docResponse) {
const medias = this.infinityScroll.getMedias(
docResponse,
this.medias.length
);
this.addMedias(medias);
this.infinityScroll.nextSended = false;
}else{
c('The page requested is on the medias');
}
}
}
close() {
if (!this.modalContainer) {
this.createModalNode(document);
}
try {
this.modal.querySelector("video").pause();
} catch (e) {}
this.modalContainer.style.display = "none";
this.toggleBodyScroll(true);
this.visible = false;
}
nextMedia() {
if (this.pointer < this.medias.length - 1) {
this.pointer++;
this.render(this.medias[this.pointer]);
} else {
this.getNextPage();
this.pointer++;
this.render(this.medias[this.pointer]);
}
}
goToMedia(index){
if(index 0) {
this.pointer--;
this.render(this.medias[this.pointer]);
} else {
console.error("Reach the start of the medias");
}
}
getCurrentMedia(){
this.medias[this.pointer];
}
getCurrentThumb(){
return F.q(`span [data-index="${this.pointer}"]`).parentElement
}
/**
*
* @param {Media[]} medias
*/
addMedias(medias) {
const mediasTemp = [...this.medias, ...medias];
// @ts-ignore
this.medias = mediasTemp;
this.thumbsGallery.updateMedias(mediasTemp);
}
async reloadMediaSrcFromMedias(index) {
await this.medias[index].reloadSrc();
}
async reloadMediaSrc(media) {
await media.reloadSrc();
}
scrollIntoCurrentMedia(){
c('fired')
this.getCurrentThumb().scrollIntoView();
this.thumbsGallery.scrollToThumb(modalObj.pointer)
}
}
class Media {
/**
* @param {string} _page This is the page of the media. Not the real src.
* @param {string} _type
* @param {string} _thumb
*/
constructor(_page, _type, _thumb, dp) {
this.page = _page;
this.type = _type;
this.thumb = _thumb;
this.dp = dp;
this.getSrc().then((src) => {
this.src = src;
});
}
async getSrc() {
const response = await fetch(this.page);
if ([202, 200].includes(response.status)) {
const body = await response.text();
const dp = new DOMParser();
const pageDocument = dp.parseFromString(body, "text/html");
const video = F.q("video source", pageDocument);
const image = F.q(".flexi img", pageDocument);
if (video) {
this.type = "video";
// @ts-ignore
return video.src;
} else {
this.type = "image";
// @ts-ignore
return image.src;
}
} else {
return "Retry";
}
}
async reloadSrc() {
this.src = await this.getSrc();
}
}
class InfinityScroll {
/**
* @param {Document} document
* @param {DOMParser} _dp
*/
constructor(document, _dp) {
this.nextSended = false;
this.pagesParsed = [this.getPageNumber(document)];
this.nextPage = this.getNextPageHref(document);
this.dp = _dp;
}
/**
* @param {Document} doc
*/
getPageNumber(doc) {
return parseInt(doc.querySelector("#paginator div b").innerHTML);
}
/**
* @param {Document} doc
*/
getNextPageHref(doc) {
const nextNode = doc.querySelector('#paginator [alt="next"]');
// @ts-ignore
return nextNode ? nextNode.href : null;
}
async getNextPage() {
c("getNextPage");
if (!this.nextPage) {
// @ts-ignore
this.nextPage = F.q('#paginator [alt="next"]').href;
}
const response = await fetch(this.nextPage);
if([204,202,200,201].includes(response.status)){
const body = await response.text();
const dp = new DOMParser();
const pageDocument = dp.parseFromString(body, "text/html");
const pageNum = this.getPageNumber(pageDocument);
this.nextPage = this.getNextPageHref(pageDocument);
if (!this.pagesParsed.includes(pageNum)) { // remove the page allready parsed
this.pagesParsed.push(pageNum);
return pageDocument;
} else {
return false;
}
}else{
await this.sleep(500);
return await this.getNextPage();
}
}
sleep(ms){
return new Promise(resolve=>setTimeout(resolve,ms));
}
/**
*
* @param {Document} document
* @param {number} pad The length of medias in the Modal Object
* @returns {Media[]}
*/
getMedias(document, pad) {
const thumbsNode = F.qa("#content .thumb", document);
const medias = [];
const nodes = [];
//const pad = this.medias? this.medias.length : 0;
for (let i = 0; i < thumbsNode.length; i++) {
const node = thumbsNode[i];
/**
* @type {HTMLImageElement} img
*/
// @ts-ignore
const img = F.q("img", node);
const title = img.title;
const anchor = node.querySelector("a");
const media = new Media(
anchor.href,
/animated|video/.test(title) ? "video" : "image",
img.src,
this.dp
);
anchor.dataset.index = (i + pad).toString();
img.dataset.index = (i + pad).toString();
//node.dataset.index = (i+pad).toString();
medias.push(media);
nodes.push(node);
}
this.addElementsToCurrentContentView(nodes); // async
return medias;
}
async addElementsToCurrentContentView(nodes) {
for (const node of nodes) {
this.clickFunction(node);
F.q(".content").insertBefore(node, F.q("#paginator"));
}
}
clickFunction(element){
element.addEventListener("click", (e) => {
e.preventDefault();
const index = e.target.dataset.index
? e.target.dataset.index
: modalObj.pointer;
modalObj.pointer = index;
modalObj.render(modalObj.medias[index]);
});
}
}
class ThumbsGallery{
/**
* @param {Media[]} medias
*/
constructor(medias){
this.medias = medias;
}
/**
* @param {number} index
*/
scrollToThumb(index){
F.q(`.gallery-item[data-index="${index}"]`).scrollIntoView();
}
/**
* @param {Media[]} medias
*/
updateMedias(medias){
this.medias = medias;
F.q('#thumbGallery').innerHTML = ``;
}
render(){
return ``
}
buildItems(){
let li = ''
this.medias.forEach((e,i)=>{
li+=`
`
})
return li;
}
}
const dp = new DOMParser();
const F = new Falaz();
let modalObj;
const loadingSVG = "https://samherbert.net/svg-loaders/svg-loaders/puff.svg";
const debug = false;
const c = (...e)=>{
if(debug){
console.log(e);
}
}
(function () {
"use strict";
modalObj = new Modal(document, dp);
if(debug){document.title = 'New Tab'}
// @ts-ignore
document.modalObj = modalObj;
F.qa(".content .thumb").forEach((element) => {
modalObj.infinityScroll.clickFunction(element);
});
document.addEventListener("keydown", (e) => {
if (e.key == "ArrowRight") {
modalObj.nextMedia();
modalObj.scrollIntoCurrentMedia();
} else if (e.key == "ArrowLeft") {
modalObj.prevMedia();
modalObj.scrollIntoCurrentMedia();
} else if (e.key == "Escape") {
modalObj.close();
}
});
// @ts-ignore
document.addEventListener("scroll", (e) => {
if (!modalObj.visible){
const inner = window.innerHeight;
if (
window.scrollY + inner >
document.documentElement.scrollHeight - inner * 2
) {
modalObj.getNextPage();
}
}else{
e.preventDefault();
modalObj.scrollIntoCurrentMedia();
}
});
$Q_Async(".awesomplete input").then(inputEl => {
if (!inputEl.value.includes("-ai_generated")) {
setTimeout(() => {
inputEl.value = inputEl.value += " -ai_generated";
$Q_Async("input[type=submit]").then(btnEl => btnEl.click());
}, 500);
}
});
})();
function $Q_Async(selector, timeout = 1000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
const ele = document.querySelector(selector);
if (ele !== null) {
resolve(ele); // 找到元素,结束 Promise
return;
}
if (Date.now() - startTime > timeout) {
reject(`$Q_Async: Timeout: Cannot find element for selector "${selector}"`);
return;
}
setTimeout(check, 100);
}
check();
});
}