BiliKit Feed

B 站首页换成手机 App 的个性化推荐流。零框架纯原生实现(无 React/Vue、gzip 约 29KB)+ 窗口化虚拟化,约束 DOM、封面与预览媒体的常驻资源。视频默认开新标签页,也可选当前页或底部抽屉;封面悬停「真视频」秒开预览(MSE,接近原生 App)。需配合 BiliKit Core(登录 / 设置)。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         BiliKit Feed
// @namespace    https://github.com/shiinayane/BiliKit
// @version      0.3.23
// @author       shiinayane
// @description  B 站首页换成手机 App 的个性化推荐流。零框架纯原生实现(无 React/Vue、gzip 约 29KB)+ 窗口化虚拟化,约束 DOM、封面与预览媒体的常驻资源。视频默认开新标签页,也可选当前页或底部抽屉;封面悬停「真视频」秒开预览(MSE,接近原生 App)。需配合 BiliKit Core(登录 / 设置)。
// @license      MIT
// @icon         data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Crect%20width%3D%22512%22%20height%3D%22512%22%20rx%3D%22116%22%20fill%3D%22%23FB7299%22%2F%3E%3Cg%20stroke%3D%22%23fff%22%20stroke-width%3D%2226%22%20stroke-linecap%3D%22round%22%3E%3Cline%20x1%3D%22212%22%20y1%3D%22182%22%20x2%3D%22166%22%20y2%3D%22104%22%2F%3E%3Cline%20x1%3D%22300%22%20y1%3D%22182%22%20x2%3D%22346%22%20y2%3D%22104%22%2F%3E%3C%2Fg%3E%3Crect%20x%3D%22108%22%20y%3D%22176%22%20width%3D%22296%22%20height%3D%22236%22%20rx%3D%2254%22%20fill%3D%22%23fff%22%2F%3E%3Cpath%20d%3D%22M234%20258%20302%20294%20234%20330Z%22%20fill%3D%22%2300AEEC%22%20stroke%3D%22%2300AEEC%22%20stroke-width%3D%2218%22%20stroke-linejoin%3D%22round%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E
// @match        *://www.bilibili.com/
// @match        *://www.bilibili.com/?*
// @match        *://www.bilibili.com/index.html*
// @connect      app.bilibili.com
// @connect      api.bilibili.com
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  function md5(s) {
    function add32(a, b) {
      return a + b & 4294967295;
    }
    function cmn(q, a, b, x, sh, t) {
      a = add32(add32(a, q), add32(x, t));
      return add32(a << sh | a >>> 32 - sh, b);
    }
    function ff(a, b, c, d, x, s2, t) {
      return cmn(b & c | ~b & d, a, b, x, s2, t);
    }
    function gg(a, b, c, d, x, s2, t) {
      return cmn(b & d | c & ~d, a, b, x, s2, t);
    }
    function hh(a, b, c, d, x, s2, t) {
      return cmn(b ^ c ^ d, a, b, x, s2, t);
    }
    function ii(a, b, c, d, x, s2, t) {
      return cmn(c ^ (b | ~d), a, b, x, s2, t);
    }
    function cycle(x, k) {
      let a = x[0], b = x[1], c = x[2], d = x[3];
      a = ff(a, b, c, d, k[0], 7, -680876936);
      d = ff(d, a, b, c, k[1], 12, -389564586);
      c = ff(c, d, a, b, k[2], 17, 606105819);
      b = ff(b, c, d, a, k[3], 22, -1044525330);
      a = ff(a, b, c, d, k[4], 7, -176418897);
      d = ff(d, a, b, c, k[5], 12, 1200080426);
      c = ff(c, d, a, b, k[6], 17, -1473231341);
      b = ff(b, c, d, a, k[7], 22, -45705983);
      a = ff(a, b, c, d, k[8], 7, 1770035416);
      d = ff(d, a, b, c, k[9], 12, -1958414417);
      c = ff(c, d, a, b, k[10], 17, -42063);
      b = ff(b, c, d, a, k[11], 22, -1990404162);
      a = ff(a, b, c, d, k[12], 7, 1804603682);
      d = ff(d, a, b, c, k[13], 12, -40341101);
      c = ff(c, d, a, b, k[14], 17, -1502002290);
      b = ff(b, c, d, a, k[15], 22, 1236535329);
      a = gg(a, b, c, d, k[1], 5, -165796510);
      d = gg(d, a, b, c, k[6], 9, -1069501632);
      c = gg(c, d, a, b, k[11], 14, 643717713);
      b = gg(b, c, d, a, k[0], 20, -373897302);
      a = gg(a, b, c, d, k[5], 5, -701558691);
      d = gg(d, a, b, c, k[10], 9, 38016083);
      c = gg(c, d, a, b, k[15], 14, -660478335);
      b = gg(b, c, d, a, k[4], 20, -405537848);
      a = gg(a, b, c, d, k[9], 5, 568446438);
      d = gg(d, a, b, c, k[14], 9, -1019803690);
      c = gg(c, d, a, b, k[3], 14, -187363961);
      b = gg(b, c, d, a, k[8], 20, 1163531501);
      a = gg(a, b, c, d, k[13], 5, -1444681467);
      d = gg(d, a, b, c, k[2], 9, -51403784);
      c = gg(c, d, a, b, k[7], 14, 1735328473);
      b = gg(b, c, d, a, k[12], 20, -1926607734);
      a = hh(a, b, c, d, k[5], 4, -378558);
      d = hh(d, a, b, c, k[8], 11, -2022574463);
      c = hh(c, d, a, b, k[11], 16, 1839030562);
      b = hh(b, c, d, a, k[14], 23, -35309556);
      a = hh(a, b, c, d, k[1], 4, -1530992060);
      d = hh(d, a, b, c, k[4], 11, 1272893353);
      c = hh(c, d, a, b, k[7], 16, -155497632);
      b = hh(b, c, d, a, k[10], 23, -1094730640);
      a = hh(a, b, c, d, k[13], 4, 681279174);
      d = hh(d, a, b, c, k[0], 11, -358537222);
      c = hh(c, d, a, b, k[3], 16, -722521979);
      b = hh(b, c, d, a, k[6], 23, 76029189);
      a = hh(a, b, c, d, k[9], 4, -640364487);
      d = hh(d, a, b, c, k[12], 11, -421815835);
      c = hh(c, d, a, b, k[15], 16, 530742520);
      b = hh(b, c, d, a, k[2], 23, -995338651);
      a = ii(a, b, c, d, k[0], 6, -198630844);
      d = ii(d, a, b, c, k[7], 10, 1126891415);
      c = ii(c, d, a, b, k[14], 15, -1416354905);
      b = ii(b, c, d, a, k[5], 21, -57434055);
      a = ii(a, b, c, d, k[12], 6, 1700485571);
      d = ii(d, a, b, c, k[3], 10, -1894986606);
      c = ii(c, d, a, b, k[10], 15, -1051523);
      b = ii(b, c, d, a, k[1], 21, -2054922799);
      a = ii(a, b, c, d, k[8], 6, 1873313359);
      d = ii(d, a, b, c, k[15], 10, -30611744);
      c = ii(c, d, a, b, k[6], 15, -1560198380);
      b = ii(b, c, d, a, k[13], 21, 1309151649);
      a = ii(a, b, c, d, k[4], 6, -145523070);
      d = ii(d, a, b, c, k[11], 10, -1120210379);
      c = ii(c, d, a, b, k[2], 15, 718787259);
      b = ii(b, c, d, a, k[9], 21, -343485551);
      x[0] = add32(a, x[0]);
      x[1] = add32(b, x[1]);
      x[2] = add32(c, x[2]);
      x[3] = add32(d, x[3]);
    }
    function blk(str, i2) {
      const m = [];
      for (let j = 0; j < 64; j += 4) m[j >> 2] = str.charCodeAt(i2 + j) + (str.charCodeAt(i2 + j + 1) << 8) + (str.charCodeAt(i2 + j + 2) << 16) + (str.charCodeAt(i2 + j + 3) << 24);
      return m;
    }
    const n = s.length;
    const state = [1732584193, -271733879, -1732584194, 271733878];
    let i;
    for (i = 64; i <= n; i += 64) cycle(state, blk(s, i - 64));
    s = s.substring(i - 64);
    const tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
    tail[i >> 2] |= 128 << (i % 4 << 3);
    if (i > 55) {
      cycle(state, tail);
      for (i = 0; i < 16; i++) tail[i] = 0;
    }
    tail[14] = n * 8;
    cycle(state, tail);
    const hc = "0123456789abcdef";
    let out = "";
    for (const w of state) for (let j = 0; j < 4; j++) out += hc[w >> j * 8 + 4 & 15] + hc[w >> j * 8 & 15];
    return out;
  }
  const APPKEY = "4409e2ce8ffd12b8";
  const APPSEC = "59b43e04ad6965f34319062b478f83dd";
  function signAppQuery(params) {
    const p = { appkey: APPKEY, ...params };
    const sorted = Object.keys(p).sort().map((k) => `${k}=${encodeURIComponent(p[k])}`).join("&");
    return `${sorted}&sign=${md5(sorted + APPSEC)}`;
  }
  const MIXIN_TAB = [
    46,
    47,
    18,
    2,
    53,
    8,
    23,
    32,
    15,
    50,
    10,
    31,
    58,
    3,
    45,
    35,
    27,
    43,
    5,
    49,
    33,
    9,
    42,
    19,
    29,
    28,
    14,
    39,
    12,
    38,
    41,
    13,
    37,
    48,
    7,
    16,
    24,
    55,
    40,
    61,
    26,
    17,
    0,
    1,
    60,
    51,
    30,
    4,
    22,
    25,
    54,
    21,
    56,
    59,
    6,
    63,
    57,
    62,
    11,
    36,
    20,
    34,
    44,
    52
  ];
  const mixinKey = (orig) => MIXIN_TAB.map((n) => orig[n]).join("").slice(0, 32);
  const REF = { Referer: "https://www.bilibili.com/" };
  const LS_KEY = "bilikit:wbi";
  let keysPromise = null;
  function todayStamp() {
    return Math.floor(Date.now() / 864e5);
  }
  async function fetchKeys() {
    var _a, _b;
    try {
      const c = JSON.parse(localStorage.getItem(LS_KEY) || "null");
      if (c && c.day === todayStamp() && c.img && c.sub) return { img: c.img, sub: c.sub };
    } catch {
    }
    try {
      const t = await gmRequest({ method: "GET", url: "https://api.bilibili.com/x/web-interface/nav", headers: REF });
      const w = (_b = (_a = JSON.parse(t)) == null ? void 0 : _a.data) == null ? void 0 : _b.wbi_img;
      const base = (u) => (u || "").split("/").pop().split(".")[0];
      const img = base(w == null ? void 0 : w.img_url), sub = base(w == null ? void 0 : w.sub_url);
      if (!img || !sub) return null;
      try {
        localStorage.setItem(LS_KEY, JSON.stringify({ img, sub, day: todayStamp() }));
      } catch {
      }
      return { img, sub };
    } catch {
      return null;
    }
  }
  function getKeys() {
    if (!keysPromise) {
      keysPromise = fetchKeys().catch(() => null).then((k) => {
        if (!k) keysPromise = null;
        return k;
      });
    }
    return keysPromise;
  }
  async function signWbi(params) {
    const keys = await getKeys();
    if (!keys) return null;
    const mk = mixinKey(keys.img + keys.sub);
    const wts = Math.floor(Date.now() / 1e3);
    const q = { ...params, wts };
    const query = Object.keys(q).sort().map((k) => {
      const v = String(q[k]).replace(/[!'()*]/g, "");
      return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
    }).join("&");
    return `${query}&w_rid=${md5(query + mk)}`;
  }
  const WBI_REF = REF;
  const NS = "bk-feed";
  const BLANK = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
  const esc = (s) => s.replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
  const coverUrl = (u) => u ? u.replace(/^http:/, "https:") : BLANK;
  function coverSized(u) {
    const base = coverUrl(u);
    if (base === BLANK) return { avif: BLANK, webp: BLANK, jpg: BLANK };
    const p = "@640w_360h_1c";
    return { avif: `${base}${p}.avif`, webp: `${base}${p}.webp`, jpg: `${base}${p}` };
  }
  function fmtCount(n) {
    if (!isFinite(n) || n < 0) n = 0;
    if (n >= 1e8) return (n / 1e8).toFixed(1).replace(/\.0$/, "") + "亿";
    if (n >= 1e4) return (n / 1e4).toFixed(1).replace(/\.0$/, "") + "万";
    return String(n);
  }
  function fmtDuration(sec) {
    if (!isFinite(sec) || sec < 0) sec = 0;
    sec = Math.floor(sec);
    const h = Math.floor(sec / 3600), m = Math.floor(sec % 3600 / 60), s = sec % 60;
    const mm = h ? String(m).padStart(2, "0") : String(m);
    return (h ? h + ":" : "") + mm + ":" + String(s).padStart(2, "0");
  }
  function fmtDate(tsSec) {
    if (!tsSec) return "";
    const d = new Date(tsSec * 1e3);
    return `${d.getMonth() + 1}月${d.getDate()}日`;
  }
  function readSetting(key, fallback) {
    try {
      const v = JSON.parse(localStorage.getItem("bilikit:settings") || "{}")[key];
      return v === void 0 ? fallback : v;
    } catch {
      return fallback;
    }
  }
  const redactKey = (u) => (u || "").replace(/(access_key=)[^&]*/i, "$1<redacted>");
  function gmXhr() {
    return typeof GM !== "undefined" && GM && GM.xmlHttpRequest ? GM.xmlHttpRequest.bind(GM) : typeof GM_xmlhttpRequest !== "undefined" ? GM_xmlhttpRequest : null;
  }
  function gmRequestBinary(url, range, signal) {
    const xhr = gmXhr();
    if (!xhr) return Promise.reject(new Error("GM.xmlHttpRequest 不可用"));
    const abortError = () => Object.assign(new Error("请求已中止"), { name: "AbortError" });
    if (signal == null ? void 0 : signal.aborted) return Promise.reject(signal.reason || abortError());
    const headers = { Referer: "https://www.bilibili.com/" };
    if (range) headers.Range = `bytes=${range.start}-${range.end}`;
    return new Promise((resolve, reject) => {
      let done = false;
      let req = null;
      const finish = (fn) => {
        if (done) return;
        done = true;
        signal == null ? void 0 : signal.removeEventListener("abort", onSignalAbort);
        fn();
      };
      const onSignalAbort = () => {
        var _a;
        try {
          (_a = req == null ? void 0 : req.abort) == null ? void 0 : _a.call(req);
        } catch {
        }
        finish(() => reject((signal == null ? void 0 : signal.reason) || abortError()));
      };
      try {
        req = xhr({
          method: "GET",
          url,
          headers,
          responseType: "arraybuffer",
          timeout: 15e3,
          onload: (r) => {
            const okStatus = range ? r.status === 206 : r.status >= 200 && r.status < 300;
            if (!okStatus) {
              finish(() => reject(new Error("HTTP " + r.status)));
              return;
            }
            const buf = r.response;
            if (buf instanceof ArrayBuffer && buf.byteLength) finish(() => resolve(buf));
            else finish(() => reject(new Error("空/非二进制响应 status=" + r.status)));
          },
          onerror: (r) => finish(() => reject(new Error("网络错误 " + (r && r.status)))),
          ontimeout: () => finish(() => reject(new Error("超时"))),
          onabort: () => finish(() => reject(abortError()))
        });
      } catch (e) {
        finish(() => reject(e));
      }
      if (!done) signal == null ? void 0 : signal.addEventListener("abort", onSignalAbort, { once: true });
      if (!done && (signal == null ? void 0 : signal.aborted)) onSignalAbort();
    });
  }
  function gmRequest(opts) {
    const xhr = gmXhr();
    if (!xhr) return Promise.reject(new Error("GM.xmlHttpRequest 不可用(Feed 需 @grant GM.xmlHttpRequest)"));
    return new Promise((resolve, reject) => {
      xhr({
        method: opts.method,
        url: opts.url,
        data: opts.data,
        headers: opts.headers || (opts.data ? { "Content-Type": "application/x-www-form-urlencoded" } : void 0),
        anonymous: opts.anonymous,
        // 不带 cookie —— passport 风控对带 web cookie 的请求会回 412 HTML
        timeout: 15e3,
        onload: (r) => {
          const t = r.responseText || "";
          if (t.trimStart().startsWith("<")) {
            console.error(
              "[BiliKit Feed] 非 JSON 响应(可能被风控/登录拦截):",
              "status =",
              r.status,
              r.statusText,
              "url =",
              redactKey(r.finalUrl || opts.url),
              "\n  正文(前 300) =\n",
              t.slice(0, 300)
            );
          }
          resolve(t);
        },
        onerror: (r) => {
          console.error("[BiliKit Feed] onerror:", r && r.status);
          reject(new Error("网络错误"));
        },
        ontimeout: () => reject(new Error("请求超时")),
        onabort: () => reject(new Error("请求被中止"))
        // 否则中止时 Promise 永不 settle → 上游 loading 卡死
      });
    });
  }
  function descDate(desc) {
    if (!desc) return "";
    const i = desc.lastIndexOf(" · ");
    return i >= 0 ? desc.slice(i + 3).trim() : "";
  }
  function normalize(item) {
    var _a;
    if (!item || typeof item !== "object") return null;
    const args = item.args || {};
    const pa = item.player_args || {};
    return {
      goto: item.goto || "",
      title: item.title || "",
      up: args.up_name || "",
      mid: String(args.up_id || item.avatar && item.avatar.up_id || ""),
      face: item.avatar && item.avatar.cover || "",
      cover: item.cover || "",
      uri: item.uri || "",
      bvid: item.bvid || pa.bvid || "",
      aid: String(args.aid || pa.aid || item.param || ""),
      cid: String(pa.cid || args.cid || item.cid || ""),
      // player_args.cid 常有;无则预览时 pagelist 兜底
      param: String(item.param || args.aid || pa.aid || ""),
      // dislike 接口的 id
      duration: item.cover_left_text_1 || "",
      // 时长(实测在 text_1,如 13:02)
      play: item.cover_left_text_2 || "",
      // 观看数(实测在 text_2,如 25.4万观看)
      danmaku: item.cover_left_text_3 || "",
      // 弹幕数(如 13弹幕)
      date: descDate(item.desc || ""),
      reason: item.bottom_rcmd_reason || "",
      dislikeReasons: Array.isArray((_a = item.three_point) == null ? void 0 : _a.dislike_reasons) ? item.three_point.dislike_reasons.filter((r) => r && typeof r.id === "number").map((r) => ({ id: r.id, name: String(r.name || ""), toast: String(r.toast || "") })) : [],
      source: "app",
      trackId: ""
    };
  }
  let _dumpedTP = false;
  async function fetchAppFeed(accessKey2 = "") {
    var _a, _b;
    const idx = Math.floor(Date.now() / 1e3) + Math.floor(Math.random() * 1e3);
    const query = signAppQuery({
      build: "1",
      mobi_app: "iphone",
      device: "pad",
      idx: String(idx),
      access_key: accessKey2
    });
    const url = `https://app.bilibili.com/x/v2/feed/index?${query}`;
    const text = await gmRequest({ method: "GET", url });
    let json;
    try {
      json = JSON.parse(text);
    } catch {
      return { code: -1, message: "响应非 JSON(可能被风控/登录拦截)", cards: [], raw: text };
    }
    const items2 = Array.isArray((_a = json == null ? void 0 : json.data) == null ? void 0 : _a.items) ? json.data.items : [];
    if (!_dumpedTP && items2.length) {
      _dumpedTP = true;
      const sample = (_b = items2.find((i) => i && i.three_point)) == null ? void 0 : _b.three_point;
      if (sample) console.debug("[BiliKit Feed] three_point 样本(校对「我不想看」reason id/name 用):", JSON.stringify(sample));
    }
    const cards = items2.map(normalize).filter((c) => !!c && c.goto === "av");
    const code = typeof (json == null ? void 0 : json.code) === "number" ? json.code : -1;
    return { code, message: (json == null ? void 0 : json.message) || "", cards, raw: json };
  }
  function normalizeWeb(item) {
    if (!item || item.goto !== "av") return null;
    const owner = item.owner || {};
    const stat = item.stat || {};
    const aid = String(item.id || item.aid || "");
    return {
      goto: "av",
      title: item.title || "",
      up: owner.name || "",
      mid: String(owner.mid || ""),
      face: owner.face || "",
      cover: item.pic || "",
      uri: item.uri || (item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : ""),
      bvid: item.bvid || "",
      aid,
      cid: String(item.cid || ""),
      param: aid,
      duration: fmtDuration(item.duration || 0),
      play: stat.view ? fmtCount(stat.view) : "",
      danmaku: stat.danmaku ? fmtCount(stat.danmaku) : "",
      date: item.pubdate ? fmtDate(item.pubdate) : "",
      reason: item.rcmd_reason && item.rcmd_reason.content || (item.is_followed ? "已关注" : ""),
      // web 端「不想看」reason 是固定两项(不像 app 从 three_point 动态取):内容不感兴趣=1、不想看此UP主=4。
      // 复用卡片现有的 pickReasons 映射:name 得能被它匹配到——'不感兴趣'命中 notInterest(id===1)、'UP主'命中 upper(/^up主/)。
      // 实际提交走 web 接口(见 actions.ts,按 source 分派);菜单显示的标签在 card.ts 里另写死。
      dislikeReasons: [
        { id: 1, name: "不感兴趣", toast: "" },
        { id: 4, name: "UP主", toast: "" }
      ],
      source: "web",
      trackId: String(item.track_id || "")
    };
  }
  async function fetchWebFeed(freshIdx) {
    var _a;
    const query = await signWbi({ fresh_type: 4, feed_version: "V8", homepage_ver: 1, ps: 12, fresh_idx: freshIdx, fresh_idx_1h: freshIdx });
    if (!query) return { code: -1, message: "wbi 签名未就绪", cards: [], raw: null };
    let text;
    try {
      text = await gmRequest({ method: "GET", url: `https://api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd?${query}`, headers: WBI_REF });
    } catch {
      return { code: -1, message: "网络错误", cards: [], raw: null };
    }
    let json;
    try {
      json = JSON.parse(text);
    } catch {
      return { code: -1, message: "响应非 JSON(可能被风控拦截)", cards: [], raw: text };
    }
    const list = Array.isArray((_a = json == null ? void 0 : json.data) == null ? void 0 : _a.item) ? json.data.item : [];
    const cards = list.map(normalizeWeb).filter((c) => !!c);
    const code = typeof (json == null ? void 0 : json.code) === "number" ? json.code : -1;
    return { code, message: (json == null ? void 0 : json.message) || "", cards, raw: json };
  }
  function injectStyle() {
    if (document.getElementById("bk-feed-style")) return;
    const s = document.createElement("style");
    s.id = "bk-feed-style";
    s.textContent = `
    .${NS}{ display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:22px 16px; padding:16px 0; }
    /* 不再用 content-visibility:真·窗口化已把 DOM 限制在可视附近;且 CV 会让「窗口内但不在视口」的卡
       塌回 contain-intrinsic-size,与视口内卡的真实高不一致 → 滚动时高度抖动。去掉后每卡真实等高。 */
    /* hover 浮起用 top 偏移而**不是 transform**:transform 会把这张卡提成合成层、移开又拆掉——
       WebKit 的重叠测试会把「视觉边界(含 hover 大投影,伸进邻卡)与之重叠、绘制顺序在后」的邻卡内容
       连带提升/降回合成层;网格列宽是 1fr 分数像素,文字在普通绘制与合成层上的亚像素栅格化不同,
       建/拆层那一瞬邻卡遮罩文字就会肉眼可见地「动一下」(圆角裁剪短暂失效露缝也是同一根)。
       top 是纯绘制偏移(position:relative 已就位):不建层、不触发邻卡任何变化,只重绘本卡区域。 */
    .${NS}-card{ cursor:pointer; top:0; transition:top .18s ease; }
    .${NS}-card:hover{ top:-4px; }
    /* isolation:isolate 修 WebKit #77572:overflow:hidden+border-radius 容器里含硬件合成子层(hover 预览的 <video>,
       媒体合成面无视元素 border-radius)时 Safari 不把子层裁到圆角、露缝;建独立层叠上下文即正确裁剪。
       **必须常驻、不能按需切换**:0.3.17 曾改成「仅挂 <video> 时加 .hasvp」以省合成面,但动态增删 isolation
       会在建/拆层叠上下文那一瞬 churn 合成层,触发 WebKit overlap testing 把邻卡遮罩(播放/弹幕图标)顶得上下位移、
       露缝(与 hover 用 transform 会晃邻卡同源)——故回归常驻。每张封面一个层叠上下文的图形开销可接受。 */
    .${NS}-cover{ position:relative; aspect-ratio:16/9; border-radius:8px; overflow:hidden; background:var(--bg2,#e3e5e7); transition:box-shadow .18s ease; isolation:isolate; }
    .${NS}-card:hover .${NS}-cover{ box-shadow:0 6px 20px rgba(0,0,0,.22); }
    .${NS}-cover img{ width:100%; height:100%; object-fit:cover; display:block; opacity:0; transition:opacity .35s ease; }
    .${NS}-cover.loaded img{ opacity:1; }
    /* hover 雪碧图预览:盖在封面上,鼠标横向刮帧;在遮罩(z-index:2)之下、图片之上 */
    .${NS}-preview{ position:absolute; inset:0; z-index:1; background-repeat:no-repeat; opacity:0; transition:opacity .15s ease; pointer-events:none; }
    .${NS}-preview.on{ opacity:1; }
    /* hover 真视频预览:低清 dash 静音自动播,盖在封面上(同雪碧图层级 z-index:1,遮罩之下、图片之上) */
    .${NS}-vpreview{ position:absolute; inset:0; z-index:1; width:100%; height:100%; object-fit:cover; display:block; opacity:0; transition:opacity .2s ease; pointer-events:none; background:#000; }
    .${NS}-vpreview.on{ opacity:1; }
    /* Safari 圆角裁剪补丁:透明度过渡的封面图 / 真视频预览会被 Safari 提为合成子层;父卡 hover 浮起(transform)
       期间,Safari 有时不把这些子层裁到 .bk-cover 的 border-radius,方角越过圆角、露出一条缝(深色下尤其明显)。
       给每个「铺满封面、贴到边角」的子层自身也上同款圆角,方角即被磨圆、与父级圆角重合,父级裁剪失效时也不再露缝。
       走 border-radius(仅编译期圆角)而非 -webkit-mask 强裁——后者会把播放中的视频每帧重刷进遮罩缓冲、增功耗。 */
    .${NS}-cover img, .${NS}-vpreview, .${NS}-preview{ border-radius:8px; }
    .${NS}-mask, .${NS}-pbar{ border-radius:0 0 8px 8px; }
    /* 预览播放时:隐藏播放/弹幕数遮罩;右下角时长转「当前 / 总时长」,去渐变、加阴影保可读 */
    .${NS}-cover.previewing .${NS}-mstat{ display:none; }
    .${NS}-cover.previewing .${NS}-mask{ background:none; justify-content:flex-end; }
    .${NS}-cover.previewing .${NS}-mask span{ text-shadow:0 1px 4px rgba(0,0,0,.95); font-variant-numeric:tabular-nums; }
    /* 预览进度条:底部细条,随播放推进(scaleX → 合成层)。z-index:3 压在遮罩之上,短视频也看得清进度 */
    .${NS}-pbar{ position:absolute; left:0; right:0; bottom:0; z-index:3; height:3px; background:rgba(0,0,0,.28); opacity:0; transition:opacity .15s ease; pointer-events:none; }
    .${NS}-pbar.on{ opacity:1; }
    .${NS}-pbar i{ display:block; width:100%; height:100%; background:var(--brand_blue,#00aeec); transform:scaleX(0); transform-origin:left; }
    /* 骨架微光:统一走「合成层友好的 transform 位移伪元素」——封面(未加载)、骨架条、头像同一套,
       只动 transform(GPU 合成,不逐帧重绘),滚动/加载期都不掉帧。封面 .loaded/.failed 后伪元素消失。 */
    .${NS}-shimmer{ position:relative; overflow:hidden; background-color:var(--bg2,#e3e5e7); }
    .${NS}-cover:not(.loaded):not(.failed)::after, .${NS}-shimmer::after{
      content:''; position:absolute; inset:0;
      /* 浅色默认:白光打在浅灰底上看不见 → 用暗色扫光(浅底上才有对比) */
      background:linear-gradient(90deg, transparent 20%, rgba(0,0,0,.07) 50%, transparent 80%);
      transform:translateX(-100%); animation:bk-shimmer 1.6s linear infinite;
    }
    /* 深色:按页面真实底色判定(JS 给 grid 加 .bk-dark),不用 @media prefers——避免「系统浅/B站深」时不生效。用淡白扫光 */
    .${NS}.bk-dark .${NS}-cover:not(.loaded):not(.failed)::after, .${NS}.bk-dark .${NS}-shimmer::after{
      background:linear-gradient(90deg, transparent 15%, rgba(255,255,255,.11) 50%, transparent 85%);
    }
    @keyframes bk-shimmer{ to{ transform:translateX(100%); } }
    /* 封面底部遮罩:左「播放·弹幕」右「时长」 */
    /* z-index:1 必需:封面 img 有 opacity 过渡,Safari 会把它提升为合成层、盖住本遮罩;
       给遮罩显式 z-index 才能压在图片层之上(否则 z-index:auto 不进合成层,被图片遮住)。 */
    .${NS}-mask{ position:absolute; left:0; right:0; bottom:0; z-index:2; display:flex; align-items:flex-end; justify-content:space-between; padding:8px 8px 7px; color:#fff; font-size:12px; line-height:1; background:linear-gradient(transparent, rgba(0,0,0,.85)); pointer-events:none; }
    .${NS}-mstat{ display:flex; align-items:center; gap:9px; }
    .${NS}-mstat span{ display:inline-flex; align-items:center; gap:3px; }
    .${NS}-mstat svg{ width:15px; height:15px; }
    /* 下方:头像独占左栏,右栏上标题、下「UP名 · 日期」 */
    .${NS}-bottom{ position:relative; z-index:2; display:flex; gap:10px; margin-top:9px; align-items:flex-start; } /* z-index:2 压过封面合成层,向上弹的三点菜单才不被封面盖住 */
    .${NS}-face{ width:34px; height:34px; flex:0 0 34px; border-radius:50%; object-fit:cover; background:var(--bg2,#e3e5e7); }
    img.${NS}-face{ cursor:pointer; transition:box-shadow .15s ease; } /* 有头像时可点进空间(占位 div 不给手型) */
    img.${NS}-face:hover{ box-shadow:0 0 0 2px var(--brand_blue,#00aeec); } /* hover 强调:品牌色圆环 */
    .${NS}-right{ flex:1; min-width:0; }
    .${NS}-up{ cursor:pointer; } /* UP 名可点进空间 */
    .${NS}-up:hover{ color:var(--brand_blue,#00aeec); }
    /* min-height 固定 2 行:让每张卡等高,虚拟化的行高估算才准、不漂移抖动(1 行标题也占 2 行位) */
    .${NS}-title{ margin:0 0 6px; font-size:15px; font-weight:500; line-height:1.4; min-height:2.8em; color:var(--text1,#18191c); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; transition:color .16s ease; }
    .${NS}-title:hover{ color:var(--brand_blue,#00aeec); } /* 与 UP 名一致:hover 标题本身即高亮成品牌色,示意可点 */
    .${NS}-sub{ display:flex; align-items:center; font-size:13px; color:var(--text3,#9499a0); }
    .${NS}-who{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .${NS}-sub i{ margin:0 5px; font-style:normal; }
    /* 推荐理由行内 pill(学 Gate):只描边+文字色、不填充;实际配色由 B 站 reason style 内联覆盖 */
    .${NS}-badge{ flex:none; margin-right:6px; padding:0 6px; border:1px solid var(--brand_blue,#00aeec); border-radius:6px; color:var(--brand_blue,#00aeec); background:transparent; font-size:11px; line-height:16px; }
    /* 骨架占位(数据未到时) */
    .${NS}-skline{ height:13px; border-radius:4px; margin-bottom:8px; }
    /* 未装 Core 的提示条(可关闭) */
    .${NS}-warn{ grid-column:1/-1; display:flex; align-items:center; gap:12px; margin-bottom:6px; padding:10px 14px; border-radius:10px; background:var(--bg2,#f1f2f3); color:var(--text2,#61666d); font-size:13px; }
    .${NS}-warn b{ color:var(--text1,#18191c); }
    .${NS}-warn a{ color:var(--brand_blue,#00aeec); text-decoration:none; white-space:nowrap; }
    .${NS}-warn .bk-x{ margin-left:auto; border:0; background:transparent; color:var(--text3,#9499a0); cursor:pointer; font-size:14px; line-height:1; padding:4px; }
    .${NS}-warn .bk-x:hover{ color:var(--text1,#18191c); }
    .${NS}-spacer{ grid-column:1/-1; height:0; }  /* 窗口化:上下占位行,撑起未渲染区的高度,保滚动位置 */
    .${NS}-sentinel{ grid-column:1/-1; height:1px; }
    .${NS}-tip{ grid-column:1/-1; text-align:center; color:var(--text3,#9499a0); font-size:13px; padding:20px; }
    .${NS}-fab{ position:fixed; right:24px; bottom:32px; z-index:1000; display:flex; flex-direction:column; gap:10px; }
    /* 悬浮工具按钮:圆形 + 细描边 + 表面底 + 轻阴影,hover 变品牌色微浮起、按下微缩 */
    .${NS}-fab button{
      width:40px; height:40px; border-radius:50%; padding:0;
      display:flex; align-items:center; justify-content:center;
      border:1px solid var(--line_regular,#e3e5e7); background:var(--bg1,#fff); color:var(--text2,#61666d);
      cursor:pointer; box-shadow:0 2px 10px rgba(0,0,0,.12);
      transition:color .16s ease, transform .16s ease, box-shadow .16s ease, opacity .18s ease;
    }
    .${NS}-fab button:hover{ color:var(--brand_blue,#00aeec); transform:translateY(-2px); box-shadow:0 5px 16px rgba(0,0,0,.2); }
    .${NS}-fab button:active{ transform:scale(.94); }
    .${NS}-fab .bk-top{ opacity:0; pointer-events:none; transform:scale(.85); }      /* 默认藏,滚动后现 */
    .${NS}-fab.scrolled .bk-top{ opacity:1; pointer-events:auto; transform:none; }
    .${NS}-fab button.busy{ pointer-events:none; }
    .${NS}-fab button.busy svg{ animation:bk-spin .8s linear infinite; }
    @keyframes bk-spin{ to{ transform:rotate(360deg); } }
    /* 推荐源切换器:占一个 FAB 槽(40x40);默认只显当前源(.bk-src-cur 复用 fab 圆钮样式),
       hover(或触屏 .open)当前钮淡出、竖向胶囊(.bk-src-pop)从底部对齐向上展开列出两项。 */
    .${NS}-src{ position:relative; width:40px; height:40px; }
    .${NS}-src-cur{ position:absolute; inset:0; transition:opacity .16s ease; }
    .${NS}-src:hover .${NS}-src-cur, .${NS}-src.open .${NS}-src-cur{ opacity:0; pointer-events:none; }
    .${NS}-src-pop{ position:absolute; right:0; bottom:0; z-index:1; display:flex; flex-direction:column;
      border-radius:20px; background:var(--bg1,#fff); border:1px solid var(--line_regular,#e3e5e7);
      box-shadow:0 4px 16px rgba(0,0,0,.18); overflow:hidden;
      opacity:0; visibility:hidden; transform:translateY(6px);
      transition:opacity .16s ease, transform .16s ease, visibility .16s; }
    .${NS}-src:hover .${NS}-src-pop, .${NS}-src.open .${NS}-src-pop{ opacity:1; visibility:visible; transform:none; }
    /* 胶囊里的两项:去掉 fab 圆钮的边/底/影/圆,做成连续竖条;当前项品牌色高亮 */
    .${NS}-fab .${NS}-src-opt{ width:40px; height:40px; border:0; border-radius:0; background:transparent; box-shadow:none; color:var(--text2,#61666d); }
    .${NS}-fab .${NS}-src-opt:hover{ transform:none; box-shadow:none; background:var(--bg2,#e3e5e7); color:var(--brand_blue,#00aeec); }
    .${NS}-fab .${NS}-src-opt.on{ color:var(--brand_blue,#00aeec); }

    /* ——— 卡片操作:稍后再看 / 我不想看 / 撤销浮层 ——— */
    .${NS}-card{ position:relative; } /* 承载「不想看」模糊浮层的定位上下文 */
    /* 三点菜单展开时抬高本卡层级,否则溢出的菜单会被 DOM 后面的卡片盖住。
       触发条件收窄为「悬到三点按钮本身」+ menuopen,而不是「悬到整张卡」——
       z-index 变化会让 Safari 重排该网格的合成层绘制顺序,touch 太频繁(鼠标扫过任意卡都算)会让
       其它卡片的圆角裁剪(同类 Safari 合成层 bug,见上面 border-radius 那段注释)瞬间失效、露出缝隙。
       用 :has() 把触发范围缩到「真悬到三点按钮」这个小目标上,日常划过网格不再触发全局重排。 */
    .${NS}-card:has(.${NS}-more-wrap:hover), .${NS}-card.menuopen{ z-index:20; }
    /* 稍后再看:封面右上角,hover 现(触屏首触即现)。深色封面上永远白图标、暗玻璃底 */
    .${NS}-wl{ position:absolute; top:8px; right:8px; z-index:3; width:32px; height:32px; border-radius:50%; border:0; padding:0; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,.55); color:#fff; cursor:pointer; opacity:0; transform:translateY(-4px); -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px); transition:opacity .18s ease, transform .18s ease, background .16s ease; }
    .${NS}-wl svg{ width:17px; height:17px; }
    .${NS}-card:hover .${NS}-wl{ opacity:1; transform:none; }
    .${NS}-wl:hover{ background:var(--brand_blue,#00aeec); }
    .${NS}-wl.busy{ pointer-events:none; }
    .${NS}-wl.busy svg{ animation:bk-spin .8s linear infinite; }
    .${NS}-wl.done{ opacity:1; transform:none; background:var(--brand_blue,#00aeec); }
    /* 三点「我不想看」:随 UP名·日期行右端、常显(不再压标题,标题恢复占满右侧宽度)。
       给 sub 定个 min-height,让「有菜单」与「无菜单」的卡等高,虚拟化行高不漂移。菜单向上弹(本行在卡底部)。 */
    .${NS}-sub{ min-height:22px; }
    .${NS}-more-wrap{ position:relative; flex:none; margin-left:auto; } /* auto 把三点推到本行最右端 */
    .${NS}-more{ width:22px; height:22px; border:0; padding:0; border-radius:6px; background:transparent; color:var(--text3,#9499a0); display:flex; align-items:center; justify-content:center; cursor:pointer; transition:background .16s ease, color .16s ease; }
    .${NS}-more svg{ width:16px; height:16px; }
    .${NS}-more:hover{ background:var(--bg2,#e3e5e7); color:var(--text1,#18191c); }
    /* 菜单向上弹;z-index 高 + 下方 .bk-feed-bottom 抬到封面之上(封面是 Safari 合成层,否则菜单被自家封面盖住) */
    .${NS}-menu{ position:absolute; bottom:26px; right:0; z-index:30; min-width:136px; padding:4px; background:var(--bg1,#fff); border:1px solid var(--line_regular,#e3e5e7); border-radius:8px; box-shadow:0 6px 24px rgba(0,0,0,.16); opacity:0; visibility:hidden; transform:translateY(4px); transition:opacity .16s ease, transform .16s ease, visibility .16s; }
    .${NS}-more-wrap:hover .${NS}-menu, .${NS}-more-wrap.open .${NS}-menu{ opacity:1; visibility:visible; transform:none; }
    .${NS}-mi{ display:block; width:100%; text-align:left; padding:8px 10px; border:0; border-radius:6px; background:transparent; color:var(--text1,#18191c); font-size:13px; white-space:nowrap; cursor:pointer; }
    .${NS}-mi:hover{ background:var(--bg2,#e3e5e7); color:var(--brand_blue,#00aeec); }
    /* 提交「不想看」后:卡片内容模糊压暗 + 浮层(愁脸文案 + 撤销),淡入过渡 */
    .${NS}-card.disliked{ cursor:default; }
    .${NS}-card.disliked:hover{ top:0; } /* 已「不想看」的卡不再浮起(浮起改走 top,见上) */
    .${NS}-card.disliked .${NS}-cover, .${NS}-card.disliked .${NS}-bottom{ filter:blur(5px); opacity:.5; pointer-events:none; transition:filter .28s ease, opacity .28s ease; }
    .${NS}-dov{ position:absolute; inset:0; z-index:8; display:none; align-items:center; justify-content:center; }
    .${NS}-card.disliked .${NS}-dov{ display:flex; animation:bk-dov-in .26s ease; }
    @keyframes bk-dov-in{ from{ opacity:0; transform:scale(.96); } to{ opacity:1; transform:none; } }
    .${NS}-dov-in{ display:flex; flex-direction:column; align-items:center; gap:11px; padding:12px; text-align:center; }
    .${NS}-dov-txt{ color:var(--text1,#18191c); font-size:14px; font-weight:500; }
    .${NS}-undo{ display:inline-flex; align-items:center; gap:5px; padding:6px 15px; border:1px solid var(--line_regular,#e3e5e7); border-radius:16px; background:var(--bg1,#fff); color:var(--text1,#18191c); font-size:13px; cursor:pointer; transition:color .16s ease, border-color .16s ease; }
    .${NS}-undo svg{ width:15px; height:15px; }
    .${NS}-undo:hover{ color:var(--brand_blue,#00aeec); border-color:var(--brand_blue,#00aeec); }
    /* 轻量 toast:底部居中,跟随 B 站主题变量 */
    .${NS}-toast{ position:fixed; left:50%; bottom:44px; z-index:2147483000; transform:translateX(-50%) translateY(12px); max-width:76vw; padding:9px 16px; border-radius:8px; background:rgba(0,0,0,.82); color:#fff; font-size:13px; line-height:1.4; opacity:0; pointer-events:none; -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); transition:opacity .2s ease, transform .2s ease; }
    .${NS}-toast.on{ opacity:1; transform:translateX(-50%) translateY(0); }
  `;
    (document.head || document.documentElement).appendChild(s);
  }
  function hideNativeChrome() {
    if (document.getElementById("bk-feed-chrome")) return;
    const s = document.createElement("style");
    s.id = "bk-feed-chrome";
    s.textContent = `
    .feed-roll-btn { display: none !important; }        /* 右侧「换一换」 */
    .palette-button-wrap { display: none !important; }   /* 右下角 刷新内容/更多/返回顶部 */
    .adblock-tips { display: none !important; }          /* 顶部「检测到浏览器插件…加入白名单」提示 */
    /* 分区栏「不钉顶」:.header-channel 是 B 站在滚动后注入的钉顶副本(首屏时 h=0、空),
       真正可见的分区在 .bili-header 内、会随页滚走。隐掉这个副本即可:分区仍在(顶部那份),
       只是不再钉顶,也避开了它注入高度时引发的画面抽搐。 */
    .header-channel { display: none !important; }
  `;
    (document.head || document.documentElement).appendChild(s);
  }
  const shotCache = /* @__PURE__ */ new Map();
  const imgLoaded = /* @__PURE__ */ new Set();
  function preloadImg(src) {
    if (!src || imgLoaded.has(src)) return Promise.resolve();
    return new Promise((resolve) => {
      const im = new Image();
      im.onload = im.onerror = () => {
        imgLoaded.add(src);
        if (imgLoaded.size > 400) imgLoaded.delete(imgLoaded.values().next().value);
        resolve();
      };
      im.src = src;
    });
  }
  async function fetchVideoshot(bvid) {
    var _a;
    if (shotCache.has(bvid)) return shotCache.get(bvid);
    let shot = null;
    try {
      const text = await gmRequest({ method: "GET", url: `https://api.bilibili.com/x/player/videoshot?bvid=${bvid}&index=1` });
      const d = (_a = JSON.parse(text)) == null ? void 0 : _a.data;
      if (d && Array.isArray(d.image) && d.image.length && Array.isArray(d.index) && d.index.length) {
        shot = { images: d.image.map((u) => coverUrl(u)), index: d.index, xlen: d.img_x_len || 10, ylen: d.img_y_len || 10 };
      }
    } catch {
    }
    shotCache.set(bvid, shot);
    if (shotCache.size > 200) shotCache.delete(shotCache.keys().next().value);
    return shot;
  }
  function setupHoverPreview(cover, bvid) {
    const RUN = 8e3;
    const FRAME_MS = 250;
    let hovering = false;
    let enterTimer = null;
    let rafId = 0;
    let startT = 0;
    let lastFrameAt = 0;
    let lastIdx = -1;
    let preview = null;
    let pbar = null;
    let bar = null;
    let shot = null;
    const showFrame = (idx) => {
      if (!preview || !shot) return;
      const per = shot.xlen * shot.ylen;
      const sheet = Math.min(Math.floor(idx / per), shot.images.length - 1);
      const local = idx % per;
      const col = local % shot.xlen;
      const row = Math.floor(local / shot.xlen);
      preview.style.backgroundImage = `url("${shot.images[sheet]}")`;
      preview.style.backgroundSize = `${shot.xlen * 100}% ${shot.ylen * 100}%`;
      preview.style.backgroundPosition = `${shot.xlen > 1 ? col / (shot.xlen - 1) * 100 : 0}% ${shot.ylen > 1 ? row / (shot.ylen - 1) * 100 : 0}%`;
    };
    const tick = (now) => {
      if (!cover.isConnected) {
        stop();
        return;
      }
      if (!hovering || !shot) {
        rafId = 0;
        return;
      }
      const p = (now - startT) % RUN / RUN;
      if (bar) bar.style.transform = `scaleX(${p})`;
      if (now - lastFrameAt >= FRAME_MS) {
        lastFrameAt = now;
        const idx = Math.min(Math.floor(p * shot.index.length), shot.index.length - 1);
        if (idx !== lastIdx) {
          lastIdx = idx;
          showFrame(idx);
        }
      }
      rafId = requestAnimationFrame(tick);
    };
    const stop = () => {
      hovering = false;
      if (enterTimer) {
        clearTimeout(enterTimer);
        enterTimer = null;
      }
      if (rafId) {
        cancelAnimationFrame(rafId);
        rafId = 0;
      }
      preview == null ? void 0 : preview.classList.remove("on");
      pbar == null ? void 0 : pbar.classList.remove("on");
    };
    cover.addEventListener("mouseenter", () => {
      if (hovering) return;
      hovering = true;
      enterTimer = setTimeout(async () => {
        enterTimer = null;
        const s = await fetchVideoshot(bvid);
        if (!s || !hovering) return;
        await preloadImg(s.images[0]);
        if (!hovering) return;
        shot = s;
        if (!preview) {
          preview = document.createElement("div");
          preview.className = `${NS}-preview`;
          cover.appendChild(preview);
          pbar = document.createElement("div");
          pbar.className = `${NS}-pbar`;
          bar = document.createElement("i");
          pbar.appendChild(bar);
          cover.appendChild(pbar);
        }
        lastIdx = -1;
        lastFrameAt = 0;
        startT = performance.now();
        showFrame(0);
        preview.classList.add("on");
        pbar.classList.add("on");
        rafId = requestAnimationFrame(tick);
        for (const src of s.images.slice(1)) void preloadImg(src);
      }, 150);
    });
    cover.addEventListener("mouseleave", stop);
  }
  function targetMirror() {
    return readSetting("module.cdn-pick.cfg.targetHost", "upos-sz-mirrorhwb.bilivideo.com");
  }
  const EXTRA_MIRROR = "upos-sz-mirrorcoso1.bilivideo.com";
  const hostOf = (u) => {
    try {
      return new URL(u).hostname;
    } catch {
      return "";
    }
  };
  const pathOf = (u) => {
    try {
      return new URL(u).pathname;
    } catch {
      return "";
    }
  };
  function isPcdn(u) {
    const h = hostOf(u);
    return /\.mcdn\.bilivideo\.(cn|com)$/i.test(h) || h.includes("mcdn") || /(^|\.)szbdyd\.com$/i.test(h) || /\.biliapi\.net$/i.test(h) || pathOf(u).startsWith("/v1/resource/");
  }
  const isUpos = (u) => /^upos-[^.]*\.bilivideo\.com$/i.test(hostOf(u));
  function swapTo(u, host) {
    try {
      const x = new URL(u);
      x.protocol = "https:";
      x.host = host;
      return x.href;
    } catch {
      return u;
    }
  }
  const xyUsource = (u) => {
    try {
      return new URL(u).searchParams.get("xy_usource") || "";
    } catch {
      return "";
    }
  };
  function prefer(urls) {
    const mirror = targetMirror();
    const uniq = [...new Set(urls.filter(Boolean))];
    const out = [];
    const push = (v) => {
      if (v && !out.includes(v)) out.push(v);
    };
    for (const u of uniq) if (!isPcdn(u) && isUpos(u)) push(swapTo(u, mirror));
    for (const u of uniq) if (isPcdn(u)) {
      const xy = xyUsource(u);
      if (xy) push(swapTo(u, xy));
      push(swapTo(u, mirror));
      push(swapTo(u, EXTRA_MIRROR));
    }
    for (const u of uniq) push(u);
    return out;
  }
  async function getCid(bvid, known) {
    var _a, _b, _c;
    if (known) return known;
    const t = await gmRequest({ method: "GET", url: `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}`, headers: WBI_REF });
    return String(((_c = (_b = (_a = JSON.parse(t)) == null ? void 0 : _a.data) == null ? void 0 : _b[0]) == null ? void 0 : _c.cid) || "");
  }
  async function requestPlayurl(bvid, cid, fnval, qn) {
    const query = await signWbi({ bvid, cid, qn, fnval, fnver: 0, fourk: 0 });
    if (!query) throw new Error("no wbi keys");
    const t = await gmRequest({ method: "GET", url: `https://api.bilibili.com/x/player/wbi/playurl?${query}`, headers: WBI_REF });
    return JSON.parse(t);
  }
  function parseSeg(v) {
    const sb = v.segment_base || v.SegmentBase || {};
    const init = sb.initialization || sb.Initialization || sb.range || "";
    const idx = sb.index_range || sb.indexRange || "";
    const [, initEndS] = init.split("-");
    const [idxStartS, idxEndS] = idx.split("-");
    const initEnd = Number(initEndS), indexStart = Number(idxStartS), indexEnd = Number(idxEndS);
    if (!Number.isFinite(initEnd) || !Number.isFinite(indexStart) || !Number.isFinite(indexEnd)) return null;
    return { initEnd, indexStart, indexEnd };
  }
  const dashCache = /* @__PURE__ */ new Map();
  const durlCache = /* @__PURE__ */ new Map();
  const lru = (m) => {
    if (m.size > 150) m.delete(m.keys().next().value);
  };
  async function getDashPreview(bvid, cid0) {
    var _a, _b;
    if (dashCache.has(bvid)) return dashCache.get(bvid);
    let out = null;
    let errored = false;
    try {
      const cid = await getCid(bvid, cid0);
      if (!cid) throw new Error("no cid");
      const j = await requestPlayurl(bvid, cid, 16, 32);
      const avc = (((_b = (_a = j == null ? void 0 : j.data) == null ? void 0 : _a.dash) == null ? void 0 : _b.video) || []).filter((v) => v.codecid === 7);
      if (avc.length) {
        const low = avc.sort((a, b) => (a.id || 0) - (b.id || 0))[0];
        const seg = parseSeg(low);
        if (seg && low.codecs) {
          out = { codecs: low.codecs, urls: prefer([low.baseUrl || low.base_url, ...low.backupUrl || low.backup_url || []]), ...seg };
        }
      }
    } catch (e) {
      errored = true;
      console.warn("[BiliKit Feed] dash 取流失败:", (e == null ? void 0 : e.message) || e);
    }
    if (!errored) {
      dashCache.set(bvid, out);
      lru(dashCache);
    }
    return out;
  }
  async function getDurlSources(bvid, cid0) {
    var _a;
    if (durlCache.has(bvid)) return durlCache.get(bvid);
    let sources = null;
    let errored = false;
    try {
      const cid = await getCid(bvid, cid0);
      if (!cid) throw new Error("no cid");
      const j = await requestPlayurl(bvid, cid, 1, 16);
      const durl = (_a = j == null ? void 0 : j.data) == null ? void 0 : _a.durl;
      if (Array.isArray(durl) && durl.length) sources = prefer(durl.flatMap((x) => [x.url, ...x.backup_url || []]));
      else console.warn("[BiliKit Feed] durl 为空 code=", j == null ? void 0 : j.code, j == null ? void 0 : j.message);
    } catch (e) {
      errored = true;
      console.warn("[BiliKit Feed] durl 取流失败:", (e == null ? void 0 : e.message) || e);
    }
    if (!errored) {
      durlCache.set(bvid, sources);
      lru(durlCache);
    }
    return sources;
  }
  const PLAY_WATCH = 1600;
  const OPEN_GUARD = 3e3;
  const HIGH_WATER = 15;
  const LOW_WATER = 8;
  const BATCH_BYTES = 5e5;
  const MAX_MEDIA_BYTES = 8e6;
  const MS_CTOR = () => window.ManagedMediaSource || window.MediaSource || null;
  const isMMS = () => !!window.ManagedMediaSource;
  async function fetchRange(urls, start, end, signal) {
    const range = `bytes=${start}-${end}`;
    let last;
    for (const u of urls) {
      if (signal.aborted) throw signal.reason || new DOMException("Aborted", "AbortError");
      try {
        const r = await fetch(u, { headers: { Range: range }, credentials: "omit", cache: "no-store", signal });
        if (r.status === 206) return await r.arrayBuffer();
        last = new Error("fetch HTTP " + r.status);
      } catch (e) {
        if (signal.aborted) throw e;
        last = e;
      }
    }
    for (const u of urls) {
      if (signal.aborted) throw signal.reason || new DOMException("Aborted", "AbortError");
      try {
        return await gmRequestBinary(u, { start, end }, signal);
      } catch (e) {
        if (signal.aborted) throw e;
        last = e;
      }
    }
    throw last || new Error("all urls failed");
  }
  function parseSidx(sidxBuf) {
    try {
      const dv = new DataView(sidxBuf);
      const N = dv.byteLength;
      const u8 = (p2) => p2 + 1 <= N ? dv.getUint8(p2) : 0;
      const u16 = (p2) => p2 + 2 <= N ? dv.getUint16(p2) : 0;
      const u32 = (p2) => p2 + 4 <= N ? dv.getUint32(p2) : 0;
      const typeAt = (p2) => String.fromCharCode(u8(p2 + 4), u8(p2 + 5), u8(p2 + 6), u8(p2 + 7));
      let pos = 0;
      if (typeAt(0) !== "sidx") {
        let p2 = 0, g = 0;
        while (p2 + 8 <= N && g++ < 32) {
          if (typeAt(p2) === "sidx") break;
          const s = u32(p2);
          if (s <= 0) return null;
          p2 += s;
        }
        if (p2 + 8 > N || typeAt(p2) !== "sidx") return null;
        pos = p2;
      }
      const version = u8(pos + 8);
      let p = pos + 12 + 4;
      p += 4;
      p += version === 0 ? 8 : 16;
      p += 2;
      const refCount = u16(p);
      p += 2;
      const sizes = [];
      for (let i = 0; i < refCount && p + 12 <= N; i++) {
        sizes.push(u32(p) & 2147483647);
        p += 12;
      }
      return sizes.length ? sizes : null;
    } catch {
      return null;
    }
  }
  function appendWait(sb, buf, signal) {
    return new Promise((res, rej) => {
      const clean = () => {
        sb.removeEventListener("updateend", ok);
        sb.removeEventListener("error", er);
        signal.removeEventListener("abort", onAbort);
      };
      const ok = () => {
        clean();
        res();
      };
      const er = () => {
        clean();
        rej(new Error("append error"));
      };
      const onAbort = () => {
        clean();
        try {
          if (sb.updating) sb.abort();
        } catch {
        }
        rej(signal.reason || new DOMException("Aborted", "AbortError"));
      };
      if (signal.aborted) {
        onAbort();
        return;
      }
      sb.addEventListener("updateend", ok);
      sb.addEventListener("error", er);
      signal.addEventListener("abort", onAbort, { once: true });
      try {
        sb.appendBuffer(buf);
      } catch (e) {
        clean();
        rej(e);
      }
    });
  }
  function attachMse(video, dash) {
    const MS = MS_CTOR();
    if (!MS) {
      console.debug("[BiliKit Feed] MSE 不可用:无 MediaSource");
      return Promise.resolve(false);
    }
    return new Promise((resolve) => {
      let settled = false, dead = false, objUrl = "";
      let ms = null, sb = null;
      const aborter = new AbortController();
      let openGuard = setTimeout(() => finish(false), OPEN_GUARD);
      let playWatch = null;
      const pumpListeners = [];
      const onPlaying = () => finish(true);
      const onVidErr = () => console.debug("[BiliKit Feed] MSE video error code=", video.error && video.error.code);
      video.addEventListener("playing", onPlaying);
      video.addEventListener("error", onVidErr, { once: true });
      const safeEnd = () => {
        try {
          if (ms && ms.readyState === "open" && sb && !sb.updating) ms.endOfStream();
        } catch {
        }
      };
      function dispose() {
        const resolvePending = !settled;
        if (resolvePending) settled = true;
        dead = true;
        aborter.abort();
        if (openGuard) {
          clearTimeout(openGuard);
          openGuard = null;
        }
        if (playWatch) {
          clearTimeout(playWatch);
          playWatch = null;
        }
        for (const [ev, h] of pumpListeners) video.removeEventListener(ev, h);
        pumpListeners.length = 0;
        video.removeEventListener("playing", onPlaying);
        video.removeEventListener("error", onVidErr);
        try {
          video.pause();
        } catch {
        }
        try {
          video.removeAttribute("src");
          video.load();
        } catch {
        }
        try {
          if (objUrl) URL.revokeObjectURL(objUrl);
        } catch {
        }
        objUrl = "";
        if (resolvePending) resolve(false);
      }
      video.__mseCleanup = dispose;
      function finish(ok) {
        if (settled) return;
        settled = true;
        if (openGuard) {
          clearTimeout(openGuard);
          openGuard = null;
        }
        if (playWatch) {
          clearTimeout(playWatch);
          playWatch = null;
        }
        video.removeEventListener("playing", onPlaying);
        video.removeEventListener("error", onVidErr);
        if (!ok) dispose();
        resolve(ok);
      }
      try {
        if (isMMS()) video.disableRemotePlayback = true;
        ms = new MS();
        objUrl = URL.createObjectURL(ms);
        video.src = objUrl;
        ms.addEventListener("error", () => finish(false));
        ms.addEventListener("sourceopen", async () => {
          if (openGuard) {
            clearTimeout(openGuard);
            openGuard = null;
          }
          try {
            const header = await fetchRange(dash.urls, 0, dash.indexEnd, aborter.signal);
            if (dead) return;
            const sizes = parseSidx(header.slice(dash.indexStart, dash.indexEnd + 1));
            sb = ms.addSourceBuffer(`video/mp4; codecs="${dash.codecs}"`);
            await appendWait(sb, header.slice(0, dash.initEnd + 1), aborter.signal);
            if (dead) return;
            const mediaBase = dash.indexEnd + 1;
            if (!sizes) {
              const media = await fetchRange(dash.urls, mediaBase, mediaBase + 6e5, aborter.signal);
              if (dead) return;
              await appendWait(sb, media, aborter.signal);
              safeEnd();
            } else {
              const offsets = [0];
              for (let i = 0; i < sizes.length; i++) offsets.push(offsets[i] + sizes[i]);
              let fi = 0, ended = false, pumping = false, fetched = 0;
              const ahead = () => {
                try {
                  const b = video.buffered;
                  return b.length ? b.end(b.length - 1) - video.currentTime : 0;
                } catch {
                  return 0;
                }
              };
              const doneAll = () => fi >= sizes.length || fetched >= MAX_MEDIA_BYTES;
              const pump = async () => {
                if (dead || pumping || !sb || sb.updating) return;
                if (doneAll()) {
                  if (!ended) {
                    ended = true;
                    safeEnd();
                  }
                  return;
                }
                if (ahead() > HIGH_WATER) return;
                pumping = true;
                try {
                  let n = 0, bytes = 0;
                  while (fi + n < sizes.length && bytes < BATCH_BYTES) {
                    bytes += sizes[fi + n];
                    n++;
                  }
                  const data = await fetchRange(dash.urls, mediaBase + offsets[fi], mediaBase + offsets[fi] + bytes - 1, aborter.signal);
                  if (dead) return;
                  await appendWait(sb, data, aborter.signal);
                  fi += n;
                  fetched += data.byteLength;
                } catch (e) {
                  if (dead) return;
                  console.debug("[BiliKit Feed] MSE 补拉失败:", e == null ? void 0 : e.message);
                  ended = true;
                  safeEnd();
                } finally {
                  pumping = false;
                }
                if (!dead && !doneAll() && ahead() < LOW_WATER) void pump();
              };
              const onTU = () => void pump();
              const onWait = () => {
                try {
                  const b = video.buffered;
                  if (b.length && video.currentTime < b.start(0)) video.currentTime = b.start(0);
                } catch {
                }
                void pump();
              };
              video.addEventListener("timeupdate", onTU);
              pumpListeners.push(["timeupdate", onTU]);
              video.addEventListener("waiting", onWait);
              pumpListeners.push(["waiting", onWait]);
              await pump();
            }
            if (dead) return;
            video.play().catch((e) => console.debug("[BiliKit Feed] MSE play() rej", e == null ? void 0 : e.name));
            playWatch = setTimeout(() => {
              console.debug("[BiliKit Feed] MSE 起播看门狗超时 rs=", video.readyState);
              finish(false);
            }, PLAY_WATCH);
          } catch (e) {
            if (dead) return;
            console.debug("[BiliKit Feed] MSE 装载失败:", (e == null ? void 0 : e.message) || e);
            finish(false);
          }
        }, { once: true });
      } catch (e) {
        console.debug("[BiliKit Feed] MSE 初始化失败:", (e == null ? void 0 : e.message) || e);
        finish(false);
      }
    });
  }
  const DELAY = 500;
  const FADE = 220;
  const MAX_ACTIVE = 3;
  const activeOrder = [];
  function registerActive(entry) {
    const i = activeOrder.indexOf(entry);
    if (i !== -1) activeOrder.splice(i, 1);
    activeOrder.push(entry);
    while (activeOrder.length > MAX_ACTIVE) {
      const idx = activeOrder.findIndex((x) => !x.isHovering());
      if (idx === -1) break;
      activeOrder.splice(idx, 1)[0].teardown();
    }
  }
  function unregisterActive(entry) {
    const i = activeOrder.indexOf(entry);
    if (i !== -1) activeOrder.splice(i, 1);
  }
  const fmt = (s) => {
    if (!isFinite(s) || s < 0) s = 0;
    const m = Math.floor(s / 60), ss = Math.floor(s % 60);
    return `${m}:${ss < 10 ? "0" : ""}${ss}`;
  };
  function setupVideoPreview(cover, bvid, cid) {
    let hovering = false;
    let enterTimer = null;
    let hideTimer = null;
    let video = null;
    let cands = [];
    let ci = 0;
    let t0 = 0;
    let mode = "";
    let attemptOk = false;
    const durEl = cover.querySelector(`.${NS}-mask > span`);
    const origDur = durEl ? durEl.textContent || "" : "";
    const ensureEls = () => {
      if (video) return;
      video = document.createElement("video");
      video.className = `${NS}-vpreview`;
      video.muted = true;
      video.loop = true;
      video.preload = "auto";
      video.setAttribute("playsinline", "");
      video.addEventListener("timeupdate", () => {
        if (video && video.duration && isFinite(video.duration) && cover.classList.contains("previewing")) {
          if (durEl) durEl.textContent = `${fmt(video.currentTime)} / ${fmt(video.duration)}`;
        }
      });
      video.addEventListener("error", () => {
        if (mode === "durl" && ci + 1 < cands.length) {
          console.warn(`[BiliKit Feed] durl 源#${ci} 失败,换下一个`);
          playDurl(ci + 1);
        } else if (mode === "durl") attemptOk = false;
      });
      cover.appendChild(video);
    };
    const playDurl = (i) => {
      if (!video || i >= cands.length) return;
      ci = i;
      mode = "durl";
      video.src = cands[i];
      video.load();
      video.play().catch(() => {
      });
    };
    const restoreCover = () => {
      cover.classList.remove("previewing");
      if (durEl) durEl.textContent = origDur;
    };
    const show = () => {
      if (!video) return;
      if (hideTimer) {
        clearTimeout(hideTimer);
        hideTimer = null;
      }
      attemptOk = true;
      cover.classList.add("previewing");
      video.classList.add("on");
      registerActive(selfEntry);
    };
    const stop = () => {
      hovering = false;
      if (enterTimer) {
        clearTimeout(enterTimer);
        enterTimer = null;
      }
      if (!video || !video.classList.contains("on")) {
        if (video) teardown();
        else restoreCover();
        return;
      }
      video.classList.remove("on");
      if (hideTimer) clearTimeout(hideTimer);
      hideTimer = setTimeout(() => {
        hideTimer = null;
        if (hovering) return;
        restoreCover();
        try {
          video == null ? void 0 : video.pause();
        } catch {
        }
      }, FADE);
    };
    const teardown = () => {
      var _a;
      hovering = false;
      if (enterTimer) {
        clearTimeout(enterTimer);
        enterTimer = null;
      }
      if (hideTimer) {
        clearTimeout(hideTimer);
        hideTimer = null;
      }
      restoreCover();
      attemptOk = false;
      unregisterActive(selfEntry);
      if (video) {
        try {
          (_a = video.__mseCleanup) == null ? void 0 : _a.call(video);
        } catch {
        }
        try {
          video.removeAttribute("src");
          video.load();
        } catch {
        }
        try {
          video.remove();
        } catch {
        }
        video = null;
      }
    };
    const selfEntry = { teardown, isHovering: () => hovering };
    cover.__bkTeardown = teardown;
    cover.addEventListener("mouseenter", () => {
      if (hovering) return;
      hovering = true;
      if (attemptOk && video && video.readyState >= 2 && !video.error) {
        video.play().catch(() => {
        });
        show();
        return;
      }
      t0 = performance.now();
      const dashP = getDashPreview(bvid, cid);
      enterTimer = setTimeout(async () => {
        enterTimer = null;
        if (!hovering || !cover.isConnected) return;
        ensureEls();
        const dash = await dashP;
        if (!hovering || !cover.isConnected) {
          stop();
          return;
        }
        let ok = false;
        if (dash && video) {
          mode = "mse";
          ok = await attachMse(video, dash);
          if (!hovering || !cover.isConnected) {
            teardown();
            return;
          }
          if (ok) console.debug(`[BiliKit Feed] MSE 起播 ${performance.now() - t0 | 0}ms ${bvid}`);
        }
        if (!ok) {
          const srcs = await getDurlSources(bvid, cid);
          if (!srcs || !srcs.length || !hovering || !cover.isConnected) {
            stop();
            return;
          }
          cands = srcs;
          playDurl(0);
        }
        if (!hovering || !cover.isConnected) {
          stop();
          return;
        }
        show();
      }, DELAY);
    });
    cover.addEventListener("mouseleave", stop);
  }
  function accessKey() {
    try {
      return JSON.parse(localStorage.getItem("bilikit:settings") || "{}")["feed.accessKey"] || "";
    } catch {
      return "";
    }
  }
  function biliJct() {
    const m = document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);
    return m ? decodeURIComponent(m[1]) : "";
  }
  const nowIdx = () => String(Math.floor(Date.now() / 1e3));
  async function feedDislike(card, reasonId, cancel) {
    const key = accessKey();
    if (!key) return { ok: false, message: "未配置 access_key,无法提交反馈" };
    const query = signAppQuery({
      access_key: key,
      build: "1",
      mobi_app: "iphone",
      device: "pad",
      goto: card.goto || "av",
      id: card.param || card.aid,
      reason_id: String(reasonId),
      idx: nowIdx()
    });
    const path = cancel ? "/x/feed/dislike/cancel" : "/x/feed/dislike";
    try {
      const text = await gmRequest({ method: "GET", url: `https://app.bilibili.com${path}?${query}` });
      const json = JSON.parse(text);
      return { ok: (json == null ? void 0 : json.code) === 0, message: (json == null ? void 0 : json.message) || ((json == null ? void 0 : json.code) === 0 ? "" : "失败") };
    } catch {
      return { ok: false, message: "网络错误" };
    }
  }
  async function webDislike(card, reasonId, cancel) {
    const csrf = biliJct();
    if (!csrf) return { ok: false, message: "需网页登录后才能反馈" };
    const path = cancel ? "/x/web-interface/feedback/dislike/cancel" : "/x/web-interface/feedback/dislike";
    const body = new URLSearchParams({
      app_id: "100",
      platform: "5",
      from_spmid: "",
      spmid: "333.1007.0.0",
      goto: card.goto || "av",
      id: card.aid || card.param,
      mid: card.mid || "0",
      track_id: card.trackId || "",
      feedback_page: "1",
      reason_id: String(reasonId),
      csrf
    });
    try {
      const r = await fetch(`https://api.bilibili.com${path}`, {
        method: "POST",
        credentials: "include",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: body.toString()
      });
      const json = await r.json();
      return { ok: (json == null ? void 0 : json.code) === 0, message: (json == null ? void 0 : json.message) || "" };
    } catch {
      return { ok: false, message: "网络错误" };
    }
  }
  const dislikeVideo = (c, reasonId) => c.source === "web" ? webDislike(c, reasonId, false) : feedDislike(c, reasonId, false);
  const undoDislikeVideo = (c, reasonId) => c.source === "web" ? webDislike(c, reasonId, true) : feedDislike(c, reasonId, true);
  async function toview(aid, del) {
    if (!aid) return { ok: false, message: "缺少视频 id" };
    const path = del ? "/x/v2/history/toview/del" : "/x/v2/history/toview/add";
    const csrf = biliJct();
    if (csrf) {
      try {
        const r = await fetch(`https://api.bilibili.com${path}`, {
          method: "POST",
          credentials: "include",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: `aid=${encodeURIComponent(aid)}&csrf=${encodeURIComponent(csrf)}`
        });
        const json = await r.json();
        return { ok: (json == null ? void 0 : json.code) === 0, message: (json == null ? void 0 : json.message) || "" };
      } catch {
        return { ok: false, message: "网络错误" };
      }
    }
    const key = accessKey();
    if (!key) return { ok: false, message: "需网页登录或在设置里配置 access_key" };
    const query = signAppQuery({ access_key: key, build: "1", mobi_app: "iphone", device: "pad", aid, idx: nowIdx() });
    try {
      const text = await gmRequest({ method: "POST", url: `https://api.bilibili.com${path}?${query}` });
      const json = JSON.parse(text);
      return { ok: (json == null ? void 0 : json.code) === 0, message: (json == null ? void 0 : json.message) || "" };
    } catch {
      return { ok: false, message: "网络错误" };
    }
  }
  const watchLaterAdd = (aid) => toview(aid, false);
  const watchLaterDel = (aid) => toview(aid, true);
  let toastEl = null;
  let toastTimer = null;
  function toast(msg) {
    if (!toastEl) {
      toastEl = document.createElement("div");
      toastEl.className = `${NS}-toast`;
      document.body.appendChild(toastEl);
    }
    toastEl.textContent = msg;
    void toastEl.offsetWidth;
    toastEl.classList.add("on");
    if (toastTimer) clearTimeout(toastTimer);
    toastTimer = setTimeout(() => toastEl == null ? void 0 : toastEl.classList.remove("on"), 2200);
  }
  const DEFAULT_OPEN_MODE = "newtab";
  const NEW_TAB_HISTORY_FLATTEN_KEY = "feed.newTabHistoryFlatten";
  const DEFAULT_NEW_TAB_HISTORY_FLATTEN = false;
  const WAYBACK_STACK_KEY = "bilikit-wayback-stack";
  const NEW_TAB_TARGET_PREFIX = "bilikit-newtab-flatten-";
  function isSafariUserAgent(userAgent, vendor) {
    return /Safari/i.test(userAgent) && /Apple Computer/i.test(vendor) && !/(?:Chrome|Chromium|CriOS|Edg|EdgiOS|Firefox|FxiOS|OPiOS)/i.test(userAgent);
  }
  function shouldUseSafariHistoryFlatten(enabled, userAgent, vendor) {
    return enabled && isSafariUserAgent(userAgent, vendor);
  }
  function newToken() {
    try {
      return crypto.randomUUID();
    } catch {
      return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
    }
  }
  function newHistoryFlattenTargetName(token = newToken()) {
    return `${NEW_TAB_TARGET_PREFIX}${token}`;
  }
  function openBiliKitVideoTab(url, enableHistoryFlatten) {
    const flatten = shouldUseSafariHistoryFlatten(
      enableHistoryFlatten,
      navigator.userAgent,
      navigator.vendor
    );
    if (!flatten) return window.open(url, "_blank", "noopener");
    let previousStack = null;
    try {
      previousStack = sessionStorage.getItem(WAYBACK_STACK_KEY);
      if (previousStack != null) sessionStorage.removeItem(WAYBACK_STACK_KEY);
    } catch {
    }
    try {
      return window.open(url, newHistoryFlattenTargetName());
    } finally {
      try {
        if (previousStack != null) sessionStorage.setItem(WAYBACK_STACK_KEY, previousStack);
      } catch {
      }
    }
  }
  const PLAY_SVG = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>';
  const DM_SVG = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h16a1.5 1.5 0 0 1 1.5 1.5v9A1.5 1.5 0 0 1 20 16H9l-5 4V5.5A1.5 1.5 0 0 1 5.5 4z"/></svg>';
  const WL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12a7.5 7.5 0 1 0 1.95-5.05"/><path d="M4 3.5V7h3.5"/><path d="M10.5 9l4.3 3-4.3 3z" fill="currentColor" stroke="none"/></svg>';
  const WL_DONE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>';
  const MORE_SVG = '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="12" cy="19" r="1.7"/></svg>';
  const UNDO_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M3.5 13a9 9 0 1 0 2.6-7.6L3 8"/></svg>';
  const stripUnit = (s) => s.replace(/观看|播放|弹幕|次/g, "").trim();
  function pickReasons(rs) {
    const upper = rs.find((r) => /^\s*up\s*主/i.test(r.name)) || rs.find((r) => /(作者|视频主)/.test(r.name));
    const notInterest = rs.find((r) => r.id === 1) || rs.find((r) => /不感兴趣|这个内容/.test(r.name)) || rs.find((r) => r !== upper);
    return { notInterest, upper };
  }
  function makeCard(c, beforeCurrentNavigation) {
    const el = document.createElement("div");
    el.className = `${NS}-card`;
    if (c.bvid) el.dataset.bvid = c.bvid;
    const mstat = (c.play ? `<span>${PLAY_SVG}${esc(stripUnit(c.play))}</span>` : "") + (c.danmaku ? `<span>${DM_SVG}${esc(stripUnit(c.danmaku))}</span>` : "");
    const badge = c.reason ? `<span class="${NS}-badge">${esc(c.reason)}</span>` : "";
    const who = `<span class="${NS}-up">${esc(c.up)}</span>` + (c.date ? `<i>·</i>${esc(c.date)}` : "");
    const sub = badge + `<span class="${NS}-who">${who}</span>`;
    const wlBtn = c.bvid ? `<button class="${NS}-wl ${NS}-noopen" type="button" title="稍后再看" aria-label="稍后再看">${WL_SVG}</button>` : "";
    const { notInterest, upper } = pickReasons(c.dislikeReasons || []);
    const menu = notInterest || upper ? `<div class="${NS}-more-wrap ${NS}-noopen"><button class="${NS}-more" type="button" title="我不想看" aria-label="我不想看">${MORE_SVG}</button><div class="${NS}-menu">` + (notInterest ? `<button class="${NS}-mi" type="button" data-rid="${notInterest.id}" data-lbl="不感兴趣">不感兴趣</button>` : "") + (upper ? `<button class="${NS}-mi" type="button" data-rid="${upper.id}" data-lbl="不想看此UP主">不想看此UP主</button>` : "") + `</div></div>` : "";
    const overlay = `<div class="${NS}-dov ${NS}-noopen"><div class="${NS}-dov-in"><div class="${NS}-dov-txt"></div><button class="${NS}-undo" type="button">${UNDO_SVG}<span>撤销</span></button></div></div>`;
    const cov = coverSized(c.cover);
    const pic = `<picture><source type="image/avif" data-srcset="${esc(cov.avif)}"><source type="image/webp" data-srcset="${esc(cov.webp)}"><img alt="" data-src="${esc(cov.jpg)}" decoding="async"></picture>`;
    el.innerHTML = `<div class="${NS}-cover">${pic}<div class="${NS}-mask"><div class="${NS}-mstat">${mstat}</div>` + (c.duration ? `<span>${esc(c.duration)}</span>` : "<span></span>") + `</div>` + wlBtn + `</div><div class="${NS}-bottom">` + (c.face ? `<img class="${NS}-face" src="${esc(coverUrl(c.face))}" alt="" loading="lazy" decoding="async">` : `<div class="${NS}-face"></div>`) + `<div class="${NS}-right"><div class="${NS}-title">${esc(c.title)}</div><div class="${NS}-sub">${sub}${menu}</div></div></div>` + overlay;
    const coverEl = el.querySelector(`.${NS}-cover`);
    const imgEl = el.querySelector("img");
    imgEl.addEventListener("load", () => {
      coverEl.classList.toggle("loaded", !imgEl.src.startsWith("data:"));
    });
    imgEl.addEventListener("error", () => {
      if (!imgEl.src.startsWith("data:")) coverEl.classList.add("failed");
    });
    if (c.bvid) {
      const pm = readSetting("feed.previewMode", "video");
      if (pm === "sprite") setupHoverPreview(coverEl, c.bvid);
      else if (pm !== "off") setupVideoPreview(coverEl, c.bvid, c.cid);
    }
    const wlEl = el.querySelector(`.${NS}-wl`);
    if (wlEl) wlEl.addEventListener("click", async (e) => {
      e.stopPropagation();
      if (wlEl.classList.contains("busy")) return;
      const added = wlEl.classList.contains("done");
      wlEl.classList.add("busy");
      const r = added ? await watchLaterDel(c.aid) : await watchLaterAdd(c.aid);
      wlEl.classList.remove("busy");
      if (!r.ok) {
        toast(r.message || (added ? "移出失败" : "添加失败"));
        return;
      }
      if (added) {
        wlEl.innerHTML = WL_SVG;
        wlEl.classList.remove("done");
        wlEl.title = "稍后再看";
        toast("已移出稍后再看");
      } else {
        wlEl.innerHTML = WL_DONE_SVG;
        wlEl.classList.add("done");
        wlEl.title = "已加入稍后再看(再点移出)";
        toast("已添加到稍后再看");
      }
    });
    const moreWrap = el.querySelector(`.${NS}-more-wrap`);
    if (moreWrap) {
      let lastRid = c.dislikedRid || 0;
      const moreBtn = moreWrap.querySelector(`.${NS}-more`);
      const setOpen = (on) => {
        moreWrap.classList.toggle("open", on);
        el.classList.toggle("menuopen", on);
      };
      moreBtn.addEventListener("click", (e) => {
        e.stopPropagation();
        setOpen(!moreWrap.classList.contains("open"));
      });
      el.querySelectorAll(`.${NS}-mi`).forEach((b) => b.addEventListener("click", async (e) => {
        e.stopPropagation();
        const btn = b;
        const rid = Number(btn.dataset.rid);
        const lbl = btn.dataset.lbl || "已标记不想看";
        setOpen(false);
        const r = await dislikeVideo(c, rid);
        if (!r.ok) {
          toast(r.message || "提交失败");
          return;
        }
        lastRid = rid;
        c.disliked = true;
        c.dislikedRid = rid;
        c.dislikedLbl = lbl;
        el.querySelector(`.${NS}-dov-txt`).textContent = lbl;
        el.classList.add("disliked");
      }));
      const undoEl = el.querySelector(`.${NS}-undo`);
      undoEl.addEventListener("click", (e) => {
        e.stopPropagation();
        c.disliked = false;
        el.classList.remove("disliked");
        undoDislikeVideo(c, lastRid).then((r) => {
          if (!r.ok) toast(r.message || "撤销失败");
        });
      });
      if (c.disliked) {
        el.querySelector(`.${NS}-dov-txt`).textContent = c.dislikedLbl || "已标记不想看";
        el.classList.add("disliked");
      }
    }
    el.addEventListener("click", (e) => {
      if (c.mid && e.target.closest(`.${NS}-face, .${NS}-up`)) {
        window.open(`https://space.bilibili.com/${c.mid}`, "_blank", "noopener");
        return;
      }
      if (c.bvid) {
        const url = `https://www.bilibili.com/video/${c.bvid}`;
        if (readSetting("feed.openMode", DEFAULT_OPEN_MODE) === "current") {
          beforeCurrentNavigation == null ? void 0 : beforeCurrentNavigation();
          location.href = url;
        } else openBiliKitVideoTab(
          url,
          readSetting(NEW_TAB_HISTORY_FLATTEN_KEY, DEFAULT_NEW_TAB_HISTORY_FLATTEN)
        );
        return;
      }
      if (c.uri && /^https?:\/\//i.test(c.uri)) window.open(c.uri, "_blank", "noopener");
    });
    return el;
  }
  function makeSkeleton() {
    const el = document.createElement("div");
    el.className = `${NS}-card ${NS}-skcard`;
    el.innerHTML = `<div class="${NS}-cover"></div><div class="${NS}-bottom"><div class="${NS}-face ${NS}-shimmer"></div><div class="${NS}-right"><div class="${NS}-shimmer ${NS}-skline"></div><div class="${NS}-shimmer ${NS}-skline" style="width:70%"></div><div class="${NS}-shimmer ${NS}-skline" style="width:45%;margin-top:6px"></div></div></div>`;
    return el;
  }
  let controls = null;
  let markerEl = null;
  let markerIo = null;
  const PHONE_SVG = '<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2" width="12" height="20" rx="2.5"/><line x1="10.5" y1="18" x2="13.5" y2="18"/></svg>';
  const PC_SVG = '<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3.5" width="20" height="13" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="16.5" x2="12" y2="21"/></svg>';
  const REFRESH_SVG = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/></svg>';
  const TOP_SVG = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>';
  function buildSourceSwitcher(initial, onSwitch) {
    let cur = initial;
    const wrap = document.createElement("div");
    wrap.className = `${NS}-src`;
    const iconOf = (s) => s === "app" ? PHONE_SVG : PC_SVG;
    wrap.innerHTML = `<button class="${NS}-src-cur" type="button" title="推荐来源" aria-label="切换推荐来源"></button><div class="${NS}-src-pop"><button class="${NS}-src-opt" data-src="app" type="button" title="App 推荐" aria-label="App 推荐">${PHONE_SVG}</button><button class="${NS}-src-opt" data-src="web" type="button" title="网页推荐" aria-label="网页推荐">${PC_SVG}</button></div>`;
    const curBtn = wrap.querySelector(`.${NS}-src-cur`);
    const paint = (s) => {
      cur = s;
      curBtn.innerHTML = iconOf(s);
      wrap.querySelectorAll(`.${NS}-src-opt`).forEach((o) => o.classList.toggle("on", o.dataset.src === s));
    };
    paint(cur);
    curBtn.addEventListener("click", (e) => {
      e.stopPropagation();
      wrap.classList.toggle("open");
    });
    wrap.querySelectorAll(`.${NS}-src-opt`).forEach((o) => o.addEventListener("click", (e) => {
      e.stopPropagation();
      const s = o.dataset.src;
      wrap.classList.remove("open");
      if (s === cur) return;
      paint(s);
      onSwitch(s);
    }));
    return wrap;
  }
  function mountControls(onRefresh, srcCtl) {
    if (controls && controls.isConnected) return;
    controls == null ? void 0 : controls.remove();
    markerEl == null ? void 0 : markerEl.remove();
    markerIo == null ? void 0 : markerIo.disconnect();
    const fab = document.createElement("div");
    fab.className = `${NS}-fab`;
    fab.innerHTML = `<button class="bk-top" title="返回顶部" aria-label="返回顶部">${TOP_SVG}</button><button class="bk-refresh" title="刷新内容" aria-label="刷新内容">${REFRESH_SVG}</button>`;
    const refreshBtn = fab.querySelector(".bk-refresh");
    refreshBtn.addEventListener("click", () => onRefresh(refreshBtn));
    fab.querySelector(".bk-top").addEventListener(
      "click",
      () => window.scrollTo({ top: 0, behavior: "smooth" })
    );
    if (srcCtl) fab.insertBefore(buildSourceSwitcher(srcCtl.initial, srcCtl.onSwitch), fab.firstChild);
    document.body.appendChild(fab);
    controls = fab;
    const marker = document.createElement("div");
    marker.style.cssText = "position:absolute;top:400px;left:0;width:1px;height:1px;pointer-events:none;";
    document.body.appendChild(marker);
    markerEl = marker;
    markerIo = new IntersectionObserver((es) => fab.classList.toggle("scrolled", !es[0].isIntersecting));
    markerIo.observe(marker);
  }
  const FEED_VERSION = "0.3.23";
  const KEY = "bilikit:feed.return-session";
  const VERSION = 1;
  const MAX_AGE_MS = 24 * 60 * 60 * 1e3;
  function isCard(value) {
    if (!value || typeof value !== "object") return false;
    const card = value;
    return typeof card.bvid === "string" && !!card.bvid && typeof card.title === "string" && typeof card.cover === "string" && (card.source === "app" || card.source === "web");
  }
  function parseFeedReturnSession(raw, now = Date.now()) {
    if (!raw) return null;
    try {
      const value = JSON.parse(raw);
      if (value.version !== VERSION || typeof value.savedAt !== "number" || now - value.savedAt > MAX_AGE_MS || value.savedAt > now + 6e4) return null;
      if (value.source !== "app" && value.source !== "web") return null;
      if (!Array.isArray(value.items) || !value.items.length || !value.items.every(isCard)) return null;
      return {
        version: VERSION,
        savedAt: value.savedAt,
        source: value.source,
        webFreshIdx: Math.max(1, Math.floor(Number(value.webFreshIdx) || 1)),
        exhausted: value.exhausted === true,
        scrollY: Math.max(0, Number(value.scrollY) || 0),
        items: value.items
      };
    } catch {
      return null;
    }
  }
  function saveFeedReturnSession(session, storage = sessionStorage, now = Date.now()) {
    try {
      const value = { version: VERSION, savedAt: now, ...session };
      storage.setItem(KEY, JSON.stringify(value));
      return true;
    } catch (error) {
      console.warn("[BiliKit Feed] 保存返回状态失败,将按普通当前页导航:", error);
      return false;
    }
  }
  function takeFeedReturnSession(now = Date.now(), storage = sessionStorage) {
    let raw;
    try {
      raw = storage.getItem(KEY);
    } catch {
      return null;
    }
    const value = parseFeedReturnSession(raw, now);
    try {
      storage.removeItem(KEY);
    } catch {
    }
    return value;
  }
  const seen = /* @__PURE__ */ new Set();
  let grid = null;
  let sentinel = null;
  let topSpacer = null;
  let bottomSpacer = null;
  let loading = false;
  let exhausted = false;
  let cardIo = null;
  let sentinelIo = null;
  let gridRo = null;
  let lastGridW = 0;
  let feedGen = 0;
  const items = [];
  const nodes = /* @__PURE__ */ new Map();
  const MAX_ITEMS = 2e3;
  let cachedCols = 1;
  let cachedRowH = 0;
  let cachedGridTop = 0;
  let metricsDirty = true;
  let lastStart = -1, lastEnd = -1, lastTotalRows = -1;
  let renderRaf = 0;
  let suppressScroll = false;
  let cooldownUntil = 0;
  let source = (() => {
    try {
      return localStorage.getItem("bilikit:feed.tab") === "web" ? "web" : "app";
    } catch {
      return "app";
    }
  })();
  let webFreshIdx = 1;
  function getAccessKey() {
    try {
      return JSON.parse(localStorage.getItem("bilikit:settings") || "{}")["feed.accessKey"] || "";
    } catch {
      return "";
    }
  }
  let darkProbe = null;
  function pageIsDark() {
    if (!darkProbe) {
      darkProbe = document.createElement("div");
      darkProbe.style.cssText = "position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;background:var(--bg2,#fff);pointer-events:none";
      (document.body || document.documentElement).appendChild(darkProbe);
    }
    const m = getComputedStyle(darkProbe).backgroundColor.match(/\d+(?:\.\d+)?/g);
    if (!m) return false;
    return 0.299 * +m[0] + 0.587 * +m[1] + 0.114 * +m[2] < 128;
  }
  function metrics() {
    if (!metricsDirty && cachedRowH > 0) return { cols: cachedCols, rowH: cachedRowH, gridTop: cachedGridTop };
    const cs = getComputedStyle(grid);
    const parts = cs.gridTemplateColumns.split(" ").filter(Boolean);
    const cols = parts.length && parts.every((p) => p.endsWith("px")) ? parts.length : cachedCols;
    cachedCols = cols;
    let cardH = 330;
    let measured = false;
    const first = nodes.size ? nodes.values().next().value : null;
    if (first && first.offsetHeight > 50) {
      cardH = first.offsetHeight;
      measured = true;
    }
    const rowGap = parseFloat(cs.rowGap) || 22;
    const rowH = cardH + rowGap;
    const gridTop = topSpacer ? topSpacer.getBoundingClientRect().top + window.scrollY : 0;
    if (measured) {
      cachedRowH = rowH;
      cachedGridTop = gridTop;
      metricsDirty = false;
    }
    return { cols, rowH, gridTop };
  }
  function invalidateLayout() {
    metricsDirty = true;
    lastStart = -1;
    lastEnd = -1;
    lastTotalRows = -1;
  }
  function teardownCard(el) {
    var _a;
    const cover = el.querySelector(`.${NS}-cover`);
    (_a = cover == null ? void 0 : cover.__bkTeardown) == null ? void 0 : _a.call(cover);
  }
  function teardownRenderedCards() {
    for (const el of nodes.values()) {
      cardIo == null ? void 0 : cardIo.unobserve(el);
      teardownCard(el);
      el.remove();
    }
    nodes.clear();
  }
  function saveCurrentFeedForReturn() {
    if (!items.length) return;
    saveFeedReturnSession({
      source,
      webFreshIdx,
      exhausted,
      scrollY: window.scrollY,
      items
    });
  }
  function render() {
    if (!grid || !sentinel || !topSpacer || !bottomSpacer) return;
    if (!items.length) {
      topSpacer.style.height = "0px";
      bottomSpacer.style.height = "0px";
      lastStart = lastEnd = lastTotalRows = -1;
      return;
    }
    const { cols, rowH, gridTop } = metrics();
    const totalRows = Math.ceil(items.length / cols);
    const into = window.scrollY - gridTop;
    const vh = window.innerHeight;
    const BUF = vh * 1.5;
    const firstRow = Math.max(0, Math.floor((into - BUF) / rowH));
    const lastRow = Math.min(totalRows - 1, Math.max(0, Math.ceil((into + vh + BUF) / rowH)));
    const startIdx = firstRow * cols;
    const endIdx = Math.min(items.length, (lastRow + 1) * cols);
    if (startIdx === lastStart && endIdx === lastEnd && totalRows === lastTotalRows) return;
    lastStart = startIdx;
    lastEnd = endIdx;
    lastTotalRows = totalRows;
    const anchor = firstRow > 0 ? nodes.get(Math.max(0, Math.floor(into / rowH)) * cols) || null : null;
    const anchorTop = anchor ? anchor.offsetTop : 0;
    for (const [i, el] of nodes) {
      if (i < startIdx || i >= endIdx) {
        cardIo == null ? void 0 : cardIo.unobserve(el);
        teardownCard(el);
        el.remove();
        nodes.delete(i);
      }
    }
    topSpacer.style.height = firstRow * rowH + "px";
    bottomSpacer.style.height = Math.max(0, (totalRows - (lastRow + 1)) * rowH) + "px";
    for (let i = startIdx; i < endIdx; i++) {
      if (nodes.has(i)) continue;
      const el = makeCard(items[i], saveCurrentFeedForReturn);
      nodes.set(i, el);
      let ref = bottomSpacer;
      for (let j = i + 1; j < endIdx; j++) {
        const n = nodes.get(j);
        if (n) {
          ref = n;
          break;
        }
      }
      grid.insertBefore(el, ref);
      cardIo == null ? void 0 : cardIo.observe(el);
    }
    if (anchor) {
      const delta = anchor.offsetTop - anchorTop;
      if (Math.abs(delta) > 0.5) {
        suppressScroll = true;
        window.scrollBy(0, delta);
      }
    }
  }
  function scheduleRender() {
    if (suppressScroll) {
      suppressScroll = false;
      return;
    }
    if (renderRaf) return;
    renderRaf = requestAnimationFrame(() => {
      renderRaf = 0;
      render();
    });
  }
  function clearAll() {
    if (cardIo) cardIo.disconnect();
    teardownRenderedCards();
    items.length = 0;
    if (topSpacer) topSpacer.style.height = "0px";
    if (bottomSpacer) bottomSpacer.style.height = "0px";
    invalidateLayout();
  }
  function renderSkeletons(n) {
    if (!grid || !bottomSpacer) return;
    const frag = document.createDocumentFragment();
    for (let i = 0; i < n; i++) frag.appendChild(makeSkeleton());
    grid.insertBefore(frag, bottomSpacer);
  }
  function clearSkeletons() {
    if (grid) grid.querySelectorAll(`.${NS}-skcard`).forEach((n) => n.remove());
  }
  function sentinelInView() {
    if (!sentinel) return false;
    return sentinel.getBoundingClientRect().top < window.innerHeight + Math.max(window.innerHeight, 1200);
  }
  function showTip(text) {
    if (!grid) return;
    let tip = grid.querySelector(`.${NS}-tip`);
    if (!tip) {
      tip = document.createElement("div");
      tip.className = `${NS}-tip`;
      grid.appendChild(tip);
    }
    tip.textContent = text;
  }
  function removeTip() {
    var _a;
    (_a = grid == null ? void 0 : grid.querySelector(`.${NS}-tip`)) == null ? void 0 : _a.remove();
  }
  function hasRealCard() {
    return !!grid && !!grid.querySelector(`.${NS}-card:not(.${NS}-skcard)`);
  }
  async function loadMore() {
    if (loading || exhausted || !grid || !sentinel) return;
    if (performance.now() < cooldownUntil) return;
    loading = true;
    const gen = feedGen;
    let failed = false;
    try {
      let emptyStreak = 0;
      let first = true;
      while ((first || sentinelInView()) && emptyStreak < 3 && items.length < MAX_ITEMS) {
        first = false;
        const { code, message, cards } = source === "web" ? await fetchWebFeed(webFreshIdx++) : await fetchAppFeed(getAccessKey());
        if (gen !== feedGen) return;
        if (code !== 0) {
          console.warn(`[BiliKit Feed] 加载失败 code=${code} ${message}`);
          failed = true;
          break;
        }
        clearSkeletons();
        removeTip();
        let addedThisPage = 0;
        for (const c of cards) {
          if (!c.bvid || seen.has(c.bvid)) continue;
          seen.add(c.bvid);
          items.push(c);
          addedThisPage++;
          if (items.length >= MAX_ITEMS) break;
        }
        if (addedThisPage) render();
        emptyStreak = addedThisPage === 0 ? emptyStreak + 1 : 0;
      }
      if (items.length >= MAX_ITEMS) {
        exhausted = true;
        showTip(`本次已加载 ${MAX_ITEMS} 条,为限制长会话内存已暂停追加;刷新内容可继续。`);
      } else if (emptyStreak >= 3) {
        if (source === "app" && !getAccessKey()) {
          exhausted = true;
          showTip("匿名推荐已刷完(B 站给匿名请求的是固定内容池)。配置 access_key 可看个性化、不重复的推荐。");
        } else {
          cooldownUntil = performance.now() + 3e3;
        }
      }
    } catch (e) {
      console.error("[BiliKit Feed] 加载出错:", e);
      failed = true;
    } finally {
      if (gen === feedGen) {
        clearSkeletons();
        loading = false;
        if (failed) cooldownUntil = performance.now() + 3e3;
      }
    }
    if (gen === feedGen && failed && !hasRealCard()) showTip("加载失败,请稍后重试;若持续失败可在设置里配置 access_key 或检查网络。");
  }
  function refreshFeed(btn) {
    if (!grid || !sentinel) return;
    feedGen++;
    loading = false;
    clearAll();
    removeTip();
    seen.clear();
    exhausted = false;
    webFreshIdx = 1;
    cooldownUntil = 0;
    renderSkeletons(12);
    if (btn) {
      btn.classList.add("busy");
      void loadMore().finally(() => btn.classList.remove("busy"));
    } else {
      void loadMore();
    }
    window.scrollTo({ top: 0, behavior: "smooth" });
  }
  function switchSource(s) {
    if (s === source) return;
    source = s;
    try {
      localStorage.setItem("bilikit:feed.tab", s);
    } catch {
    }
    webFreshIdx = 1;
    refreshFeed();
  }
  function findNativeFeed() {
    const card = document.querySelector(".feed-card, .bili-video-card");
    const byCard = card && card.closest(".container");
    if (byCard) return byCard;
    return [...document.querySelectorAll(".container")].find((c) => c.querySelector(".feed-card, .bili-video-card")) || null;
  }
  function takeover() {
    if (grid && grid.isConnected) return true;
    const native = findNativeFeed();
    if (!native || !native.parentElement) return false;
    if (gridRo) {
      gridRo.disconnect();
      gridRo = null;
    }
    if (cardIo) cardIo.disconnect();
    if (sentinelIo) sentinelIo.disconnect();
    if (renderRaf) {
      cancelAnimationFrame(renderRaf);
      renderRaf = 0;
    }
    teardownRenderedCards();
    document.querySelectorAll(`.${NS}`).forEach((g) => g.remove());
    feedGen++;
    loading = false;
    items.length = 0;
    seen.clear();
    exhausted = false;
    cooldownUntil = 0;
    invalidateLayout();
    const returnSession = takeFeedReturnSession();
    if (returnSession) {
      source = returnSession.source;
      webFreshIdx = returnSession.webFreshIdx;
      exhausted = returnSession.exhausted;
      items.push(...returnSession.items.slice(0, MAX_ITEMS));
      for (const card of items) seen.add(card.bvid);
    }
    injectStyle();
    native.style.setProperty("display", "none", "important");
    cardIo = new IntersectionObserver(
      (ents) => {
        var _a;
        for (const e of ents) {
          const card = e.target;
          const img = card.querySelector("img");
          const sources = card.querySelectorAll("picture source[data-srcset]");
          if (e.isIntersecting) {
            if (img && (!img.getAttribute("src") || img.src.startsWith("data:")) && img.dataset.src) {
              (_a = img.parentElement) == null ? void 0 : _a.classList.remove("failed");
              sources.forEach((s) => {
                const ss = s.dataset.srcset;
                if (ss) s.srcset = ss;
              });
              img.src = img.dataset.src;
            }
          } else {
            if (img && img.src && !img.src.startsWith("data:")) {
              sources.forEach((s) => {
                s.srcset = "";
              });
              img.src = BLANK;
            }
          }
        }
      },
      { rootMargin: "1000px 0px" }
    );
    grid = document.createElement("div");
    grid.className = NS;
    grid.classList.toggle("bk-dark", pageIsDark());
    topSpacer = document.createElement("div");
    topSpacer.className = `${NS}-spacer`;
    bottomSpacer = document.createElement("div");
    bottomSpacer.className = `${NS}-spacer`;
    sentinel = document.createElement("div");
    sentinel.className = `${NS}-sentinel`;
    grid.append(topSpacer, bottomSpacer, sentinel);
    native.parentElement.insertBefore(grid, native);
    sentinelIo = new IntersectionObserver((es) => {
      if (es.some((e) => e.isIntersecting)) loadMore();
    }, { rootMargin: "1000px 0px" });
    sentinelIo.observe(sentinel);
    lastGridW = 0;
    if ("ResizeObserver" in window) {
      gridRo = new ResizeObserver((es) => {
        const w = es[0].contentRect.width;
        if (w && w !== lastGridW) {
          lastGridW = w;
          invalidateLayout();
          scheduleRender();
        }
      });
      gridRo.observe(grid);
    }
    mountControls((btn) => refreshFeed(btn), { initial: source, onSwitch: switchSource });
    if (returnSession) {
      const scrollY = returnSession.scrollY;
      render();
      requestAnimationFrame(() => {
        invalidateLayout();
        render();
        window.scrollTo(0, scrollY);
        scheduleRender();
        if (!exhausted && sentinelInView()) void loadMore();
      });
    } else {
      renderSkeletons(12);
      void loadMore();
    }
    return true;
  }
  const REPO = "https://github.com/shiinayane/BiliKit";
  function warnCoreMissing() {
    if (!grid || !topSpacer) return;
    if (localStorage.getItem("bilikit:dismiss.core-missing") || grid.querySelector(`.${NS}-warn`)) return;
    const bar = document.createElement("div");
    bar.className = `${NS}-warn`;
    bar.innerHTML = `<span>未检测到 <b>BiliKit Core</b>:登录、设置、抽屉净化都需要它。</span><a href="${REPO}" target="_blank" rel="noopener">前往安装</a><button class="bk-x" aria-label="关闭">✕</button>`;
    bar.querySelector(".bk-x").addEventListener("click", () => {
      try {
        localStorage.setItem("bilikit:dismiss.core-missing", "1");
      } catch {
      }
      bar.remove();
    });
    grid.insertBefore(bar, topSpacer);
    invalidateLayout();
  }
  function checkCore() {
    const alive = Number(localStorage.getItem("bilikit:alive.core") || 0);
    if (Date.now() - alive > 15e3) warnCoreMissing();
  }
  function mountFeed() {
    if (window.top !== window.self) return;
    const beat = () => {
      try {
        localStorage.setItem("bilikit:alive.feed", String(Date.now()));
      } catch {
      }
    };
    beat();
    let lastHeartbeatAt = Date.now();
    try {
      localStorage.setItem("bilikit:feed.version", FEED_VERSION);
    } catch {
    }
    window.addEventListener("scroll", scheduleRender, { passive: true });
    window.addEventListener("resize", () => {
      invalidateLayout();
      scheduleRender();
    });
    let themeRaf = 0;
    const syncDark = () => {
      if (themeRaf) return;
      themeRaf = requestAnimationFrame(() => {
        themeRaf = 0;
        if (grid) grid.classList.toggle("bk-dark", pageIsDark());
      });
    };
    try {
      new MutationObserver(syncDark).observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
    } catch {
    }
    const onHome = () => location.pathname === "/" || location.pathname === "/index.html";
    const tick = () => {
      if (onHome()) {
        hideNativeChrome();
        takeover();
      }
    };
    tick();
    setTimeout(() => {
      if (onHome()) checkCore();
    }, 2500);
    setInterval(() => {
      if (!onHome()) return;
      const now = Date.now();
      if (now - lastHeartbeatAt >= 5e3) {
        beat();
        lastHeartbeatAt = now;
      }
      hideNativeChrome();
      takeover();
    }, 2e3);
  }
  mountFeed();

})();