Greasy Fork

Greasy Fork is available in English.

Pixiv Overhaul

以瀑布流的样式刷P站

当前为 2024-12-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Pixiv Overhaul
// @namespace    http://tampermonkey.net/
// @version      2024-12-1
// @description  以瀑布流的样式刷P站
// @author       wlm3201
// @license      MIT
// @match        https://www.pixiv.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 !== "/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)"
      );
    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 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>
    <button id="leftbtn" class="round float" title="Tab">≡</button>
    <button id="back" class="round float"><</button>
    <button id="forward" class="round float">></button>
    <button id="top" class="round float">△</button>
    <button id="end" class="round float">▽</button>
    <div id="leftbar" class="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>
      </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>
    <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>
          <div class="wrap">
            <img class="thumb" loading="lazy" />
            <div class="info">
              <div class="title"></div>
              <div class="like"></div>
            </div>
          </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>
    
    
  </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));
}
let sleep = ms => new Promise(r => setTimeout(r, ms));
let del = document.documentElement;
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);
  };
}
HTMLElement.prototype.\$ = function () {
  return \$.apply(this, arguments);
};
HTMLElement.prototype.\$\$ = function () {
  return \$\$.apply(this, arguments);
};
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;
        this.db.createObjectStore("k_v", { keyPath: "k" });
        res(this);
      };
      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 = {};
let enums = {
  size: {
    large: "large",
    original: "original",
    master: "master",
    auto: "auto",
    zip: "zip",
    origzip: "origzip",
  },
  imgbox: "imgbox",
  userbox: "userbox",
};
// 接口
{
  g.apis = {
    host: "https://www.pixiv.net",
    ajax: "https://www.pixiv.net/ajax",
    uid: undefined,
    async init() {
      if (this.inited) return;
      let r = await fetch(this.host);
      let html = await r.text();
      let parser = new DOMParser();
      let doc = parser.parseFromString(html, "text/html");
      let text = doc.querySelector('[name="global-data"]').content;
      let j = JSON.parse(text);
      this.csrf = j.token;
      this.uid = j.userData.id;
      this.inited = 1;
    },
    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);
      let j = await r.json();
      if (j.error) throw j.message;
      return j.body || j;
    },
    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]) {
    if (u.match("limit_unknown_360")) return u;
    let hash = u.match(/\\/img\\/(.*?)_/)[1];
    let _p = u.includes("_p") ? \`_p\${p}\` : "";
    switch (q) {
      case enums.size.large:
        return \`https://i.pximg.net/c/600x1200_90/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_webp",
          "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\`;
    }
  }
  async function loadNext() {
    if (loading) return;
    if (del.scrollTop + del.clientHeight < del.scrollHeight - del.clientHeight)
      return;
    try {
      loading = 1;
      await g.pageMgr?.loadNext();
    } finally {
      loading = 0;
    }
  }
  function resize(wrap) {
    let illust = wrap.illust;
    let ratio = illust.width / illust.height;
    wrap.style.flexBasis = ratio * g.imgHeight + "px";
    wrap.style.flexGrow = ratio;
    return wrap;
  }
  class PageMgr {
    constructor(frag, type, finite, getFlex) {
      this.\$_ = frag.querySelector.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;
      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.p++;
      await this.loadPage();
    }
    reset(p = 1) {
      this.p = p;
      this.flexbox.replaceChildren();
      this.loadPage();
    }
    wrapImg(illust) {
      let wrap = \$("#wraps").content.cloneNode(true).children[0];
      wrap.illust = illust;
      let \$_ = wrap.querySelector.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 = \$("#users").content.cloneNode(true).children[0];
        let \$_ = user.querySelector.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 = \$("#user").content.cloneNode(true).children[0];
      g.nav.push(view, "?user=" + uid, "用户 - " + uid);
      {
        let info = await g.apis.user(uid);
        let \$_ = view.querySelector.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 = \$("#illust").content.cloneNode(true).children[0];
      g.nav.push(view, "?illust=" + id, "插画 - " + id);
      {
        let illust = await g.apis.illust(id);
        illust.url = illust.urls.original;
        view.illust = illust;
        let \$_ = view.querySelector.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();
      }
      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;
      });
    },
    async followed(uid) {
      let view = \$("#followed").content.cloneNode(true).children[0];
      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 = \$("#bookmarked").content.cloneNode(true).children[0];
      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.querySelector.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);
        \$_("[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;
      });
    },
  };
  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
      .querySelectorAll("slot")
      .forEach(
        slot => (slot.outerHTML = \$(\`template[name="\${slot.name}"]\`)?.innerHTML)
      )
  );
  g.formatUrl = formatUrl;
  g.resize = resize;
}
// 侧栏
{
  g.nav = {
    layers: [],
    states: [],
    index: 0,
    cover(el) {
      el.show();
      this.layers.add(el);
    },
    uncover() {
      if (this.layers.length === 0) return;
      let el = this.layers.pop();
      el.hide();
    },
    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)");
      lastFrag.lastScroll = del.scrollTop;
      lastFrag.pageMgr = g.pageMgr;
      lastFrag.hide();
      g.pageMgr = frag.pageMgr;
      del.scrollTop = frag.lastScroll;
      frag.show();
      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) \$("#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();
  {
    let params = location.search.slice(1).split("=");
    history.replaceState(null, null, params[0] ? "?" : "");
    g.init[params[0]]?.(params[1]);
  }
  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 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);
      \$\$(".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 = zoomImg;
    cover.onmousemove = moveImg;
    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 () => {
    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();
  };
}
// 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;
  box-sizing: border-box;
  gap: 5px;
}
body {
  margin: 0;
  background: black;
  color: white;
  overflow-y: scroll;
  #leftbar {
    width: 200px;
    padding: 10px;
    height: 100vh;
    position: fixed;
    text-align: center;
    left: 0;
    top: 0;
    z-index: 3;
    background: var(--dark);
    display: flex;
    flex-direction: column;
    gap: 10px;
    &.hide {
      display: none;
    }
    #tabs {
      display: contents;
      .split {
        border: var(--border);
      }
      .tab {
        height: 50px;
        cursor: pointer;
        border: var(--border);
      }
    }
  }
  .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;
    }
  }
  #leftbtn {
    bottom: 25px;
    left: 25px;
  }
  #forward {
    bottom: 25px;
    left: 175px;
  }
  #back {
    left: 100px;
    bottom: 25px;
  }
  #refresh {
    bottom: 25px;
    left: 100px;
  }
  #closeleft {
    bottom: 25px;
    left: 25px;
  }
  #top {
    bottom: 100px;
    right: 25px;
  }
  #end {
    right: 25px;
    bottom: 25px;
  }
  #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;
    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 {
        text-align: center;
        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;
        .title {
          text-align: center;
        }
      }
      .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: white solid 2px;
          }
        }
      }
    }
    .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;
          text-align: center;
          .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 {
      text-align: center;
      .user {
        display: flex;
        .avatar {
          object-fit: contain;
          object-position: top;
        }
        .info {
          text-align: center;
          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;
  }
}
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-color: black;
  &::-webkit-slider-thumb {
    appearance: none;
    cursor: pointer;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    background-color: 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;
}
`;
  document.documentElement.innerHTML = htmlText;
  addStyle(cssText);
  addScript(scriptText);
}