秀人相册查看器:列表页快速预览,详情页用 Viewer.js 浏览全部图片
// ==UserScript==
// @name Viewer for XiuRen22
// @namespace http://tampermonkey.net/XiuRen22
// @version 1.0.0
// @description 秀人相册查看器:列表页快速预览,详情页用 Viewer.js 浏览全部图片
// @author You
// @include *://*.xx05.my/*
// @include *://*xiuren*
// @include *://*xr*
// @include *://*.*/*XiuRen*
// @icon https://www.google.com/s2/favicons?domain=xiurenb.net
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_listValues
// @grant GM_deleteValue
// @require https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.8/viewer.min.js
// @resource viewerCSS https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.8/viewer.min.css
// ==/UserScript==
(() => {
// src/shared/constants.js
var API_BASE_URL = "http://de.ik5.cc/06/";
var DEBUG_INFO = true;
var DEBUG_VERBOSE = true;
var CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
var VIEWER_OPTIONS = {
fullscreen: false,
interval: 1200,
loop: false,
transition: false
};
// src/shared/debug.js
function debugInfo(...data) {
if (DEBUG_INFO) console.log("[INFO]", ...data);
}
function debugVerbose(...data) {
if (DEBUG_VERBOSE) console.log("[VERBOSE]", ...data);
}
// src/shared/img.js
function fixImgSrc(src) {
if (!src || src.indexOf("/uploadfile/") === -1) return src;
const parts = src.split("/uploadfile/");
return parts.length > 1 ? API_BASE_URL + "Uploadfile/" + parts[1] : src;
}
function fixImgNode(img) {
const raw = img.getAttribute("src");
const fixed = fixImgSrc(raw);
if (raw === fixed) return;
img.setAttribute("src", fixed);
const origin = img.getAttribute("data-original");
if (origin) img.setAttribute("data-original", fixImgSrc(origin));
}
function fixPageImages(root) {
for (const img of root.getElementsByTagName("img")) fixImgNode(img);
}
function imageParagraphNode(rootDoc) {
for (const img of rootDoc.getElementsByTagName("img")) {
const src = img.getAttribute("src");
if (!src) continue;
if (src.indexOf("/uploadfile/2") !== -1 || src.indexOf("Uploadfile/2") !== -1) {
return img.parentNode;
}
}
return null;
}
// src/shared/cache.js
function createCache(prefix, ttl) {
const memCache = {};
function get(url) {
const key = prefix + url;
if (memCache[key]) return memCache[key];
let entry = null;
try {
const raw = GM_getValue(key, null);
if (Array.isArray(raw)) {
entry = { urls: raw, pageCount: 1, time: Date.now() };
try {
GM_setValue(key, entry);
} catch (e) {
}
} else if (raw && Array.isArray(raw.urls)) {
entry = raw;
}
} catch (e) {
return null;
}
if (!entry || Date.now() - entry.time > ttl) {
delete memCache[key];
try {
GM_deleteValue(key);
} catch (e) {
}
return null;
}
memCache[key] = entry;
return entry;
}
function set(url, urls, pageCount) {
const key = prefix + url;
const entry = { urls, pageCount: pageCount || 1, time: Date.now() };
memCache[key] = entry;
try {
GM_setValue(key, entry);
} catch (e) {
console.warn("\u6301\u4E45\u7F13\u5B58\u5199\u5165\u5931\u8D25\uFF0C\u6E05\u7406\u8FC7\u671F\u7F13\u5B58\u540E\u91CD\u8BD5", e);
purge();
try {
GM_setValue(key, entry);
} catch (e2) {
}
}
}
function purge() {
const now = Date.now();
try {
GM_listValues().forEach((key) => {
if (key.indexOf(prefix) !== 0) return;
const raw = GM_getValue(key, null);
if (!raw || Array.isArray(raw)) return;
if (!raw.time || now - raw.time > ttl) {
GM_deleteValue(key);
}
});
} catch (e) {
}
}
return { get, set, purge };
}
// src/shared/page.js
function fetchPageHtml(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url,
method: "GET",
onload: (xhr) => resolve(xhr.response),
onerror: reject
});
});
}
function parseDetailPage(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
const node = imageParagraphNode(doc);
const urls = node ? Array.from(node.getElementsByTagName("img")).map((img) => img.getAttribute("src")).filter(Boolean).map(fixImgSrc) : [];
const current = doc.getElementsByClassName("current")[0];
const indicator = current ? current.parentNode : null;
let pageCount = 1;
if (indicator) {
indicator.querySelectorAll("a").forEach((a) => {
const m = a.textContent.trim().match(/^(\d+)$/);
if (m) pageCount = Math.max(pageCount, parseInt(m[1], 10));
const h = (a.getAttribute("href") || "").match(/_(\d+)\.html$/);
if (h) pageCount = Math.max(pageCount, parseInt(h[1], 10) + 1);
});
}
return { urls, pageCount };
}
function parseImageUrlsFromHtml(html) {
return parseDetailPage(html).urls;
}
function createImageLoader(cache2) {
async function fetchPageImageUrls(url) {
const cached = cache2.get(url);
if (cached) {
debugVerbose(`\u7F13\u5B58\u547D\u4E2D: ${url} (${cached.urls.length} \u5F20)`);
return cached.urls;
}
debugInfo("\u91C7\u96C6\u56FE\u7247\u81EA", url);
const html = await fetchPageHtml(url);
const urls = parseImageUrlsFromHtml(html);
debugVerbose(`\u89E3\u6790\u5230 ${urls.length} \u5F20\u56FE\u7247: ${url}`);
if (urls.length > 0) cache2.set(url, urls, 1);
return urls;
}
async function loadSubPageUrls(baseUrl, pageCount) {
if (pageCount <= 1) {
const all = [];
let i = 1;
while (true) {
const urls = await fetchPageImageUrls(baseUrl + "_" + i + ".html");
if (urls.length === 0) break;
all.push(...urls);
i++;
}
return all;
}
const tasks = [];
for (let i = 1; i < pageCount; i++) {
tasks.push(fetchPageImageUrls(baseUrl + "_" + i + ".html"));
}
const results = await Promise.allSettled(tasks);
return results.filter((r) => r.status === "fulfilled").map((r) => r.value).flat();
}
return { fetchPageImageUrls, loadSubPageUrls };
}
// src/shared/preview.js
async function openGalleryPreview(btn, galleryUrl, { cache: cache2, loader: loader2 }) {
const originalText = btn.innerText;
btn.disabled = true;
btn.innerText = "...";
debugInfo("\u5F00\u59CB\u5FEB\u901F\u9884\u89C8:", galleryUrl);
try {
let firstUrls;
let pageCount;
const cached = cache2.get(galleryUrl);
if (cached && cached.urls.length > 0) {
firstUrls = cached.urls;
pageCount = cached.pageCount;
debugVerbose(`\u7F13\u5B58\u547D\u4E2D: ${galleryUrl} (${firstUrls.length} \u5F20)`);
} else {
const html = await fetchPageHtml(galleryUrl);
const data = parseDetailPage(html);
firstUrls = data.urls;
pageCount = data.pageCount;
if (firstUrls.length === 0) {
alert("\u672A\u627E\u5230\u56FE\u7247");
return;
}
cache2.set(galleryUrl, firstUrls, pageCount);
}
const ul = document.createElement("ul");
ul.style.display = "none";
document.body.appendChild(ul);
const addImgs = (urls) => {
for (const url of urls) {
const img = document.createElement("img");
img.src = url;
img.style.maxHeight = "1080px";
ul.appendChild(img);
}
};
addImgs(firstUrls);
let closed = false;
let playerObserver = null;
const viewer = new Viewer(ul, {
...VIEWER_OPTIONS,
title: false,
hidden() {
closed = true;
if (playerObserver) playerObserver.disconnect();
viewer.destroy();
ul.remove();
btn.innerText = originalText;
}
});
viewer.show();
if (viewer.player) {
playerObserver = new MutationObserver(() => {
if (!viewer.played) return;
const imgs = viewer.player.querySelectorAll("img");
const last = imgs[imgs.length - 1];
if (last && last.classList.contains("viewer-in")) {
setTimeout(() => {
if (viewer.played) viewer.stop();
}, Math.max(50, viewer.options.interval - 100));
}
});
playerObserver.observe(viewer.player, {
attributes: true,
subtree: true,
attributeFilter: ["class"]
});
}
const baseUrl = galleryUrl.replace(".html", "");
const extraUrls = await loader2.loadSubPageUrls(baseUrl, pageCount);
if (!closed && extraUrls.length > 0) {
addImgs(extraUrls);
viewer.update();
}
debugInfo("\u5FEB\u901F\u9884\u89C8\u5171\u52A0\u8F7D", ul.children.length, "\u5F20\u56FE\u7247");
} catch (e) {
console.error("Quick Preview Error:", e);
btn.innerText = "Err";
setTimeout(() => {
btn.innerText = originalText;
}, 2e3);
} finally {
btn.disabled = false;
}
}
// src/shared/random.js
function createRandomPlayButton({ getUrls, cache: cache2, loader: loader2 }) {
const btn = document.createElement("button");
btn.className = "xr-random-play";
btn.title = "\u968F\u673A\u64AD\u653E\u5F53\u524D\u7011\u5E03\u6D41\u4E2D\u7684\u4E00\u4E2A\u76F8\u518C";
btn.textContent = "\u968F\u673A\u64AD\u653E";
btn.addEventListener("click", () => {
const urls = getUrls();
if (urls.length === 0) {
alert("\u6682\u65E0\u53EF\u64AD\u653E\u7684\u76F8\u518C");
return;
}
const url = urls[Math.floor(Math.random() * urls.length)];
debugInfo("\u968F\u673A\u64AD\u653E:", url);
openGalleryPreview(btn, url, { cache: cache2, loader: loader2 });
});
document.body.appendChild(btn);
return btn;
}
// src/shared/styles.js
function addSharedStyles() {
GM_addStyle(GM_getResourceText("viewerCSS"));
GM_addStyle(`
.xiuren-preview-btn {
position: absolute;
top: 5px;
right: 5px;
z-index: 1000;
padding: 4px 8px;
font-size: 12px;
opacity: 0.9;
display: inline-block;
background-color: #e91e63;
color: #fff;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
border: none;
transition: background-color 0.3s;
line-height: normal;
}
.xiuren-preview-btn:hover {
background-color: #c2185b;
}
.xiuren-preview-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* \u7F29\u7565\u56FE\u5DE6\u4E0B\u89D2\u65E5\u671F\u5FBD\u6807 */
.xr-s-date {
position: absolute;
left: 5px;
bottom: 5px;
z-index: 1;
padding: 2px 6px;
font-size: 11px;
line-height: 1.2;
color: #fff;
background: rgba(0, 0, 0, 0.55);
border-radius: 3px;
pointer-events: none;
}
/* \u53F3\u4E0B\u89D2\u60AC\u6D6E\u7684\u968F\u673A\u64AD\u653E\u6309\u94AE */
.xr-random-play {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 9999;
padding: 10px 16px;
font-size: 14px;
font-weight: bold;
color: #fff;
background: #e91e63;
border: none;
border-radius: 20px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.xr-random-play:hover {
background: #c2185b;
}
.xr-random-play:disabled {
background: #ccc;
cursor: not-allowed;
}
`);
}
// src/shared/infinite.js
function setupInfiniteScroll({ loadPage }) {
let loading = false;
let stopped = false;
async function maybeLoad() {
if (loading || stopped) return;
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 600) {
return;
}
loading = true;
let more = false;
try {
more = await loadPage();
} catch (e) {
more = false;
}
loading = false;
if (!more) {
stopped = true;
return;
}
maybeLoad();
}
let ticking = false;
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
maybeLoad();
});
};
window.addEventListener("scroll", onScroll);
window.addEventListener("resize", onScroll);
maybeLoad();
}
// src/shared/util.js
function extractPostId(url) {
const m = url.match(/(\d+)\.html/);
return m ? m[1] : null;
}
// src/main.js
var CACHE_PREFIX = "xr_cache_";
var LIST_ITEM_SELECTOR = ".update_area_lists .i_list";
var LIST_CONTAINER_SELECTOR = "ul.update_area_lists";
var cache = createCache(CACHE_PREFIX, CACHE_TTL);
var loader = createImageLoader(cache);
addSharedStyles();
GM_addStyle(`
/* \u5217\u8868\u9875\u5361\u7247\u6837\u5F0F\u7EDF\u4E00\u4E3A\u641C\u7D22\u9875\u98CE\u683C\uFF1A\u54CD\u5E94\u5F0F\u7EA6 5 \u5217\u5706\u89D2\u5361\u7247 */
ul.update_area_lists {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: 12px;
padding: 12px;
margin: 0;
list-style: none;
}
ul.update_area_lists > .i_list {
position: relative;
overflow: hidden;
border-radius: 6px;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
margin: 0 !important;
width: auto !important;
}
ul.update_area_lists .i_list > a {
display: block;
position: relative;
}
ul.update_area_lists .i_list img {
width: 100%;
aspect-ratio: 3 / 4;
object-fit: cover;
display: block !important;
}
ul.update_area_lists .case_info {
padding: 0;
}
ul.update_area_lists .case_info .meta-title {
padding: 6px 8px;
font-size: 12px;
line-height: 17px;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
ul.update_area_lists .case_info .meta-post {
display: none;
}
/* \u6A21\u7279\u540D\u505A\u6210\u5C0F\u5FBD\u6807\uFF0C\u653E\u5728\u7F29\u7565\u56FE\u53F3\u4E0B\u89D2\uFF08\u4E0E\u5DE6\u4E0B\u89D2\u65E5\u671F\u5FBD\u6807\u5BF9\u79F0\uFF09 */
ul.update_area_lists .postlist-imagenum {
position: absolute;
right: 5px;
bottom: 5px;
left: auto;
top: auto;
z-index: 1;
width: auto !important;
max-width: calc(100% - 10px);
padding: 2px 6px;
background: rgba(0, 0, 0, 0.55);
border-radius: 3px;
}
.postlist-imagenum span {
color: #fff !important;
cursor: pointer;
}
.postlist-imagenum span:hover {
text-decoration: underline;
}
`);
function scanListItems() {
document.querySelectorAll(LIST_ITEM_SELECTOR).forEach((item) => {
if (item.dataset.xiurenPreviewDone) return;
item.dataset.xiurenPreviewDone = "true";
const link = item.querySelector('a[href*=".html"]');
if (!link) return;
const href = link.getAttribute("href");
if (!extractPostId(href)) return;
item.style.position = "relative";
const meta = item.querySelector(".case_info .meta-post");
const dateMatch = meta ? meta.textContent.match(/(\d{4})\.(\d{1,2})\.(\d{1,2})/) : null;
if (dateMatch) {
const badge = document.createElement("span");
badge.className = "xr-s-date";
badge.textContent = `${dateMatch[1].slice(-2)}.${parseInt(dateMatch[2], 10)}.${parseInt(dateMatch[3], 10)}`;
link.appendChild(badge);
}
const btn = document.createElement("button");
btn.className = "xiuren-preview-btn";
btn.innerText = "\u25B6";
btn.title = "Quick Preview";
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
openGalleryPreview(btn, href, { cache, loader });
});
item.appendChild(btn);
link.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
openGalleryPreview(btn, href, { cache, loader });
});
const modelSpan = item.querySelector(".postlist-imagenum span");
if (modelSpan) {
modelSpan.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const name = modelSpan.textContent.trim();
if (!name) return;
window.open(
location.origin + "/plus/search/index.asp?keyword=" + encodeURIComponent(name) + "&searchtype=titlekeywords",
"_blank"
);
});
}
});
}
var infiniteScroll = {
current: 1,
// 已加载到的页码(首页为 1)
total: 1,
// 总页数
finished: false
};
function detectPagination() {
const m = location.pathname.match(/index(\d+)\.html$/);
infiniteScroll.current = m ? parseInt(m[1], 10) : 1;
let max = 1;
document.querySelectorAll('.page a[href*="index"]').forEach((a) => {
const n = a.getAttribute("href").match(/index(\d+)\.html/);
if (n) max = Math.max(max, parseInt(n[1], 10));
});
infiniteScroll.total = max;
infiniteScroll.finished = infiniteScroll.current >= max;
}
function nextPageUrl() {
const base = location.pathname.replace(/index\d+\.html$/, "").replace(/\/+$/, "") + "/";
return location.origin + base + `index${infiniteScroll.current + 1}.html`;
}
function parseListItems(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
return doc.querySelectorAll(LIST_ITEM_SELECTOR);
}
function appendListPage(items) {
const container = document.querySelector(LIST_CONTAINER_SELECTOR);
if (!container || items.length === 0) return false;
items.forEach((li) => {
const node = document.importNode(li, true);
fixPageImages(node);
container.appendChild(node);
});
return true;
}
function loadNextListPage() {
return new Promise((resolve) => {
if (infiniteScroll.finished) {
resolve(false);
return;
}
const url = nextPageUrl();
debugInfo("\u7011\u5E03\u6D41\u52A0\u8F7D\u7B2C", infiniteScroll.current + 1, "\u9875:", url);
GM_xmlhttpRequest({
url,
method: "GET",
onload: (xhr) => {
const items = parseListItems(xhr.response);
let more = false;
if (appendListPage(items)) {
infiniteScroll.current++;
debugInfo("\u5DF2\u8FFD\u52A0\u7B2C", infiniteScroll.current, "\u9875\uFF0C\u5171", items.length, "\u4E2A\u76F8\u518C");
scanListItems();
more = true;
} else {
infiniteScroll.finished = true;
debugInfo("\u7011\u5E03\u6D41\u7ED3\u675F\uFF1A\u8BE5\u9875\u65E0\u66F4\u591A\u76F8\u518C");
}
if (infiniteScroll.current >= infiniteScroll.total) {
infiniteScroll.finished = true;
debugInfo("\u7011\u5E03\u6D41\u7ED3\u675F\uFF1A\u5DF2\u52A0\u8F7D\u5168\u90E8\u9875\u9762");
}
resolve(more);
},
onerror: () => {
console.error("XiuRen: \u7011\u5E03\u6D41\u52A0\u8F7D\u5931\u8D25", url);
infiniteScroll.finished = true;
resolve(false);
}
});
});
}
function initListPage() {
scanListItems();
new MutationObserver(scanListItems).observe(document.body, {
childList: true,
subtree: true
});
createRandomPlayButton({
getUrls() {
const urls = [];
document.querySelectorAll(LIST_ITEM_SELECTOR).forEach((item) => {
const link = item.querySelector('a[href*=".html"]');
const href = link ? link.getAttribute("href") : null;
if (href && extractPostId(href)) urls.push(href);
});
return urls;
},
cache,
loader
});
detectPagination();
if (infiniteScroll.finished) return;
setupInfiniteScroll({ loadPage: loadNextListPage });
}
function getPageImages(i, htmlDoc) {
const node = imageParagraphNode(htmlDoc);
if (!node) return [];
const template = document.createElement("div");
template.innerHTML = `<div class='child${i}'>${node.outerHTML}</div>`;
return Array.from(template.firstChild.getElementsByTagName("img"));
}
function getPageNumbers() {
const current = document.getElementsByClassName("current")[0];
const indicator = current ? current.parentNode : null;
if (!indicator) return [];
const nums = [];
indicator.querySelectorAll("a").forEach((a) => {
const m = a.textContent.trim().match(/^(\d+)$/);
if (m) nums.push(parseInt(m[1], 10));
});
return nums;
}
function buildPageUrls(href, pageNumbers) {
const urls = [href];
for (const num of pageNumbers) {
const page = parseInt(num, 10);
if (!page || page - 1 === 0) continue;
urls.push(href.slice(0, href.lastIndexOf(".")) + `_${page - 1}.html`);
}
return urls;
}
function initGalleryPage(href) {
const paragraph = imageParagraphNode(document);
if (!paragraph) return;
const container = paragraph.parentNode;
const viewerUl = document.createElement("ul");
viewerUl.id = "imagesViewer";
container.appendChild(viewerUl);
const gallery = new Viewer(viewerUl, VIEWER_OPTIONS);
paragraph.remove();
const pageUrls = buildPageUrls(href, getPageNumbers());
const pagesMap = {};
function updateImgSet(page, imgSet) {
pagesMap[page] = imgSet;
debugVerbose(
`update_img_set: \u52A0\u8F7D\u9875\u9762 ${page + 1} / ${pageUrls.length}, \u5171\u8BA1 ${imgSet.length} \u5F20\u56FE\u7247`
);
if (Object.keys(pagesMap).length === pageUrls.length) {
debugInfo("\u6240\u6709\u9875\u9762\u7684\u4FE1\u606F\u5DF2\u7ECF\u52A0\u8F7D\u5230 pages_map \u4E2D");
let total = 0;
for (let i = 0; i < pageUrls.length; i++) {
const imgs = pagesMap[i];
if (!imgs) continue;
let pageCount = 0;
for (const img of imgs) {
img.style.maxHeight = "1080px";
fixImgNode(img);
viewerUl.appendChild(img);
total++;
pageCount++;
}
debugInfo(i, "\u9875", pageCount, "\u5F20");
}
debugInfo("\u5171\u8BA1", total, "\u5F20");
}
gallery.update();
}
pageUrls.forEach((url, i) => {
debugInfo("\u91C7\u96C6\u56FE\u7247\u81EA", url);
GM_xmlhttpRequest({
url,
method: "GET",
onload: (xhr) => {
const doc = new DOMParser().parseFromString(xhr.response, "text/html");
updateImgSet(i, getPageImages(i, doc));
}
});
});
}
function init() {
cache.purge();
fixPageImages(document);
const href = window.location.href;
debugInfo("handle: " + href);
if (href.indexOf("_") !== -1) return;
if (document.querySelector(".update_area_lists")) {
debugInfo("\u68C0\u6D4B\u5230\u5217\u8868\u9875\uFF0C\u542F\u7528\u9884\u89C8\u6309\u94AE");
initListPage();
return;
}
initGalleryPage(href);
}
init();
})();