// ==UserScript==
// @name Pixiv 瀑布流
// @namespace http://tampermonkey.net/
// @version 2025-01-01
// @description 以瀑布流的样式刷P站
// @author wlm3201
// @license MIT
// @match https://www.pixiv.net/*
// @match https://www.pixivision.net/*
// @icon https://www.pixiv.net/favicon.ico
// @run-at document-start
// ==/UserScript==
"use strict";
function addStyle(cssText) {
let link = document.createElement("link");
link.href = URL.createObjectURL(new Blob([cssText], { type: "text/css" }));
link.rel = "stylesheet";
document.head.appendChild(link);
}
function addScript(scriptText) {
let script = document.createElement("script");
script.src = URL.createObjectURL(
new Blob([scriptText], { type: "text/javascript" })
);
document.body.appendChild(script);
}
if (!location.pathname.includes("/wtf")) {
window.addEventListener("load", () => {
let ctn =
document.querySelector(
"#root > div.charcoal-token > div > div:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div > div"
) ||
document.querySelector(
"#__next > div > div:nth-child(2) > div > div > div:nth-child(1) > div > div.box-border"
) ||
document.querySelector(
"#js-mount-point-header > div > div > div > div:nth-child(1) > div:nth-child(2) > div:nth-child(1)"
) ||
document.querySelector(".htd__logo-copy-container");
let btn = document.createElement("button");
btn.innerText = "瀑布流";
btn.className = "pblbtn";
addStyle(`.pblbtn {
color-scheme: dark;
height: 40px;
width: 80px;
border-radius: 20px;
border: none;
cursor: pointer;
font-weight: 600;
font-size: 14px;
background: rgb(58, 58, 58);
margin-left: 20px;
color: #d6d6d6;
align-self: center;
z-index: 1;
&:hover {
background: rgb(82, 82, 82);
}
}`);
btn.onclick = () => {
let path = location.pathname;
let id, uid;
let wtf = "https://www.pixiv.net/wtf/";
if (path.match("discovery/users")) location.href = wtf + "/?#users";
else if (path.match("discovery")) location.href = wtf + "/?#discovery";
else if (path.match("bookmark_new_illust.php"))
location.href = wtf + "/#latest";
else if (path.match("ranking.php")) location.href = wtf + "/#ranking";
else if (path.match(/tags\/(.*)/)) location.href = wtf + "/#search";
else if ((id = path.match(/artworks\/(\d+)/)?.[1]))
location.href = wtf + `/?illust=${id}`;
else if ((uid = path.match(/users\/(\d+)\/bookmarks\/artworks/)?.[1]))
location.href = wtf + `/?bookmarked=${uid}`;
else if ((uid = path.match(/users\/(\d+)\/following/)?.[1]))
location.href = wtf + `/?followed=${uid}`;
else if ((uid = path.match(/users\/(\d+)/)?.[1]))
location.href = wtf + `/?user=${uid}`;
else if (location.host.includes("pixivision"))
location.href = "https://www.pixivision.net/zh/wtf#vision";
else location.href = wtf + "/?#discovery";
};
ctn.append(btn);
});
} else {
window.stop();
let htmlText = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Pixiv瀑布流</title>
</head>
<body>
<div id="frags">
<div name="discovery" class="frag">
<div class="imgbox"></div>
</div>
<div name="latest" class="frag hide">
<slot name="navbar"></slot>
<div class="imgbox"></div>
</div>
<div name="users" class="frag hide">
<div class="userbox"></div>
</div>
<div name="search" class="frag hide">
<form id="searchbar" class="menu">
<span id="searchbox">
<input
id="searchinput"
name="word"
type="text"
autocomplete="off" />
<ul id="prompts"></ul>
</span>
<select name="type">
<option value="all">插画、漫画、动图</option>
<option value="illust_and_ugoira">插画、动图</option>
<option value="illust">插画</option>
<option value="manga">漫画</option>
<option value="ugoira">动图</option>
</select>
<select name="s_mode">
<option value="s_tag">标签(部分—致)</option>
<option value="s_tag_full">标签(完全一致)</option>
<option value="s_tc">标题、说明文字</option>
</select>
<label>
隐藏AI
<input type="checkbox" name="ai_type" value="1" />
</label>
<span>
<input type="text" name="wlt" style="width: 50px" />×<input
type="text"
name="hlt"
style="width: 50px" />
~
<input type="text" name="wgt" style="width: 50px" />×<input
type="text"
name="lgt"
style="width: 50px" />
</span>
<select name="ratio">
<option value="">所有纵横比</option>
<option value="0.5">横图</option>
<option value="-0.5">纵图</option>
<option value="0">正方形图</option>
</select>
<input type="date" name="scd" />
<input type="date" name="ecd" />
<select name="mode">
<option value="all">全部</option>
<option value="safe">全年龄</option>
<option value="r18">R-18</option>
</select>
<select name="order">
<option value="date_d">按最新排序</option>
<option value="date">按旧排序</option>
</select>
<input type="submit" value="搜索" id="searchbtn" />
</form>
<slot name="navbar"></slot>
<div class="imgbox"></div>
</div>
<div name="following" class="frag hide">
<slot name="navbar"></slot>
<div class="userbox"></div>
</div>
<div name="bookmarks" class="frag hide">
<slot name="navbar"></slot>
<div class="imgbox"></div>
</div>
<div name="history" class="frag hide">
<div class="imgbox"></div>
</div>
<div name="ranking" class="frag hide">
<div class="menu">
<label><input type="radio" name="mode" value="daily" />今日</label>
<label><input type="radio" name="mode" value="weekly" />本周</label>
<label><input type="radio" name="mode" value="monthly" />本月</label>
<label><input type="radio" name="mode" value="rookie" />新人</label>
<label><input type="radio" name="mode" value="original" />原创</label>
<label><input type="radio" name="mode" value="daily_ai" />A I</label>
<label><input type="radio" name="mode" value="male" />男性</label>
<label><input type="radio" name="mode" value="female" />女性</label>
<button class="prev"><</button>
<input type="date" name="date" />
<button class="next">></button>
<label>
<input type="checkbox" value="_r18" />
R-18
</label>
</div>
<slot name="navbar"></slot>
<div class="imgbox"></div>
</div>
<div name="vision" class="frag hide">
<div class="imgbox"></div>
<button id="rightbtn" class="round float" title="Caps">≡</button>
<div id="rightbar" class="sidebar">
<ul id="articlebox"></ul>
<div class="pages">
<input type="text" class="pagenum" />
<button class="goto">Go</button>
</div>
<button id="closeright" class="round">×</button>
</div>
</div>
</div>
<button id="leftbtn" class="round float" title="Tab">≡</button>
<button id="back" class="round float" title="alt+←"><</button>
<button id="forward" class="round float" title="alt+→">></button>
<button id="top" class="round float" title="home">△</button>
<button id="end" class="round float" title="end">▽</button>
<div id="leftbar" class="sidebar hide">
<div id="tabs">
<button name="discovery" class="tab">发现</button>
<button name="latest" class="tab">动态</button>
<button name="users" class="tab">用户</button>
<button name="search" class="tab">搜索</button>
<div class="split"></div>
<button name="following" class="tab">关注</button>
<button name="bookmarks" class="tab">收藏</button>
<button name="history" class="tab">历史</button>
<button name="ranking" class="tab">排行</button>
<button name="vision" class="tab">vision</button>
</div>
<span>
<span>高度:</span>
<span id="heightvalue"></span>
</span>
<input
id="heightinput"
type="range"
min="200"
max="600"
step="50"
value="300" />
<button id="refresh" class="round">↻</button>
<button id="closeleft" class="round">×</button>
</div>
<div id="cover" class="hide">
<img id="zoom" />
<button class="prev round float"><</button>
<button class="next round float">></button>
</div>
<div class="view"></div>
<div id="rest"></div>
<div class="toast hide"></div>
<template id="wraps">
<div class="wrap">
<img class="thumb" loading="lazy" />
<div class="info">
<img class="avatar" loading="lazy" />
<div class="textbox">
<div class="title"></div>
<div class="name"></div>
</div>
<div class="tags"></div>
<div class="like"></div>
</div>
<div class="detail"></div>
<div class="page"></div>
</div>
</template>
<template id="users">
<div class="user">
<div class="profile">
<img class="avatar" loading="lazy" />
<div class="name"></div>
<div class="uid"></div>
<button class="follow"></button>
</div>
<div class="works">
<div class="holder"></div>
<button class="scroll round">></button>
</div>
</div>
</template>
<template name="navbar">
<div class="navbar">
<button class="prev"><</button>
<span class="pages"></span>
<button class="next">></button>
<input type="text" class="pagenum" />
<button class="goto">Go</button>
</div>
</template>
<template id="user">
<div class="view userpage">
<img src="" alt="" class="bg" />
<div class="user">
<img class="avatar" />
<div class="info">
<span class="name"></span>
<span class="uid"></span>
<button class="bookmarked">收藏</button>
<button class="followed">已关注</button>
<button class="follow"></button>
<div class="desc"></div>
</div>
</div>
<slot name="navbar"></slot>
<div class="imgbox nouser"></div>
</div>
</template>
<template id="illust">
<div class="view illustpage">
<div class="illust">
<div class="pics">
<img class="orig" loading="lazy" />
</div>
<div class="info">
<div>
<img class="avatar" />
<span class="name"></span>
<span class="uid"></span>
</div>
<div>
<span class="title"></span>
<span class="id"></span>
<span class="like"></span>
</div>
<div class="desc"></div>
<div class="tags"></div>
<div class="stat"></div>
<div class="date"></div>
</div>
</div>
<slot name="navbar"></slot>
<div class="imgbox"></div>
</div>
</template>
<template id="followed">
<div class="view">
<slot name="navbar"></slot>
<div class="userbox"></div>
</div>
</template>
<template id="bookmarked">
<div class="view">
<slot name="navbar"></slot>
<div class="imgbox"></div>
</div>
</template>
<template id="article">
<li class="article">
<div class="title"></div>
<div class="detail">
<img class="preview" loading="lazy" />
<div class="tags"></div>
</div>
</li>
</template>
</body>
</html>
`;
let scriptText = `"use strict";
//#region
let nel = document.createElement;
nel = nel.bind(document);
nel = (tag, text) => {
let el = document.createElement(tag);
if (text) el.textContent = text;
return el;
};
let \$ = document.querySelector;
\$ = \$.bind(document);
let \$\$ = document.querySelectorAll;
\$\$ = \$\$.bind(document);
function \$x(xpath, el = document) {
let result = document.evaluate(
xpath,
el,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
return Array(result.snapshotLength)
.fill()
.map((_, i) => result.snapshotItem(i));
}
async function parse(url) {
let html = await fetch(url).then(r => r.text());
let parser = new DOMParser();
let doc = parser.parseFromString(html, "text/html");
return doc;
}
function getTemplate(selector) {
let content = \$(selector).content.cloneNode(true).children[0];
let frag = new DocumentFragment();
frag.append(content);
return content;
}
let sleep = ms => new Promise(r => setTimeout(r, ms));
let del = document.documentElement;
let div = nel("div");
let log = console.log;
let range = (s, e) =>
Array(e - s + 1)
.fill()
.map((_, i) => s + i);
function debounce(func, ms = 1000) {
let timeout;
return function () {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, arguments), ms);
};
}
Node.prototype.\$ = function (arg) {
return this.querySelector(arg);
};
Node.prototype.\$\$ = function (arg) {
return this.querySelectorAll(arg);
};
HTMLElement.prototype.\$x = function (xpath) {
return \$x(xpath, this);
};
HTMLElement.prototype.hide = function () {
this.classList.add("hide");
};
HTMLElement.prototype.show = function () {
this.classList.remove("hide");
};
HTMLElement.prototype.toggle = function () {
this.classList.toggle("hide");
};
HTMLElement.prototype.hided = function () {
return this.matches(".hide");
};
Array.prototype.remove = function (el) {
let index = this.indexOf(el);
if (index > -1) this.splice(index, 1);
};
Array.prototype.add = function (el) {
this.remove(el);
this.push(el);
};
class IDB {
db;
constructor(name = "Default") {
this.initing = this.init(name);
}
init(name) {
if (this.initing) return this.initing;
return new Promise((res, rej) => {
let r = indexedDB.open(name, 1);
r.onsuccess = e => {
this.db = e.target.result;
if (this.db.objectStoreNames.contains("k_v")) res(this);
};
r.onupgradeneeded = e => {
this.db = e.target.result;
e.target.transaction.oncomplete = () => res(this);
this.db.createObjectStore("k_v", { keyPath: "k" });
};
r.onerror = e => rej(e);
r.onblocked = e => rej(e);
});
}
exec(f, o) {
return new Promise((res, rej) => {
let t = this.db.transaction(["k_v"], "readwrite");
let s = t.objectStore("k_v");
let r = s[f](o);
r.onsuccess = e => res(e.target.result?.v || e.target.result);
r.onerror = e => rej(e.target.error);
});
}
get(k) {
return this.exec("get", k);
}
put(k, v) {
return this.exec("put", { k, v, t: Date.now() });
}
delete(k) {
return this.exec("delete", k);
}
clear() {
return this.exec("clear");
}
async getAll() {
let results = await this.exec("getAll");
return results.sort((a, b) => b.t - a.t).map(r => r.v);
}
}
//#endregion
let g = {};
g.VisonMode = location.host.includes("pixivision");
let enums = {
size: {
large: "large",
original: "original",
master: "master",
auto: "auto",
zip: "zip",
origzip: "origzip",
},
imgbox: "imgbox",
userbox: "userbox",
};
//toast
{
let timeout;
let toast = \$(".toast");
g.popup = (text, ms = 1000) => {
clearTimeout(timeout);
toast.textContent = text;
toast.classList.remove("hide");
timeout = setTimeout(() => toast.classList.add("hide"), ms);
};
}
//接口
{
g.apis = {
host: "https://www.pixiv.net",
ajax: "https://www.pixiv.net/ajax",
uid: undefined,
async init() {
if (this.inited) return this.inited;
return (this.inited = new Promise(async r => {
let doc = await parse(this.host);
let text = doc.\$('[name="global-data"]').content;
let j = JSON.parse(text);
this.csrf = j.token;
this.uid = j.userData.id;
r();
}));
},
toParam(obj) {
return Object.entries(obj)
.flatMap(([k, v]) =>
v instanceof Array ? v.map(v => \`\${k}[]=\${v}\`).join("&") : \`\${k}=\${v}\`
)
.join("&");
},
async request(path, init) {
let url;
if (path.startsWith("https://")) url = path;
else url = this.ajax + path;
let r = await fetch(url, init);
if (r.status === 429) g.popup("429 Too Many Requests");
if (r.headers.get("content-type").includes("application/json")) {
let j = await r.json();
if (j.error) return log(j.message), j;
return j.body || j;
} else return r.text();
},
async get(path, params) {
return this.request(
path + "?" + this.toParam({ ...params, lang: "zh" }),
{
method: "GET",
}
);
},
async _post(path, body, init) {
if (!this.csrf) await this.init();
return this.request(path, {
method: "POST",
body,
...init,
headers: {
"content-type": "application/json",
"x-csrf-token": this.csrf,
...init?.headers,
},
});
},
async post(path, data, init) {
return this._post(path, JSON.stringify(data), init);
},
async postForm(path, data, init) {
return this._post(path, this.toParam(data), {
headers: {
"content-type": "application/x-www-form-urlencoded",
...init?.headers,
},
...init,
});
},
discovery() {
let path = "/discovery/artworks";
let params = {
mode: "all",
limit: 100,
};
return this.get(path, params);
},
latest(p) {
let path = "/follow_latest/illust";
let params = {
p,
mode: "all",
};
return this.get(path, params);
},
async bookmarks(uid, p) {
let path = \`/user/\${uid}/illusts/bookmarks\`;
let params = {
tag: "",
offset: 100 * (p - 1),
limit: 100,
rest: "show",
};
return this.get(path, params);
},
users() {
let path = "/discovery/users";
let params = {
limit: 100,
};
return this.get(path, params);
},
async following(uid, p) {
let path = \`/user/\${uid}/following\`;
let params = {
offset: 100 * (p - 1),
limit: 100,
rest: "show",
tag: "",
acceptingRequests: 0,
};
return this.get(path, params);
},
user(uid) {
let path = \`/user/\${uid}\`;
let params = {
full: 1,
};
return this.get(path, params);
},
useIds(uid) {
let path = \`/user/\${uid}/profile/all\`;
let params = {
sensitiveFilterMode: "userSetting",
};
return this.get(path, params);
},
userIllusts(uid, ids) {
let path = \`/user/\${uid}/profile/illusts\`;
let params = {
ids,
work_category: "illustManga",
is_first_page: 0,
sensitiveFilterMode: "userSetting",
};
return this.get(path, params);
},
illust(id) {
let path = \`/illust/\${id}\`;
let params = {};
return this.get(path, params);
},
recommendIds(id) {
let path = \`/illust/\${id}/recommend/init\`;
let params = {
limit: 100,
};
return this.get(path, params);
},
recommendIllusts(illust_ids) {
let path = "/illust/recommend/illusts";
let params = {
illust_ids,
};
return this.get(path, params);
},
search(params, p) {
params = { ...params, p, csw: 0 };
let pathname = {
all: "artworks",
illust_and_ugoira: "illustrations",
illust: "illustrations",
manga: "manga",
ugoira: "illustrations",
}[params.type];
let path = \`/search/\${pathname}/\${params.word}\`;
return this.get(path, params);
},
like(id) {
let path = "/illusts/bookmarks/add";
let data = {
illust_id: id,
restrict: 0,
comment: "",
tags: [],
};
return this.post(path, data);
},
unLike(bid) {
let path = "/illusts/bookmarks/delete";
let data = { bookmark_id: bid };
return this.postForm(path, data);
},
follow(uid) {
let url = this.host + "/bookmark_add.php";
let data = {
mode: "add",
type: "user",
user_id: uid,
tag: "",
restrict: 0,
format: "json",
};
return this.postForm(url, data);
},
unFollow(uid) {
let url = this.host + "/rpc_group_setting.php";
let data = {
mode: "del",
type: "bookuser",
id: uid,
};
return this.postForm(url, data);
},
ugoiraMeta(id) {
let path = \`/illust/\${id}/ugoira_meta\`;
let params = {};
return this.get(path, params);
},
getPrompts(keyword) {
let path = this.host + "/rpc/cps.php";
let params = {
keyword,
};
return this.get(path, params);
},
ranking(mode, date, p) {
let path = this.host + "/ranking.php";
let params = {
mode,
date,
p,
format: "json",
};
return this.get(path, params);
},
};
}
//显示
{
let loading = 0;
g.imgHeight = 300;
function formatUrl(u, q = enums.size.large, p = 0, [w, h] = [1920, 1080]) {
let hash = u.match(/\\/img\\/(.*?)_/)?.[1];
if (!hash) return u;
let _p = u.includes("_p") ? \`_p\${p}\` : "";
switch (q) {
case enums.size.large:
return \`https://i.pximg.net/c/600x1200_90_webp/img-master/img/\${hash}\${_p}_master1200.jpg\`;
case enums.size.master:
return \`https://i.pximg.net/img-master/img/\${hash}\${_p}_master1200.jpg\`;
case enums.size.original:
if (!u.includes("_p"))
return \`https://i.pximg.net/img-original/img/\${hash}_ugoira0.jpg\`;
let suffix = u.split(".").pop();
return \`https://i.pximg.net/img-original/img/\${hash}\${_p}.\${suffix}\`;
case enums.size.zip:
return \`https://i.pximg.net/img-zip-ugoira/img/\${hash}_ugoira600x600.zip\`;
case enums.size.origzip:
return \`https://i.pximg.net/img-zip-ugoira/img/\${hash}_ugoira1920x1080.zip\`;
case enums.size.auto:
[w, h] = [(g.imgHeight * w) / h, g.imgHeight];
let sizes = [
"c/100x100",
"c/128x128",
"c/150x150",
"c/240x240",
"c/240x480",
"c/260x260_80",
"c/360x360_70",
"c/400x250_80",
"c/540x540_70",
"c/600x600",
"c/600x1200_90",
"c/768x1200_80",
];
for (let size of sizes) {
let [mw, mh, q] = size.match(/c\\/(\\d+)x(\\d+)(?:_(\\d+))?/).slice(1);
if (q) [mw, mh] = [(mw * q) / 100, (mh * q) / 100];
if (w < mw && h < mh) {
return \`https://i.pximg.net/\${size}/img-master/img/\${hash}\${_p}_master1200.jpg\`;
}
}
return \`https://i.pximg.net/img-master/img/\${hash}\${_p}_master1200.jpg\`;
}
}
let recur = 0;
async function loadNext() {
if (
loading ||
recur > 5 ||
del.scrollTop + del.clientHeight < del.scrollHeight - del.clientHeight
)
return (recur = 0);
if (!g.pageMgr || g.pageMgr.loadend) return;
try {
loading = 1;
await g.pageMgr?.loadNext();
} finally {
loading = 0;
recur++;
await loadNext();
}
}
function resize(wrap) {
let illust = wrap.illust;
let ratio = illust.width / illust.height;
if (isNaN(ratio)) {
let img = wrap.\$(".thumb");
ratio = img.naturalWidth / img.naturalHeight;
}
wrap.style.flexBasis = ratio * g.imgHeight + "px";
wrap.style.flexGrow = ratio;
return wrap;
}
class PageMgr {
constructor(frag, type, finite, getFlex) {
this.\$_ = frag.\$.bind(frag);
this.getFlex = getFlex;
this.finite = finite;
if (type === enums.imgbox) {
this.flexbox = this.\$_(".imgbox");
this.wrapFlex = this.loadImgs;
} else if (type === enums.userbox) {
this.flexbox = this.\$_(".userbox");
this.wrapFlex = this.loadUsers;
}
this.flexbox.replaceChildren();
if (finite) {
this.p = 1;
this.tp = 1;
this.\$_(".navbar").onclick = e => {
let el = e.target;
let np = this.p,
tp = this.tp,
\$_ = this.\$_;
if (el.matches(".prev")) np--;
else if (el.matches(".next")) np++;
else if (el.matches(".pages button:not(.active)"))
np = +el.textContent;
else if (el.matches(".goto")) np = +\$_(".pagenum").value;
else return;
if (isNaN(np) || np < 1 || (tp && np > tp) || np === this.p) return;
this.reset(np);
};
this.\$_(".pagenum").onkeydown = e =>
e.key !== "Enter" || (e.target.blur(), this.\$_(".goto").click());
}
this.loadPage();
}
updateNav() {
let p = this.p,
tp = this.tp,
n = 3;
let pages = this.\$_(".pages");
if (!this.flexbox.children.length) {
let ps = [
...new Set([
...range(1, Math.min(1 + n, p)),
...range(Math.max(1, p - n), Math.min(p + n, tp)),
...range(Math.max(p, tp - n), tp),
]),
];
ps = ps.flatMap((v, i, a) => (v - a[i - 1] > 1 ? ["...", v] : [v]));
pages.replaceChildren(...ps.map(i => nel("button", i)));
} else if (p + n < tp - n) {
let el = nel("button", p + n);
pages.\$x(\`./*[text()='\${p + n - 1}']\`)[0].after(el);
if (p + n === tp - n - 1)
pages.\$x(\`./*[text()='\${p + n}']\`)[0].nextElementSibling.remove();
}
pages.\$x(\`./*[text()='\${p}']\`)[0].classList.add("active");
}
async loadPage() {
let flexs = (await this.getFlex()) || [];
if (!flexs.length) return (this.loadend = 1);
let wraps = this.wrapFlex(flexs);
if (this.finite) this.updateNav();
this.flexbox.append(...wraps);
}
async loadNext() {
if (this.finite && this.p >= this.tp) return (this.loadend = 1);
this.p++;
await this.loadPage();
}
reset(p = 1) {
this.loadend = 0;
this.p = p;
this.flexbox.replaceChildren();
this.loadPage();
}
wrapImg(illust) {
let wrap = getTemplate("#wraps");
wrap.illust = illust;
let \$_ = wrap.\$.bind(wrap);
\$_(".thumb").src = formatUrl(illust.url, enums.size.auto, 0, [
illust.width,
illust.height,
]);
\$_(".avatar").src = illust.profileImageUrl;
g.isFollowed?.(illust.userId)
? \$_(".avatar").classList.add("isfollowed")
: null;
\$_(".avatar").userId = illust.userId;
illust.pageCount > 1
? (\$_(".page").textContent = illust.pageCount)
: null;
\$_(".name").textContent = illust.userName;
\$_(".title").textContent = illust.title;
\$_(".detail").innerText = [
illust.id,
\`\${illust.width}x\${illust.height}\`,
new Date(illust.updateDate).toLocaleDateString(),
].join("\\n");
illust.bookmarkData?.id ? \$_(".like").classList.add("liked") : null;
illust.aiType === 2 ? wrap.classList.add("ai") : null;
illust.xRestrict === 1 ? wrap.classList.add("r18") : null;
\$_(".tags").replaceChildren(...illust.tags.map(tag => nel("span", tag)));
return wrap;
}
loadImgs = illusts => illusts.map(illust => resize(this.wrapImg(illust)));
loadUsers = users =>
users.map(info => {
let user = getTemplate("#users");
let \$_ = user.\$.bind(user);
user.info = info;
\$_(".avatar").userId = info.userId;
\$_(".avatar").src = info.profileImageUrl;
\$_(".name").textContent = info.userName;
\$_(".uid").textContent = info.userId;
info.following ? \$_(".follow").classList.add("isfollowed") : null;
\$_(".works").replaceChildren(
\$_(".holder"),
\$_(".scroll"),
...info.illusts.map(illust => this.wrapImg(illust, 0))
);
return user;
});
}
g.init = {
async discovery(frag) {
g.pageMgr = new PageMgr(frag, enums.imgbox, false, async function () {
let j = await g.apis.discovery();
let illusts = j.thumbnails.illust;
illusts.forEach(illust => (illust.url = illust.urls["1200x1200"]));
return illusts;
});
},
async latest(frag) {
g.pageMgr = new PageMgr(frag, enums.imgbox, true, async function () {
let j = await g.apis.latest(this.p);
this.tp = 34;
let illusts = j.thumbnails.illust;
illusts.forEach(illust => (illust.url = illust.urls["1200x1200"]));
return illusts;
});
},
async users(frag) {
g.pageMgr = new PageMgr(frag, enums.userbox, false, async () => {
let j = await g.apis.users();
j.users.forEach(user => {
let recommendedUser = j.recommendedUsers.find(
recommendedUser => recommendedUser.userId === user.userId
);
user.illusts =
recommendedUser?.recentIllustIds.map(id =>
j.thumbnails.illust.find(illust => illust.id === id)
) || [];
user.illusts.forEach(
illust => (illust.url = illust.urls["1200x1200"])
);
});
j.users.forEach(user => {
user.profileImageUrl = user.imageBig;
user.userName = user.name;
});
return j.users;
});
},
async search(frag) {
let searchinput = \$("#searchinput");
g.pageMgr = new PageMgr(frag, enums.imgbox, true, async function () {
if (!searchinput.value) return [];
let params = Object.fromEntries(new FormData(\$("#searchbar")));
let j = await g.apis.search(params, this.p);
let body =
j[
{
all: "illustManga",
illust_and_ugoira: "illust",
illust: "illust",
manga: "manga",
ugoira: "illust",
}[params.type]
];
this.tp = body.lastPage;
return body.data;
});
searchinput.addEventListener(
"keydown",
e =>
e.key !== "Enter" ||
(searchinput.blur(e.preventDefault()), g.pageMgr.reset())
);
\$("#searchbtn").onclick = e => g.pageMgr.reset(e.preventDefault());
},
async following(frag) {
g.pageMgr = new PageMgr(frag, enums.userbox, true, async function () {
if (!g.apis.uid) await g.apis.init();
let j = await g.apis.following(g.apis.uid, this.p);
this.tp = Math.ceil(j.total / 100);
j.users.forEach(user => (user.isFollowed = user.following));
return j.users;
});
},
async bookmarks(frag) {
g.pageMgr = new PageMgr(frag, enums.imgbox, true, async function () {
if (!g.apis.uid) await g.apis.init();
let j = await g.apis.bookmarks(g.apis.uid, this.p);
this.tp = Math.ceil(j.total / 100);
return j.works;
});
},
async history(frag) {
let p = 0;
g.pageMgr = new PageMgr(frag, enums.imgbox, false, () =>
!p++ ? g.viewed?.getAll() : []
);
},
async user(uid) {
let view = getTemplate("#user");
g.nav.push(view, "?user=" + uid, "用户 - " + uid);
{
let info = await g.apis.user(uid);
let \$_ = view.\$.bind(view);
\$_(".user").info = info;
\$_(".bg").src = info.background?.url;
\$_(".avatar").src = info.imageBig;
\$_(".name").textContent = info.name;
\$_(".uid").textContent = info.userId;
info.isFollowed ? \$_(".follow").classList.add("isfollowed") : null;
\$_(".followed").textContent = "已关注 " + info.following;
\$_(".desc").innerHTML = info.commentHtml;
}
g.pageMgr = new PageMgr(view, enums.imgbox, true, async function () {
if (!this.allIds) {
let j = await g.apis.useIds(uid);
this.allIds = Object.keys(j.illusts).reverse();
this.tp = Math.ceil(this.allIds.length / 100);
}
let offset = (this.p - 1) * 100;
let ids = this.allIds.slice(offset, offset + 100);
if (!ids.length) return [];
let j = await g.apis.userIllusts(uid, ids);
return Object.values(j.works).reverse();
});
},
async illust(id) {
let view = getTemplate("#illust");
g.nav.push(view, "?illust=" + id, "插画 - " + id);
{
let illust = await g.apis.illust(id);
illust.url = illust.urls.original;
view.illust = illust;
let \$_ = view.\$.bind(view);
\$_(".avatar").src = Object.values(illust.userIllusts)
.find(illust => illust?.profileImageUrl)
?.profileImageUrl.replace("_50", "_170");
\$_(".avatar").userId = illust.userId;
\$_(".name").textContent = illust.userName;
\$_(".uid").textContent = illust.userId;
\$_(".pics").replaceChildren(
...Array(illust.pageCount)
.fill()
.map((_, p) => {
let img = \$_(".orig").cloneNode();
img.src = formatUrl(illust.url, enums.size.master, p);
return img;
})
);
illust.illustType === 2
? g
.getUgoiraCanvas(illust.id)
.then(canvas => \$_(".orig").replaceWith(canvas))
: null;
illust.bookmarkData?.id ? \$_(".like").classList.add("liked") : null;
\$_(".title").textContent = illust.illustTitle;
\$_(".id").textContent = illust.illustId;
\$_(".desc").innerHTML = illust.description;
\$_(".tags").replaceChildren(
...illust.tags.tags.flatMap(tag => [
nel("span", tag.tag),
nel("small", tag.translation?.en),
])
);
\$_(".stat").textContent = \`\${illust.isOriginal ? "原创" : ""} \${
illust.width
}×\${illust.height} 🖒\${illust.likeCount} ♡\${illust.bookmarkCount} 👁︎\${
illust.viewCount
}\`;
\$_(".date").textContent = new Date(illust.uploadDate).toLocaleString();
}
let onwheel = e => {
if (e.deltaY > 0 && view.parentElement) {
document.removeEventListener("wheel", onwheel);
g.pageMgr = new PageMgr(view, enums.imgbox, true, async function () {
let j;
if (this.p === 1) {
j = await g.apis.recommendIds(id);
this.allIds = j.nextIds;
this.tp = Math.ceil(this.allIds.length / 100 + 1);
} else {
let offset = (this.p - 2) * 100;
let ids = this.allIds.slice(offset, offset + 100);
if (!ids.length) return [];
j = await g.apis.recommendIllusts(ids);
}
return j.illusts;
});
}
};
document.addEventListener("wheel", onwheel);
},
async followed(uid) {
let view = getTemplate("#followed");
g.nav.push(view, "?followed=" + uid, uid + " 的关注");
g.pageMgr = new PageMgr(view, enums.userbox, true, async function () {
let j = await g.apis.following(uid, this.p);
this.tp = Math.ceil(j.total / 100);
j.users.forEach(user => (user.isFollowed = user.following));
return j.users;
});
},
async bookmarked(uid) {
let view = getTemplate("#bookmarked");
g.nav.push(view, "?bookmarked=" + uid, uid + " 的收藏");
g.pageMgr = new PageMgr(view, enums.imgbox, true, async function () {
let j = await g.apis.bookmarks(uid, this.p);
this.tp = Math.ceil(j.total / 100);
return j.works;
});
},
async ranking(frag) {
let \$_ = frag.\$.bind(frag);
\$_("[value='daily']").checked = true;
\$_("[value='_r18']").onclick = e => {
\$_("[value='monthly']").disabled =
\$_("[value='rookie']").disabled =
\$_("[value='original']").disabled =
e.target.checked;
if (
e.target.checked &&
(\$_("[value='monthly']").checked ||
\$_("[value='rookie']").checked ||
\$_("[value='original']").checked)
)
\$_("[value='daily']").checked = true;
};
let maxDate = new Date();
maxDate.setDate(maxDate.getDate() - 1);
\$_("[name='date']").value = \$_("[name='date']").max = maxDate
.toISOString()
.split("T")[0];
\$_(".menu").onclick = e => {
let el = e.target;
let i;
if (el.matches(".prev")) i = -1;
else if (el.matches(".next")) i = 1;
if (!i) return;
let date = new Date(\$_("[name='date']").value);
date.setDate(date.getDate() + i);
if (date >= maxDate) return;
\$_("[name='date']").value = date.toISOString().split("T")[0];
g.pageMgr.reset();
};
frag.\$\$("input").forEach(el => (el.onchange = () => g.pageMgr.reset()));
g.pageMgr = new PageMgr(frag, enums.imgbox, true, async function () {
let mode =
\$_("[name='mode']:checked").value +
(\$_("[value='_r18']").checked ? "_r18" : "");
mode = mode.replace("daily_ai_r18", "daily_r18_ai");
let date = \$_("[name='date']").value.replaceAll("-", "");
let j = await g.apis.ranking(mode, date, this.p);
this.tp = Math.ceil(j.rank_total / 50);
j.contents.forEach(illust => {
illust.id = illust.illust_id;
illust.userId = illust.user_id;
illust.userName = illust.user_name;
illust.pageCount = illust.illust_page_count;
illust.updateDate = illust.illust_upload_timestamp * 1000;
illust.profileImageUrl = illust.profile_img;
});
return j.contents;
});
},
async vision(frag) {
let p = 1;
let currLi;
let loadingIllusts = 0;
let loadingArticles = 0;
let imgbox = frag.\$(".imgbox");
let articlebox = frag.\$("#articlebox");
async function getArticles() {
let doc = await parse(
\`https://www.pixivision.net/zh/c/illustration/?p=\${p}\`
);
return [...doc.\$\$(".article-card-container")]
.map(el => ({
link: el.\$(".arc__title a").getAttribute("href"),
title: el.\$(".arc__title a").textContent,
preview: el
.\$("._thumbnail")
.style.cssText.match(/url\\("(.*?)"\\)/)[1],
tags: [...el.\$\$("._tag-list a")].map(el => ({
link: el.getAttribute("href"),
name: el.textContent,
})),
}))
.filter(article => !article.title.includes("大合辑"));
}
async function loadArticles() {
let articles = await getArticles();
let lis = articles.map(article => {
let li = getTemplate("#article");
li.article = article;
li.\$(".title").textContent = article.title;
li.\$(".preview").src = article.preview;
li.\$(".tags").replaceChildren(
...article.tags.map(tag => nel("span", tag.name))
);
return li;
});
frag.\$("#articlebox").append(nel("div", \`第\${p}页\`), ...lis);
}
async function loadNextArticles() {
if (
!loadingArticles &&
articlebox.scrollTop + 2 * articlebox.clientHeight >=
articlebox.scrollHeight
) {
loadingArticles = 1;
await loadArticles();
p++;
loadingArticles = 0;
loadNextArticles();
}
}
async function getIllusts(link) {
let doc = await parse("https://www.pixivision.net" + link);
return [...doc.\$\$(".am__work")].map(el => ({
userId: el.\$(".am__work__user-name a").href.match(/\\d+/)[0],
userName: el.\$(".am__work__user-name a").textContent,
profileImageUrl: el.\$(".am__work__uesr-icon").src,
id: el.\$(".am__work__title a").href.match(/\\d+/)[0],
url: el.\$(".am__work__illust").src,
title: el.\$(".am__work__title a").textContent,
pageCount: +el.\$(".mic__label")?.textContent || 1,
}));
}
async function loadIllusts(article) {
return new Promise(async r => {
let illusts = await getIllusts(article.link);
if (!illusts.length) r();
illusts.forEach(illust => {
loadingIllusts++;
let wrap = getTemplate("#wraps");
wrap.illust = illust;
let \$_ = wrap.\$.bind(wrap);
\$_(".thumb").loading = "eager";
\$_(".thumb").onload = () => {
imgbox.append(resize(wrap));
--loadingIllusts || r();
};
\$_(".thumb").onerror = () => --loadingIllusts || r();
\$_(".thumb").src = illust.url;
\$_(".avatar").src = illust.profileImageUrl;
\$_(".avatar").userId = illust.userId;
illust.pageCount > 1
? (\$_(".page").textContent = illust.pageCount)
: null;
\$_(".name").textContent = illust.userName;
\$_(".title").textContent = illust.title;
\$_(".detail").innerText = illust.id;
\$_(".tags").replaceChildren(
...article.tags.map(tag => nel("span", tag.name)),
nel(
"span",
article.title
.match(/(?: - |!|。|?|♡)+(.*?)(?:插画)?特辑/)?.[1]
.replaceAll(/“|”| - /g, "")
)
);
});
});
}
async function loadNextIllusts() {
let li = currLi.nextElementSibling;
if (li && !li.matches(".article")) li = li.nextElementSibling;
if (!li) return await loadArticles();
currLi = li;
li.classList.add("active");
g.popup(li.article.title, 3000);
await loadIllusts(li.article);
}
frag.\$("#articlebox").onclick = async e => {
let li = e.target.closest(".article");
if (li) {
frag.\$("#rightbar").hide();
frag
.\$\$(".article.active")
.forEach(el => el.classList.remove("active"));
currLi = li;
li.classList.add("active");
imgbox.replaceChildren();
await loadIllusts(li.article);
loadNext();
} else frag.\$("#rightbar").classList.toggle("hide");
};
function jump() {
p = +frag.\$(".pagenum").value;
articlebox.replaceChildren();
loadNextArticles();
}
frag.\$(".pagenum").onkeydown = e => e.key !== "Enter" || jump();
frag.\$(".goto").onclick = () => jump();
g.pageMgr = {
loadNext: loadNextIllusts,
flexbox: frag.\$(".imgbox"),
};
articlebox.onscroll = () => loadNextArticles();
loadNextArticles();
frag.\$("#rightbtn").onclick = () => frag.\$("#rightbar").show();
document.addEventListener("keydown", e => {
if (e.key === "CapsLock") frag.\$("#rightbar").toggle();
});
frag.\$("#closeright").onclick = () => frag.\$("#rightbar").hide();
if (g.VisonMode)
imgbox.addEventListener("click", async e => {
let el = e.target;
let host = "https://www.pixiv.net/wtf";
if (el.matches(".like")) {
let id = el.closest(".wrap,.view").illust.id;
open(host + \`/?illust=\${id}\`, "_blank");
} else if (el.matches(".thumb")) {
let id = el.closest(".wrap").illust.id;
open(host + \`/?illust=\${id}\`, "_blank");
} else if (el.matches(".avatar")) {
let uid = el.userId;
open(host + \`/?user=\${uid}\`, "_blank");
}
e.stopPropagation();
});
},
};
document.addEventListener("click", async e => {
let el = e.target;
if (el.matches(".like")) {
let illust = el.closest(".wrap,.view").illust;
if (!illust.bookmarkData?.id) {
let j = await g.apis.like(illust.id);
if (!j.error)
illust.bookmarkData = {
id: j.last_bookmark_id,
};
el.classList.add("liked");
} else {
let j = await g.apis.unLike(illust.bookmarkData.id);
if (!j.error) delete illust.bookmarkData.id;
el.classList.remove("liked");
}
} else if (el.matches(".scroll")) {
let works = el.closest(".works");
works.scrollTo({ left: works.scrollWidth, behavior: "smooth" });
} else if (el.matches(".follow")) {
let info = el.closest(".user").info;
if (info.isFollowed) {
let j = await g.apis.unFollow(info.userId);
info.isFollowed = false;
g.followed.delete(info.userId);
el.classList.remove("isfollowed");
} else {
let j = await g.apis.follow(info.userId);
info.isFollowed = true;
g.followed.add(info.userId);
el.classList.add("isfollowed");
}
} else if (el.matches(".avatar")) {
let uid = el.userId;
if (!uid) return;
g.init.user(uid);
} else if (el.matches(".followed")) {
let uid = el.closest(".user").info.userId;
g.init.followed(uid);
} else if (el.matches(".bookmarked")) {
let uid = el.closest(".user").info.userId;
g.init.bookmarked(uid);
} else if (el.matches(".thumb")) {
let illust = el.closest(".wrap").illust;
let id = illust.id;
g.viewed.put(id, illust);
g.init.illust(id);
} else if (el.matches("span") && el.closest(".tags")) {
if (\$("#searchinput").closest(".hide")) return;
\$("#searchinput").value += " " + el.textContent;
}
});
document.addEventListener("auxclick", e => {
if (e.button === 3) g.nav.back();
if (e.button === 4) g.nav.forward();
});
document.addEventListener("mouseup", e => {
if (e.button === 3 || e.button === 4) e.preventDefault();
});
document.addEventListener("keydown", e => {
if (e.ctrlKey && e.key === "f") \$("#searchinput").focus(e.preventDefault());
});
window.onresize = () => loadNext();
document.onscroll = () => loadNext();
\$\$("slot").forEach(
slot => (slot.outerHTML = \$(\`template[name="\${slot.name}"]\`)?.innerHTML)
);
\$\$("template").forEach(tpl =>
tpl.content
.\$\$("slot")
.forEach(
slot => (slot.outerHTML = \$(\`template[name="\${slot.name}"]\`)?.innerHTML)
)
);
g.formatUrl = formatUrl;
g.resize = resize;
}
//侧栏
{
g.nav = {
layers: [],
states: [],
index: 0,
cover(el) {
this.layers.at(-1)?.hide();
el.show();
this.layers.add(el);
},
uncover() {
if (this.layers.length === 0) return;
let el = this.layers.pop();
el.hide();
this.layers.at(-1)?.show();
},
getState() {
return {
view: \$(".view"),
scrollTop: del.scrollTop,
pageMgr: g.pageMgr,
title: document.title,
path: (location.search || "?") + location.hash,
};
},
setState(state) {
\$(".view").replaceWith(state.view);
del.scrollTop = state.scrollTop;
g.pageMgr = state.pageMgr;
document.title = state.title;
history.replaceState(null, null, state.path);
},
switch(frag, path, title) {
this.clear();
let lastFrag = \$(".frag:not(.hide)");
if (frag === lastFrag) return (del.scrollTop = lastFrag.lastScroll);
lastFrag.lastScroll = del.scrollTop || lastFrag.lastScroll || 0;
lastFrag.pageMgr = g.pageMgr;
lastFrag.hide();
g.pageMgr = frag.pageMgr;
frag.show();
del.scrollTop = frag.lastScroll;
document.title = title;
history.replaceState(null, null, path);
},
push(view, path, title) {
this.states.splice(this.index, this.states.length, this.getState());
if (this.index === 0)
(\$(".frag:not(.hide)").lastScroll = del.scrollTop), \$("#frags").hide();
this.setState({
view,
title,
path,
});
this.index++;
},
back() {
if (this.index === 0) return;
this.states[this.index] = this.getState();
if (this.index === 1) \$("#frags").show();
this.setState(this.states[--this.index]);
},
forward() {
if (this.index >= this.states.length - 1) return;
this.states[this.index] = this.getState();
if (this.index === 0) \$("#frags").hide();
this.setState(this.states[++this.index]);
},
clear() {
if (!this.states.length) return;
this.index = 0;
this.setState(this.states[this.index]);
this.states = [];
\$("#frags").show();
},
};
let leftbar = \$("#leftbar");
\$("#leftbtn").onclick = () => leftbar.show();
\$("#closeleft").onclick = () => leftbar.hide();
\$("#back").onclick = () => g.nav.back();
\$("#forward").onclick = () => g.nav.forward();
\$("#top").onclick = () => del.scrollTo({ behavior: "smooth", top: 0 });
\$("#end").onclick = () =>
del.scrollTo({ behavior: "smooth", top: del.scrollHeight });
\$("#refresh").onclick = () => (
leftbar.hide(), g.init[\$(".tab.active").name](\$(".frag:not(.hide)"))
);
\$("#tabs").onclick = e => {
let tab = e.target;
if (!tab.matches(".tab")) return;
leftbar.hide();
\$(".tab.active")?.classList.remove("active");
tab.classList.add("active");
let name = tab.name;
let frag = \$(\`.frag[name="\${name}"]\`);
g.nav.switch(frag, "?#" + name, "pixiv - " + tab.textContent);
if (!frag.init || name === "history") frag.init = (g.init[name](frag), 1);
};
\$(\`.tab[name="\${location.hash.slice(1)}"\`)?.click();
document.addEventListener("keydown", e => {
if (e.key === "Escape") {
if (g.nav.layers.length) g.nav.uncover();
else if (g.nav.index > 0) g.nav.back();
} else if (e.key === "Tab") {
if (document.activeElement === document.body)
leftbar.toggle(e.preventDefault());
} else if (e.altKey && e.key === "ArrowLeft")
g.nav.back(e.preventDefault());
else if (e.altKey && e.key === "ArrowRight")
g.nav.forward(e.preventDefault());
});
let params = location.search.slice(1).split("=");
history.replaceState(null, null, params[0] ? "?" : "");
g.init[params[0]]?.(params[1]);
if (!location.search && !location.hash) leftbar.show();
//图高
{
let heightinput = \$("#heightinput");
heightinput.oninput = () =>
(\$("#heightvalue").textContent = heightinput.value);
heightinput.onchange = () => {
g.imgHeight = +heightinput.value;
del.style.setProperty("--height", heightinput.value + "px");
localStorage.setItem("height", heightinput.value);
if (g.pageMgr?.flexbox?.matches(".imgbox"))
\$\$(".wrap").forEach(wrap => g.resize(wrap));
};
heightinput.value = localStorage.getItem("height") || g.imgHeight;
heightinput.oninput();
heightinput.onchange();
}
}
//缩放
{
g.viewed = new IDB("viewd");
g.fails = [];
let cover = \$("#cover");
let zoom = \$("#zoom");
function toggleZoom(src) {
if (g.fails.includes(src)) zoom.src = src.replace(".jpg", ".png");
else zoom.src = src;
zoom.onerror = () => {
g.fails.push(zoom.src);
let src = zoom.src.replace(".jpg", ".png");
if (zoom.src !== src) zoom.src = src;
};
zoom.style = "";
zoom.scale = 1;
g.nav.cover(cover);
}
function zoomImg(e) {
e.preventDefault();
let scale = zoom.scale * (e.deltaY < 0 ? 1.25 : 0.8);
if (scale < 0.8 || scale > 4) return;
else zoom.scale = scale;
zoom.style.transform = \`scale(\${zoom.scale})\`;
moveImg(e);
}
function moveImg(e) {
let t,
l,
br = 0.8,
ih = zoom.clientHeight * zoom.scale,
iw = zoom.clientWidth * zoom.scale,
dh = del.clientHeight,
dw = del.clientWidth;
if (ih > dh + 1) t = (br * dh - ih) * (e.clientY / dh - 0.5);
else t = 0;
if (iw > dw + 1) l = (br * dw - iw) * (e.clientX / dw - 0.5);
else l = 0;
zoom.style.translate = \`\${l}px \${t}px 0px\`;
}
//切换
{
let wrap, p;
function navZoom(forward) {
let illust = wrap?.illust;
if (!illust) return;
if (forward) {
if (p < illust.pageCount - 1) p++;
else if (wrap.matches(".view")) return;
else {
wrap = wrap.nextElementSibling;
if (!wrap) return;
illust = wrap.illust;
p = 0;
}
} else {
if (p > 0) p--;
else if (wrap.matches(".view")) return;
else {
wrap = wrap.previousElementSibling;
if (!wrap) return;
illust = wrap.illust;
p = illust.pageCount - 1;
}
}
toggleZoom(g.formatUrl(illust.url, enums.size.original, p));
g.viewed.put(illust.id, illust);
}
document.addEventListener("contextmenu", e => {
let el = e.target;
if (el.matches(".thumb")) {
e.preventDefault();
wrap = el.closest(".wrap");
p = 0;
let illust = wrap.illust;
toggleZoom(g.formatUrl(illust.url, enums.size.original, p));
g.viewed.put(illust.id, illust);
} else if (el.matches(".orig")) {
e.preventDefault();
wrap = el.closest(".view");
let illust = wrap.illust;
p = el.src.match(/_p(\\d+)_/)?.[1] || 0;
toggleZoom(g.formatUrl(illust.url, enums.size.original, p));
}
});
document.addEventListener("keydown", e => {
if (!cover.hided()) {
if (["ArrowRight", "d", "D"].includes(e.key)) navZoom(1);
else if (["ArrowLeft", "a", "A"].includes(e.key)) navZoom(0);
}
});
cover.onwheel = e => zoomImg(e);
cover.onmousemove = e => moveImg(e);
cover.onclick = e => {
let btn = e.target;
if (btn.matches(".next")) navZoom(1);
else if (btn.matches(".prev")) navZoom(0);
else g.nav.uncover();
};
}
}
//关注
{
(async () => {
if (g.VisonMode) return;
let idb = new IDB();
window.idb = idb;
await idb.init();
let followed = await idb.get("followed");
if (!followed) {
followed = new Set();
let p = 1;
let tp;
await g.apis.init();
do {
let j = await g.apis.following(g.apis.uid, p);
tp = Math.ceil(j.total / 100);
j.users.forEach(u => followed.add(u.userId));
} while (p++ < tp);
await idb.put("followed", followed);
}
let commit = debounce(() => {
idb.put("followed", followed);
});
followed.add = function (uid) {
Set.prototype.add.call(this, uid);
commit();
};
followed.delete = function (uid) {
Set.prototype.delete.call(this, uid);
commit();
};
g.followed = followed;
g.isFollowed = uid => followed.has(uid);
})();
}
//动图
{
async function unzip(buffer) {
let dv = new DataView(buffer);
let offset = 0;
while (
dv.getUint32(offset, true) !== 0x06054b50 &&
offset < buffer.byteLength
)
offset++;
let fileCount = dv.getUint16(offset + 10, true);
offset = dv.getUint32(offset + 16, true);
let files = [];
let decoder = new TextDecoder();
for (let _ of Array(fileCount)) {
let fileOffset = dv.getUint32(offset + 42, true) + 40;
let zipedSize = dv.getUint32(offset + 20, true);
files.push({
name: decoder.decode(buffer.slice(offset + 46, offset + 56)),
blob: new Blob([buffer.slice(fileOffset, fileOffset + zipedSize)]),
});
offset += dv.getUint16(offset + 32, true) + 56;
}
return files;
}
g.getUgoiraCanvas = async id => {
let canvas = nel("canvas");
let j = await g.apis.ugoiraMeta(id);
let frames = j.frames;
let zip = await fetch(j.originalSrc).then(r => r.arrayBuffer());
let files = await unzip(zip);
await Promise.all(
frames.map(
async (f, i) => (f.image = await createImageBitmap(files[i].blob))
)
);
canvas.height = frames[0].image.height;
canvas.width = frames[0].image.width;
let ctx = canvas.getContext("2d");
let intersecting = 1,
visible = 1,
pause = 0,
index = 0,
looping = 0;
async function render() {
if (looping) return;
looping = 1;
while (intersecting && visible && !pause) {
index = index >= frames.length - 1 ? 0 : index + 1;
let frame = frames[index];
ctx.drawImage(frame.image, 0, 0);
await sleep(frame.delay);
}
looping = 0;
}
canvas.onclick = async () => render((pause = !pause));
let obs = new IntersectionObserver(es =>
render((intersecting = es.at(-1).isIntersecting))
);
obs.observe(canvas);
document.onvisibilitychange = () =>
render((visible = document.visibilityState === "visible"));
render();
return canvas;
};
}
//补全
{
let searchinput = \$("#searchinput");
let ul = \$("#prompts");
let lastWord = "";
let index = 0;
function getSE() {
let text = searchinput.value;
let start = searchinput.selectionStart;
let end = searchinput.selectionEnd;
while (start > 0 && !' |"'.includes(text[start - 1])) start--;
while (end < text.length && !' |"'.includes(text[end])) end++;
return [start, end];
}
let updatePrompt = debounce(async word => {
let j = await g.apis.getPrompts(word);
let tags = j.candidates;
ul.replaceChildren(
...tags.map(tag => {
let li = nel("li");
li.replaceChildren(
nel("span", tag.tag_name),
nel("small", tag.tag_translation)
);
return li;
})
);
setFocus((index = 0));
}, 250);
let autocomplete = () => {
let [start, end] = getSE();
let word = searchinput.value.slice(start, end);
if (word === lastWord) return;
lastWord = word;
if (!word) return ul.replaceChildren();
updatePrompt(word);
};
function setFocus() {
index = Math.max(0, Math.min(index, ul.children.length - 1));
let li = ul.children[index];
if (!li) return;
ul.\$(".focus")?.classList.remove("focus");
li.classList.add("focus");
}
function select() {
let tag = ul.\$(".focus span").textContent;
let text = searchinput.value;
let [start, end] = getSE();
searchinput.value = text.slice(0, start) + tag + (text.slice(end) || " ");
let cursor = start + tag.length + (text.slice(end) ? 0 : 1);
searchinput.setSelectionRange(cursor, cursor);
searchinput.oninput();
}
searchinput.onclick = () => autocomplete();
searchinput.onfocus = () => autocomplete();
searchinput.onblur = () => ul.replaceChildren();
searchinput.oninput = () => {
autocomplete();
searchinput.style.width = "";
searchinput.style.width =
Math.max(200, searchinput.scrollWidth + 10) + "px";
};
searchinput.addEventListener("keydown", e => {
if (e.key === "ArrowLeft") autocomplete();
else if (e.key === "ArrowRight") autocomplete();
else if (e.key === "ArrowUp" || (e.ctrlKey && e.key === " " && e.shiftKey))
setFocus(index--);
else if (e.key === "ArrowDown" || (e.ctrlKey && e.key === " "))
setFocus(index++);
else if (e.key === "Tab") select(e.preventDefault());
else if (e.key === "Escape") searchinput.blur();
else if (e.ctrlKey && e.key === "d")
searchinput.setSelectionRange(...getSE(e.preventDefault()));
});
ul.onmousedown = e => {
let li = e.target.closest("li");
if (!li) return;
e.preventDefault();
index = [...ul.children].indexOf(e.target);
setFocus();
select();
};
}
//vision
{
let visiontab = \$('[name="vision"].tab');
if (g.VisonMode) {
\$\$("button.tab").forEach(tab => {
if (tab !== visiontab)
tab.onclick = e => {
location.href = "https://www.pixiv.net/wtf/#" + tab.name;
e.stopPropagation();
};
});
} else
visiontab.onclick = e => {
e.stopPropagation();
location.href = "https://www.pixivision.net/zh/wtf/#vision";
};
}
//nwjs
chrome.webRequest?.onBeforeSendHeaders.addListener(
details => ({
requestHeaders: [
...details.requestHeaders,
{
name: "Referer",
value: "https://www.pixiv.net",
},
],
}),
{ urls: ["https://i.pximg.net/*", "https://www.pixiv.net/*"] },
["blocking", "requestHeaders", "extraHeaders"]
);
`;
let cssText = `:root {
color-scheme: dark;
--height: 300px;
--light: rgb(255 255 255 /0.5);
--dark: rgb(0 0 0 /0.5);
--border: white solid medium;
}
* {
border-radius: 5px;
gap: 5px;
box-sizing: border-box;
}
body {
margin: 0;
background: black;
color: white;
overflow-y: scroll;
text-align: center;
.frag {
#searchbar {
justify-content: unset;
#searchbox {
position: relative;
#searchinput {
max-width: 50vw;
}
#prompts {
position: absolute;
background: var(--dark);
width: 200px;
top: 100%;
left: 0;
margin: 0;
padding: 0;
& li {
display: flex;
cursor: pointer;
border: transparent solid medium;
justify-content: space-between;
&:hover {
border: var(--border);
}
&.focus {
border: var(--border);
}
}
}
}
}
&[name="search"] .navbar,
&[name="ranking"] .navbar {
z-index: 1;
position: fixed;
top: unset;
}
.menu {
position: sticky;
user-select: none;
top: 0;
z-index: 2;
background: var(--dark);
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
&[name="vision"] {
#rightbtn {
bottom: 25px;
right: 25px;
}
#rightbar {
right: 0;
width: 300px;
backdrop-filter: blur(10px);
&.hide {
display: unset;
visibility: hidden;
}
#articlebox {
padding-left: 100vw;
margin-left: -100vw;
padding-bottom: 50vh;
height: calc(100% - 75px);
pointer-events: none;
overflow-y: scroll;
& > * {
pointer-events: initial;
}
.article {
cursor: pointer;
position: relative;
border: 2px solid transparent;
&:hover {
border-color: white;
.detail {
display: block;
}
}
.detail {
z-index: 1;
position: absolute;
background: var(--dark);
display: none;
top: 0;
right: 100%;
.preview {
width: 600px;
}
.tags {
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
& span {
border: 2px solid var(--light);
&:hover {
border-color: white;
}
padding: 0 5px;
}
}
}
&.active {
background: var(--light);
}
.title {
text-align: left;
}
}
&::after {
pointer-events: initial;
content: "";
height: 100%;
position: absolute;
top: 0;
right: 100%;
width: 100vw;
}
}
.pages {
text-align: left;
margin: 20px;
.pagenum {
width: 50px;
}
}
#closeright {
right: 25px;
bottom: 25px;
}
}
}
}
.sidebar {
width: 200px;
padding: 10px;
height: 100vh;
position: fixed;
top: 0;
z-index: 3;
background: var(--dark);
display: flex;
flex-direction: column;
gap: 10px;
}
#leftbar {
left: 0;
&.hide {
display: none;
}
#tabs {
display: contents;
.split {
border: var(--border);
}
.tab {
height: 50px;
cursor: pointer;
border: var(--border);
}
}
#refresh {
bottom: 25px;
left: 100px;
}
#closeleft {
bottom: 25px;
left: 25px;
}
}
#leftbtn {
bottom: 25px;
left: 25px;
}
#forward {
bottom: 25px;
left: 175px;
}
#back {
left: 100px;
bottom: 25px;
}
#top {
right: 25px;
bottom: 175px;
}
#end {
right: 25px;
bottom: 100px;
}
#cover {
position: fixed;
z-index: 4;
width: 100vw;
height: 100vh;
background: var(--dark);
left: 0;
top: 0;
display: grid;
place-items: center;
&.hide {
display: none;
}
#zoom {
transform: translateZ(0);
object-fit: contain;
height: 100vh;
width: 100vw;
}
.prev {
left: 20px;
}
.next {
right: 20px;
}
}
#rest {
position: fixed;
top: 0;
height: 100vh;
width: 1px;
z-index: 1;
}
.imgbox {
display: flex;
align-content: start;
flex-wrap: wrap;
line-height: 0;
gap: 20px;
&::after {
content: "";
flex-grow: 114514;
}
&.nouser {
.avatar {
display: none;
}
.name {
display: none;
}
}
}
.userbox {
display: flex;
flex-direction: column;
gap: 20px;
.user {
position: relative;
.profile {
width: 100px;
z-index: 1;
position: absolute;
background: var(--dark);
word-break: break-all;
.avatar {
width: 100%;
cursor: pointer;
}
}
.works {
display: flex;
gap: 20px;
overflow-x: scroll;
.holder {
width: 100px;
flex-shrink: 0;
height: var(--height);
}
&::-webkit-scrollbar {
display: none;
}
.wrap {
.thumb {
width: unset;
height: var(--height);
}
.avatar {
display: none;
}
.name {
display: none;
}
}
&:hover .scroll {
display: inline-flex;
}
.scroll {
display: none;
position: absolute;
z-index: 1;
top: calc(50% - 25px);
right: 50px;
opacity: 0.5;
&:hover {
opacity: 1;
background: var(--dark);
}
&:active {
opacity: 0.5;
}
}
}
}
}
.wrap {
position: relative;
line-height: normal;
min-height: var(--height);
&.ai {
border: thin solid cyan;
}
&.r18 {
border: thin solid pink;
}
&.ai.r18 {
border-color: cyan pink pink cyan;
}
.thumb {
width: 100%;
cursor: pointer;
}
&:hover > .info {
display: flex;
}
&:hover > .detail {
display: block;
}
.info {
display: none;
position: absolute;
bottom: 0;
width: 100%;
padding: 5px;
background: var(--dark);
.avatar {
align-self: end;
cursor: pointer;
width: 50px;
height: 50px;
border-radius: 25px;
&.isfollowed {
border: thin solid yellow;
}
}
.textbox {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.tags {
position: absolute;
left: 0;
bottom: 100%;
pointer-events: none;
margin-bottom: 5px;
display: flex;
flex-wrap: wrap-reverse;
& span {
pointer-events: initial;
overflow: hidden;
max-width: 200px;
white-space: nowrap;
padding: 0 5px;
background: var(--dark);
border-radius: 20px;
cursor: pointer;
border: transparent solid 2px;
&:hover {
border-color: white;
}
}
}
}
.detail {
display: none;
position: absolute;
top: 0;
left: 0;
border-radius: 5px 0;
background: var(--dark);
}
.page {
border-radius: 0 5px;
background: var(--dark);
position: absolute;
top: 0;
right: 0;
}
}
.view {
&.illustpage {
.illust {
display: flex;
.pics {
max-width: 80%;
max-height: 100vh;
overflow: scroll;
display: flex;
flex-direction: column;
gap: 20px;
.orig {
max-height: 100vh;
object-fit: contain;
}
}
.info {
flex: 1;
display: flex;
flex-direction: column;
padding: 25px;
.avatar {
width: 100px;
border-radius: 50px;
cursor: pointer;
}
.title {
font-size: xx-large;
}
.like {
position: unset;
}
.tags {
display: flex;
flex-wrap: wrap;
& span {
padding: 0 5px;
border-radius: 20px;
cursor: pointer;
border: white solid 2px;
&:hover {
background: var(--light);
}
}
}
}
}
}
&.userpage {
.user {
display: flex;
.avatar {
object-fit: contain;
object-position: top;
}
.info {
flex: 1;
}
}
}
}
.follow {
&::before {
content: "关注";
}
&.isfollowed {
&::before {
content: "取消关注";
color: red;
}
border-color: red;
}
}
.like {
cursor: pointer;
position: absolute;
bottom: 0;
right: 5px;
margin-bottom: 0px;
user-select: none;
&:active {
opacity: 0.5;
}
&::before {
content: "♡";
font-size: xx-large;
}
&.liked {
&::before {
content: "♥";
color: red;
}
}
}
.navbar {
position: sticky;
top: 0;
width: 100%;
z-index: 2;
display: flex;
justify-content: center;
background: var(--dark);
.pages {
display: contents;
}
.pagenum {
width: 30px;
}
}
.round {
position: fixed;
height: 50px;
width: 50px;
border-radius: 50px;
font-size: xx-large;
cursor: pointer;
}
.float {
z-index: 2;
opacity: 0.1;
&:hover {
background: var(--dark);
opacity: 1;
}
&:active {
opacity: 0.5;
}
}
.hide {
display: none;
}
.toast {
position: fixed;
background: var(--dark);
z-index: 1;
bottom: 100px;
left: 50%;
translate: -50% 0;
font-size: xx-large;
text-align: center;
}
}
select {
background: black;
color: white;
border: var(--border);
cursor: pointer;
outline: none;
&:hover {
background: var(--light);
}
}
option {
background: black;
}
button,
[type="submit"],
label:has([type="radio"]) {
display: inline-flex;
justify-content: center;
align-items: center;
color: white;
outline: none;
user-select: none;
background: var(--dark);
border: var(--border);
&:hover {
cursor: pointer;
background: var(--light);
}
&:active {
opacity: 0.5;
}
&.active {
background: var(--light);
}
}
[type="text"] {
outline: none;
background: var(--dark);
border: var(--border);
}
[type="date"] {
background: black;
border: var(--border);
font-family: "Microsoft YaHei";
outline: none;
width: 110px;
height: 1.6em;
&::-webkit-datetime-edit-fields-wrapper {
cursor: text;
}
&::-webkit-datetime-edit-text {
cursor: initial;
}
&::-webkit-datetime-edit-month-field:focus {
background: white;
color: black;
}
&::-webkit-datetime-edit-day-field:focus {
background: white;
color: black;
}
&::-webkit-datetime-edit-year-field:focus {
background: white;
color: black;
}
&::-webkit-calendar-picker-indicator {
cursor: pointer;
}
}
[type="range"] {
appearance: none;
border-radius: 15px;
border: var(--border);
background: black;
&::-webkit-slider-thumb {
appearance: none;
cursor: pointer;
width: 15px;
height: 15px;
border-radius: 50%;
background: white;
}
}
[type="checkbox"] {
cursor: pointer;
appearance: none;
border: var(--border);
width: 1.6em;
height: 1.6em;
display: inline-flex;
place-content: center;
align-items: center;
&::after {
content: "✔";
font-weight: bold;
color: transparent;
}
&:checked {
&::after {
color: white;
}
}
}
label:has([type="checkbox"]) {
cursor: pointer;
}
label:has([type="radio"]) {
padding: 0 5px;
&:has(:checked) {
background: var(--light);
}
&:has(:disabled) {
opacity: 0.5;
}
}
[type="radio"] {
display: none;
}
::selection {
background: white;
color: black;
}
ul {
padding: 0;
margin: 0;
}
li {
list-style: none;
}
`;
document.documentElement.innerHTML = htmlText;
addStyle(cssText);
addScript(scriptText);
}