知乎历史记录

给知乎添加历史记录

// ==UserScript==
// @name       知乎历史记录
// @namespace  https://maxchang.me
// @version    1.2.1
// @author     Max Chang
// @license    MIT
// @icon       https://static.zhihu.com/heifetz/favicon.ico
// @match      https://www.zhihu.com/
// @match      https://www.zhihu.com/follow*
// @match      https://www.zhihu.com/hot*
// @match      https://www.zhihu.com/column-square*
// @match      https://www.zhihu.com/search?*
// @match      https://www.zhihu.com/topic/*
// @grant      GM_addStyle
// @grant      GM_getValue
// @grant      GM_info
// @grant      GM_registerMenuCommand
// @grant      GM_setValue
// @grant      unsafeWindow
// @description 给知乎添加历史记录
// ==/UserScript==

(r=>{if(typeof GM_addStyle=="function"){GM_addStyle(r);return}const o=document.createElement("style");o.textContent=r,document.head.append(o)})(' ._item_1ee58_1{padding:var(--spacing-md) 0;border-bottom:1px solid #f0f0f0;display:flex;flex-direction:column}._item_1ee58_1:last-child{border-bottom:none}._link_1ee58_12{flex:1;color:var(--text-color);text-decoration:none;min-width:0}._link_1ee58_12:hover,._link_1ee58_12:focus{color:var(--primary-color)}._header_1ee58_24{display:flex}._visitTime_1ee58_28{color:var(--text-secondary);font-size:var(--font-size-sm);white-space:nowrap;flex-shrink:0}._title_1ee58_35{flex:1;font-weight:500;transition:color .2s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}._content_1ee58_45{color:var(--text-secondary);font-size:var(--font-size-sm);margin:var(--spacing-sm) 0 0 0;overflow:hidden;text-overflow:ellipsis;word-break:break-word;display:-webkit-box;line-clamp:2;-webkit-box-orient:vertical;-webkit-line-clamp:2}._content_1ee58_45 em{font-style:normal;background-color:#ffe60066;border-radius:2px;padding:0 2px;margin:0 -2px;font-weight:500}._answer_1ee58_68:before,._article_1ee58_69:before,._pin_1ee58_70:before{background-color:var(--primary-bg);font-weight:700;font-size:var(--font-size-sm);padding:1px var(--spacing-sm) 0;border-radius:var(--border-radius-sm);margin-right:var(--spacing-sm);display:inline-block}._answer_1ee58_68:before{content:"\u95EE\u9898";color:#2196f3}._article_1ee58_69:before{content:"\u6587\u7AE0";color:#004b87}._pin_1ee58_70:before{content:"\u60F3\u6CD5";color:#60a912}._srOnly_1ee58_95{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}._dialog_rd0y7_2,._dialog_rd0y7_2::backdrop{transition:display .25s allow-discrete,overlay .25s allow-discrete,opacity .25s;opacity:0}._dialog_rd0y7_2[open],._dialog_rd0y7_2[open]::backdrop{opacity:1}@starting-style{._dialog_rd0y7_2[open],._dialog_rd0y7_2[open]::backdrop{opacity:0}}._dialog_rd0y7_2{padding:0;border:0;border-radius:var(--border-radius);box-shadow:0 4px 12px var(--shadow-color);background-color:#fff;max-width:800px;width:80%;overflow:hidden;-webkit-user-select:text!important;user-select:text!important}._dialog_rd0y7_2::backdrop{background-color:var(--backdrop-color)}._content_rd0y7_36{padding:var(--spacing-lg) var(--spacing-xl);outline:none}._header_nqn5f_1{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--spacing-sm);border-bottom:1px solid #eee;padding-bottom:var(--spacing-md)}._title_nqn5f_10{margin:0;font-size:18px;color:var(--text-color)}._body_nqn5f_16{max-height:70vh;overflow-y:auto}._list_nqn5f_21{list-style:none;margin:0;display:flex;flex-direction:column;padding:0 1.5em}._container_1sav8_1{position:relative;flex:1;margin:0 var(--spacing-lg);display:flex;align-items:center}._input_1sav8_9{flex:1;width:100%;padding:var(--spacing-sm) var(--spacing-md);padding-right:calc(var(--spacing-md) * 3);border:1px solid #eee;border-radius:var(--border-radius);font-size:var(--font-size-md);color:var(--text-color);background-color:#f9f9f9;transition:border-color .2s}._input_1sav8_9:focus{outline:none;border-color:var(--primary-light)}._clearButton_1sav8_27{position:absolute;right:var(--spacing-sm);background:none;border:none;cursor:pointer;color:var(--text-secondary);font-size:12px;padding:var(--spacing-sm);display:flex;align-items:center;justify-content:center;border-radius:50%;transition:background-color .2s}._clearButton_1sav8_27:hover{background-color:#eee}._info_1sav8_47{padding:var(--spacing-sm) var(--spacing-lg);font-size:var(--font-size-sm);color:var(--text-secondary);background-color:#f5f5f5;margin-bottom:var(--spacing-md);border-radius:var(--border-radius-sm)}._emptyState_1sav8_56{text-align:center;padding:var(--spacing-xl);color:var(--text-secondary);font-style:italic}._historyCard_4h12a_1{background:#fff;border-radius:var(--border-radius-sm);box-shadow:0 1px 3px var(--shadow-color);margin-bottom:10px;padding:5px 0}._historyButton_4h12a_9{margin:0 18px;display:flex;justify-content:center;align-items:center;border:1px solid var(--primary-light);background:transparent;color:var(--primary-color);border-radius:var(--border-radius);height:40px;font-size:var(--font-size-md);cursor:pointer;width:calc(100% - 36px)}:root{--primary-color: rgb(5, 109, 232);--primary-light: rgba(5, 109, 232, .5);--primary-bg: rgba(33, 150, 243, .2);--text-color: #333;--text-secondary: #666;--shadow-color: hsla(0, 0%, 7%, .1);--backdrop-color: hsla(0, 0%, 7%, .65);--border-radius-sm: 2px;--border-radius: 4px;--spacing-sm: 4px;--spacing-md: 8px;--spacing-lg: 16px;--spacing-xl: 25px;--font-size-sm: 13px;--font-size-md: 14px} ');

(function (require$$1, ReactDOM) {
  'use strict';

  var jsxRuntime = { exports: {} };
  var reactJsxRuntime_production_min = {};
  /*
  object-assign
  (c) Sindre Sorhus
  @license MIT
  */
  var objectAssign;
  var hasRequiredObjectAssign;
  function requireObjectAssign() {
    if (hasRequiredObjectAssign) return objectAssign;
    hasRequiredObjectAssign = 1;
    var getOwnPropertySymbols = Object.getOwnPropertySymbols;
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    var propIsEnumerable = Object.prototype.propertyIsEnumerable;
    function toObject(val) {
      if (val === null || val === void 0) {
        throw new TypeError("Object.assign cannot be called with null or undefined");
      }
      return Object(val);
    }
    function shouldUseNative() {
      try {
        if (!Object.assign) {
          return false;
        }
        var test1 = new String("abc");
        test1[5] = "de";
        if (Object.getOwnPropertyNames(test1)[0] === "5") {
          return false;
        }
        var test2 = {};
        for (var i = 0; i < 10; i++) {
          test2["_" + String.fromCharCode(i)] = i;
        }
        var order2 = Object.getOwnPropertyNames(test2).map(function(n) {
          return test2[n];
        });
        if (order2.join("") !== "0123456789") {
          return false;
        }
        var test3 = {};
        "abcdefghijklmnopqrst".split("").forEach(function(letter) {
          test3[letter] = letter;
        });
        if (Object.keys(Object.assign({}, test3)).join("") !== "abcdefghijklmnopqrst") {
          return false;
        }
        return true;
      } catch (err) {
        return false;
      }
    }
    objectAssign = shouldUseNative() ? Object.assign : function(target, source) {
      var from;
      var to = toObject(target);
      var symbols;
      for (var s = 1; s < arguments.length; s++) {
        from = Object(arguments[s]);
        for (var key in from) {
          if (hasOwnProperty.call(from, key)) {
            to[key] = from[key];
          }
        }
        if (getOwnPropertySymbols) {
          symbols = getOwnPropertySymbols(from);
          for (var i = 0; i < symbols.length; i++) {
            if (propIsEnumerable.call(from, symbols[i])) {
              to[symbols[i]] = from[symbols[i]];
            }
          }
        }
      }
      return to;
    };
    return objectAssign;
  }
  /** @license React v17.0.2
   * react-jsx-runtime.production.min.js
   *
   * Copyright (c) Facebook, Inc. and its affiliates.
   *
   * This source code is licensed under the MIT license found in the
   * LICENSE file in the root directory of this source tree.
   */
  var hasRequiredReactJsxRuntime_production_min;
  function requireReactJsxRuntime_production_min() {
    if (hasRequiredReactJsxRuntime_production_min) return reactJsxRuntime_production_min;
    hasRequiredReactJsxRuntime_production_min = 1;
    requireObjectAssign();
    var f = require$$1, g = 60103;
    reactJsxRuntime_production_min.Fragment = 60107;
    if ("function" === typeof Symbol && Symbol.for) {
      var h = Symbol.for;
      g = h("react.element");
      reactJsxRuntime_production_min.Fragment = h("react.fragment");
    }
    var m = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner, n = Object.prototype.hasOwnProperty, p = { key: true, ref: true, __self: true, __source: true };
    function q(c, a, k) {
      var b, d = {}, e = null, l = null;
      void 0 !== k && (e = "" + k);
      void 0 !== a.key && (e = "" + a.key);
      void 0 !== a.ref && (l = a.ref);
      for (b in a) n.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
      if (c && c.defaultProps) for (b in a = c.defaultProps, a) void 0 === d[b] && (d[b] = a[b]);
      return { $$typeof: g, type: c, key: e, ref: l, props: d, _owner: m.current };
    }
    reactJsxRuntime_production_min.jsx = q;
    reactJsxRuntime_production_min.jsxs = q;
    return reactJsxRuntime_production_min;
  }
  var hasRequiredJsxRuntime;
  function requireJsxRuntime() {
    if (hasRequiredJsxRuntime) return jsxRuntime.exports;
    hasRequiredJsxRuntime = 1;
    {
      jsxRuntime.exports = requireReactJsxRuntime_production_min();
    }
    return jsxRuntime.exports;
  }
  var jsxRuntimeExports = requireJsxRuntime();
  var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_info = /* @__PURE__ */ (() => typeof GM_info != "undefined" ? GM_info : void 0)();
  var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  const item = "_item_1ee58_1";
  const link = "_link_1ee58_12";
  const header$1 = "_header_1ee58_24";
  const visitTime = "_visitTime_1ee58_28";
  const title$1 = "_title_1ee58_35";
  const content$1 = "_content_1ee58_45";
  const answer = "_answer_1ee58_68";
  const article = "_article_1ee58_69";
  const pin = "_pin_1ee58_70";
  const srOnly = "_srOnly_1ee58_95";
  const Item = {
    item,
    link,
    header: header$1,
    visitTime,
    title: title$1,
    content: content$1,
    answer,
    article,
    pin,
    srOnly
  };
  const formatTime = (date) => {
    const now = /* @__PURE__ */ new Date();
    const diff = Math.floor((now.getTime() - date.getTime()) / 1e3);
    if (diff < 60) return "刚刚";
    if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
    if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
    return date.toLocaleDateString("zh-CN");
  };
  const highlightTextWithPositions = (text, fieldPositions) => {
    if (!fieldPositions || fieldPositions.length === 0) return text;
    const highlightMarkers = new Array(text.length).fill(false);
    for (const { start, end } of fieldPositions) {
      const endIndex = Math.min(end, text.length);
      for (let i = start; i < endIndex; i++) {
        highlightMarkers[i] = true;
      }
    }
    const segments = [];
    let currentSegment = null;
    for (let i = 0; i < text.length; i++) {
      const shouldHighlight = highlightMarkers[i];
      if (!currentSegment || currentSegment.highlight !== shouldHighlight) {
        if (currentSegment) segments.push(currentSegment);
        currentSegment = {
          text: text[i],
          highlight: shouldHighlight
        };
        continue;
      }
      currentSegment.text += text[i];
    }
    if (currentSegment) segments.push(currentSegment);
    return segments.map(
      (segment, index) => segment.highlight ? /* @__PURE__ */ jsxRuntimeExports.jsx("em", { children: segment.text }, index) : /* @__PURE__ */ jsxRuntimeExports.jsx(require$$1.Fragment, { children: segment.text }, index)
    );
  };
  const HistoryItem = require$$1.forwardRef(({ item: item2, searchResult }, ref) => {
    const contentTypeMap = {
      answer: "问题",
      article: "文章",
      pin: "想法"
    };
    const visitTime2 = !item2.visitTime ? null : new Date(item2.visitTime);
    const formattedVisitTime = !visitTime2 ? null : {
      short: formatTime(visitTime2),
      full: visitTime2.toLocaleString("zh-CN")
    };
    const highlightedTitle = require$$1.useMemo(
      () => {
        var _a;
        return highlightTextWithPositions(item2.title, (_a = searchResult == null ? void 0 : searchResult.matches) == null ? void 0 : _a.title);
      },
      [item2.title, searchResult]
    );
    const highlightedContent = require$$1.useMemo(() => {
      var _a;
      if (!item2.content) return null;
      return highlightTextWithPositions(item2.content, (_a = searchResult == null ? void 0 : searchResult.matches) == null ? void 0 : _a.content);
    }, [item2.content, searchResult]);
    const highlightedAuthorName = require$$1.useMemo(() => {
      var _a;
      if (formattedVisitTime || !searchResult) return null;
      return highlightTextWithPositions(item2.authorName, (_a = searchResult.matches) == null ? void 0 : _a.authorName);
    }, [item2.authorName, formattedVisitTime, searchResult]);
    return /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: Item.item, children: [
      /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: item2.url, className: Item.link, ref, children: [
        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: Item.srOnly, children: contentTypeMap[item2.type] }),
        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: Item.header, children: [
          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `${Item.title} ${Item[item2.type]}`, children: highlightedTitle }),
          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: Item.visitTime, title: formattedVisitTime == null ? void 0 : formattedVisitTime.full, "aria-hidden": true, tabIndex: -1, children: (formattedVisitTime == null ? void 0 : formattedVisitTime.short) ?? (highlightedAuthorName || item2.authorName) })
        ] }),
        // 没有访问时间的是之前的历史记录,没有包含作者的 content,所以需要提示作者
        !formattedVisitTime && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: Item.srOnly, children: [
          "作者:",
          item2.authorName
        ] }),
        formattedVisitTime && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: Item.srOnly, children: [
          "浏览于",
          /* @__PURE__ */ jsxRuntimeExports.jsx("time", { dateTime: formattedVisitTime.short, children: formattedVisitTime.short })
        ] })
      ] }),
      item2.content && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: Item.content, children: highlightedContent })
    ] });
  });
  const dialog = "_dialog_rd0y7_2";
  const content = "_content_rd0y7_36";
  const styles$1 = {
    dialog,
    content
  };
  const Dialog = ({ isOpen, onClose, children, initialFocusRef, className = "" }) => {
    const dialogRef = require$$1.useRef(null);
    require$$1.useEffect(() => {
      var _a;
      const dialogElement = dialogRef.current;
      if (!dialogElement) return;
      if (isOpen) {
        dialogElement.showModal();
        document.body.style.overflow = "hidden";
        (_a = initialFocusRef == null ? void 0 : initialFocusRef.current) == null ? void 0 : _a.focus();
      } else if (dialogElement.open) {
        dialogElement.close();
        document.body.style.overflow = "";
      }
    }, [isOpen, initialFocusRef]);
    const handleClose = () => {
      onClose();
    };
    return ReactDOM.createPortal(
      /* @__PURE__ */ jsxRuntimeExports.jsx(
        "dialog",
        {
          ref: dialogRef,
          className: `${styles$1.dialog} ${className}`,
          onClose: handleClose,
          onClick: (e) => {
            if (e.target === dialogRef.current) {
              handleClose();
            }
          },
          onKeyDown: (e) => {
            if (e.key === "Escape") {
              handleClose();
            }
          },
          children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: styles$1.content, children })
        }
      ),
      document.body
    );
  };
  const createSegmenter = () => {
    if (Intl == null ? void 0 : Intl.Segmenter) {
      const segmenterInstance = new Intl.Segmenter("zh", { granularity: "word" });
      return (text) => {
        const trimmedText = text.trim();
        if (!trimmedText) return [];
        const ignoreWords = /* @__PURE__ */ new Set([
          "的",
          "了",
          "是",
          "在",
          "和",
          "有",
          "就",
          "不",
          "也",
          "这",
          "那",
          "吗",
          "吧",
          "啊",
          "哦",
          "啦",
          "呀",
          "!",
          "?",
          ",",
          "。",
          "、",
          ";",
          ":",
          "“",
          "”",
          "‘",
          "’",
          "《",
          "》",
          "[",
          "]",
          "{",
          "}",
          ".",
          "(",
          ")",
          "【",
          "】",
          "——",
          "—",
          "…",
          "·"
        ]);
        const segments = Array.from(segmenterInstance.segment(trimmedText)).map((item2) => item2.segment.trim()).filter((word) => word && !ignoreWords.has(word));
        const uniqueTerms = /* @__PURE__ */ new Set([...segments, trimmedText]);
        return Array.from(uniqueTerms);
      };
    }
    return (text) => {
      const trimmedText = text.trim();
      if (!trimmedText) return [];
      const parts = trimmedText.split(/\s+/).map((part) => part.trim()).filter(Boolean);
      const uniqueTerms = /* @__PURE__ */ new Set([...parts, trimmedText]);
      return Array.from(uniqueTerms);
    };
  };
  const segmenter = createSegmenter();
  const isItemMatch = (item2, term) => {
    if (!term) return true;
    const lowerTerm = term.toLowerCase();
    const { title: title2, content: content2, authorName } = item2;
    if (title2.toLowerCase().includes(lowerTerm)) return true;
    if (content2 == null ? void 0 : content2.toLowerCase().includes(lowerTerm)) return true;
    return authorName.toLowerCase().includes(lowerTerm);
  };
  const findAllMatches = (text, searchTerm) => {
    if (!text) return [];
    const result = [];
    const termLower = searchTerm.toLowerCase();
    let startIndex = 0;
    let matchIndex;
    while ((matchIndex = text.toLowerCase().indexOf(termLower, startIndex)) !== -1) {
      result.push({
        start: matchIndex,
        end: matchIndex + searchTerm.length,
        term: searchTerm
      });
      startIndex = matchIndex + 1;
    }
    return result;
  };
  const searchItem = (items, term) => {
    if (!term) return /* @__PURE__ */ new Map();
    const result = /* @__PURE__ */ new Map();
    const searchTerms = segmenter(term);
    items.forEach((item2, index) => {
      let hasMatches = false;
      const itemResult = {
        terms: [],
        matches: {}
      };
      for (const searchTerm of searchTerms) {
        if (!isItemMatch(item2, searchTerm)) continue;
        if (!itemResult.terms.includes(searchTerm)) {
          itemResult.terms.push(searchTerm);
        }
        hasMatches = true;
        const fields = ["title", "authorName"];
        if (item2.content) fields.push("content");
        fields.forEach((field) => {
          const text = item2[field];
          const matches = findAllMatches(text, searchTerm);
          if (matches.length > 0) {
            if (!itemResult.matches[field]) {
              itemResult.matches[field] = [];
            }
            itemResult.matches[field].push(...matches);
          }
        });
      }
      if (hasMatches) result.set(index, itemResult);
    });
    return result;
  };
  const log = (logMethod, tag, ...args) => {
    const colors = {
      log: "#2c3e50",
      error: "#ff4500",
      warn: "#f39c12"
    };
    const fontFamily = "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;";
    console[logMethod](
      `%c ${_GM_info.script.name} %c ${tag} `,
      `padding: 2px 6px; border-radius: 3px 0 0 3px; color: #fff; background: #056de8; font-weight: bold; ${fontFamily}`,
      `padding: 2px 6px; border-radius: 0 3px 3px 0; color: #fff; background: ${colors[logMethod]}; font-weight: bold; ${fontFamily}`,
      ...args
    );
  };
  const logger = {
    log: (...args) => log("log", "日志", ...args),
    error: (...args) => log("error", "错误", ...args),
    warn: (...args) => log("warn", "警告", ...args)
  };
  class Result {
    constructor(inner) {
      this.inner = inner;
    }
    static Ok(value) {
      return new Result({ ok: true, value });
    }
    static Err(error) {
      return new Result({ ok: false, error });
    }
    static try(fn) {
      try {
        const val = fn();
        const okValue = val === void 0 ? null : val;
        return Result.Ok(okValue);
      } catch (err) {
        return Result.Err(err instanceof Error ? err : new Error(String(err)));
      }
    }
    isOk() {
      return this.inner.ok;
    }
    isErr() {
      return !this.inner.ok;
    }
    unwrap() {
      if (this.inner.ok) return this.inner.value;
      throw new Error("Tried to unwrap Err: " + JSON.stringify(this.inner.error));
    }
    unwrapErr() {
      if (!this.inner.ok) return this.inner.error;
      throw new Error("Tried to unwrapErr Ok: " + JSON.stringify(this.inner.value));
    }
    unwrapOr(defaultValue) {
      return this.inner.ok ? this.inner.value : defaultValue;
    }
    map(fn) {
      return this.inner.ok ? Result.Ok(fn(this.inner.value)) : Result.Err(this.inner.error);
    }
    mapErr(fn) {
      return this.inner.ok ? Result.Ok(this.inner.value) : Result.Err(fn(this.inner.error));
    }
    andThen(fn) {
      return this.inner.ok ? fn(this.inner.value) : Result.Err(this.inner.error);
    }
    match(handlers) {
      return this.inner.ok ? handlers.Ok(this.inner.value) : handlers.Err(this.inner.error);
    }
  }
  const STORAGE_KEY = "ZH_HISTORY";
  const HISTORY_LIMIT_KEY = "HISTORY_LIMIT";
  const DEFAULT_HISTORY_LIMIT = 20;
  const HISTORY_LIMIT = _GM_getValue(HISTORY_LIMIT_KEY) || DEFAULT_HISTORY_LIMIT;
  const setHistoryLimit = (limit) => {
    const numericLimit = Number(limit);
    if (!Number.isNaN(numericLimit) && numericLimit > 0) {
      _GM_setValue(HISTORY_LIMIT_KEY, numericLimit);
      return Result.Ok(null);
    }
    return Result.Err("输入无效,请输入一个正整数");
  };
  const saveHistory = (item2) => Result.try(() => {
    const raw = _GM_getValue(STORAGE_KEY);
    const historyItems = raw ? JSON.parse(raw) : [];
    const existingIndex = historyItems.findIndex((i) => i.itemId === item2.itemId);
    if (existingIndex !== -1) {
      historyItems.splice(existingIndex, 1);
    }
    historyItems.push(item2);
    if (historyItems.length > HISTORY_LIMIT) {
      historyItems.splice(0, historyItems.length - HISTORY_LIMIT);
    }
    _GM_setValue(STORAGE_KEY, JSON.stringify(historyItems));
  }).mapErr((error) => `保存浏览历史失败:${error}`);
  const importHistory = (history, merge = false) => {
    const parsedHistory = Result.try(() => JSON.parse(history)).unwrapOr([]).reverse();
    if (!Array.isArray(parsedHistory) || parsedHistory.length === 0) {
      return Result.Err("导入的历史记录格式不正确或为空数组");
    }
    let historyItems = getHistory(false);
    let mergeCount = 0;
    if (merge) {
      parsedHistory.forEach((item2) => {
        const existingIndex = historyItems.findIndex((i) => i.itemId === item2.itemId);
        if (existingIndex !== -1) {
          historyItems.splice(existingIndex, 1);
          mergeCount++;
        }
        historyItems.push(item2);
      });
    } else {
      historyItems = parsedHistory;
    }
    if (historyItems.length > HISTORY_LIMIT) {
      historyItems.splice(0, historyItems.length - HISTORY_LIMIT);
    }
    historyItems.sort((a, b) => (a.visitTime || 0) - (b.visitTime || 0));
    _GM_setValue(STORAGE_KEY, JSON.stringify(historyItems));
    return Result.Ok(
      `成功导入 ${parsedHistory.length} 条历史记录` + (mergeCount > 0 ? `,合并了 ${mergeCount} 条重复记录` : "")
    );
  };
  const migrateToGMStorage = () => Result.try(() => {
    logger.log("检测到旧的浏览历史数据,正在转换...");
    const raw = localStorage.getItem(STORAGE_KEY);
    if (raw) {
      _GM_setValue(STORAGE_KEY, raw);
      localStorage.removeItem(STORAGE_KEY);
    }
    logger.log("转换浏览历史数据成功");
  });
  const getHistory = (reverse = true) => Result.try(() => {
    if (localStorage.getItem(STORAGE_KEY) !== null) {
      const migrationResult = migrateToGMStorage();
      migrationResult.mapErr((error) => {
        logger.error("历史记录转换失败:", error);
      });
    }
    const raw = _GM_getValue(STORAGE_KEY);
    return raw ? reverse ? JSON.parse(raw).reverse() : JSON.parse(raw) : [];
  }).match({
    Ok: (history) => history,
    Err: (error) => {
      logger.error("获取浏览历史失败:", error);
      return [];
    }
  });
  const clearHistory = () => Result.try(() => {
    _GM_setValue(STORAGE_KEY, null);
  });
  function throttle(delay, callback, options) {
    var _ref = options || {}, _ref$noTrailing = _ref.noTrailing, noTrailing = _ref$noTrailing === void 0 ? false : _ref$noTrailing, _ref$noLeading = _ref.noLeading, noLeading = _ref$noLeading === void 0 ? false : _ref$noLeading, _ref$debounceMode = _ref.debounceMode, debounceMode = _ref$debounceMode === void 0 ? void 0 : _ref$debounceMode;
    var timeoutID;
    var cancelled = false;
    var lastExec = 0;
    function clearExistingTimeout() {
      if (timeoutID) {
        clearTimeout(timeoutID);
      }
    }
    function cancel(options2) {
      var _ref2 = options2 || {}, _ref2$upcomingOnly = _ref2.upcomingOnly, upcomingOnly = _ref2$upcomingOnly === void 0 ? false : _ref2$upcomingOnly;
      clearExistingTimeout();
      cancelled = !upcomingOnly;
    }
    function wrapper() {
      for (var _len = arguments.length, arguments_ = new Array(_len), _key = 0; _key < _len; _key++) {
        arguments_[_key] = arguments[_key];
      }
      var self = this;
      var elapsed = Date.now() - lastExec;
      if (cancelled) {
        return;
      }
      function exec() {
        lastExec = Date.now();
        callback.apply(self, arguments_);
      }
      function clear() {
        timeoutID = void 0;
      }
      if (!noLeading && debounceMode && !timeoutID) {
        exec();
      }
      clearExistingTimeout();
      if (debounceMode === void 0 && elapsed > delay) {
        if (noLeading) {
          lastExec = Date.now();
          if (!noTrailing) {
            timeoutID = setTimeout(debounceMode ? clear : exec, delay);
          }
        } else {
          exec();
        }
      } else if (noTrailing !== true) {
        timeoutID = setTimeout(debounceMode ? clear : exec, debounceMode === void 0 ? delay - elapsed : delay);
      }
    }
    wrapper.cancel = cancel;
    return wrapper;
  }
  function debounce(delay, callback, options) {
    var _ref = options || {}, _ref$atBegin = _ref.atBegin, atBegin = _ref$atBegin === void 0 ? false : _ref$atBegin;
    return throttle(delay, callback, {
      debounceMode: atBegin !== false
    });
  }
  function useDebouncedState(initialValue, delay = 300) {
    const [value, setValue] = require$$1.useState(initialValue);
    const [debouncedValue, setDebouncedValue] = require$$1.useState(initialValue);
    require$$1.useEffect(() => {
      const handler = debounce(delay, setDebouncedValue, {
        atBegin: true
      });
      handler(value);
      return () => {
        var _a;
        (_a = handler.cancel) == null ? void 0 : _a.call(handler);
      };
    }, [value, delay]);
    return [value, debouncedValue, setValue];
  }
  const header = "_header_nqn5f_1";
  const title = "_title_nqn5f_10";
  const body = "_body_nqn5f_16";
  const list = "_list_nqn5f_21";
  const Viewer = {
    header,
    title,
    body,
    list
  };
  const container = "_container_1sav8_1";
  const input = "_input_1sav8_9";
  const clearButton = "_clearButton_1sav8_27";
  const info = "_info_1sav8_47";
  const emptyState = "_emptyState_1sav8_56";
  const Search = {
    container,
    input,
    clearButton,
    info,
    emptyState
  };
  const SearchBox = ({ searchTerm, onSearchChange, placeholder = "搜索历史记录" }) => {
    const handleChange = (e) => {
      onSearchChange(e.target.value);
    };
    const clearSearch = () => {
      onSearchChange("");
    };
    return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: Search.container, children: [
      /* @__PURE__ */ jsxRuntimeExports.jsx(
        "input",
        {
          type: "text",
          placeholder,
          className: Search.input,
          value: searchTerm,
          onChange: handleChange,
          "aria-label": placeholder
        }
      ),
      searchTerm && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: Search.clearButton, onClick: clearSearch, "aria-label": "清除搜索", children: "✕" })
    ] });
  };
  const SearchStatus = ({ totalCount, matchedCount }) => {
    if (totalCount === 0) return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: Search.emptyState, children: "暂无浏览历史" });
    if (matchedCount !== -1) {
      if (matchedCount === 0) return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: Search.emptyState, children: "没有找到匹配的历史记录" });
      return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: Search.info, children: [
        "找到 ",
        matchedCount,
        " 条匹配结果"
      ] });
    }
    return null;
  };
  const HistoryViewer = ({ isOpen, onClose }) => {
    const [searchTerm, debouncedValue, setSearchTerm] = useDebouncedState("", 300);
    const historyItems = getHistory();
    const matchedItems = require$$1.useMemo(() => searchItem(historyItems, debouncedValue), [historyItems, debouncedValue]);
    const firstItemRef = require$$1.useRef(null);
    return /* @__PURE__ */ jsxRuntimeExports.jsxs(Dialog, { isOpen, onClose, initialFocusRef: firstItemRef, children: [
      /* @__PURE__ */ jsxRuntimeExports.jsxs("header", { className: Viewer.header, children: [
        /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: Viewer.title, children: "浏览历史" }),
        /* @__PURE__ */ jsxRuntimeExports.jsx(SearchBox, { searchTerm, onSearchChange: setSearchTerm })
      ] }),
      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: Viewer.body, children: [
        /* @__PURE__ */ jsxRuntimeExports.jsx(SearchStatus, { totalCount: historyItems.length, matchedCount: searchTerm ? matchedItems.size : -1 }),
        /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: Viewer.list, children: historyItems.map((item2, i) => {
          const isMatch = !searchTerm || matchedItems.has(i);
          if (!isMatch) return null;
          return /* @__PURE__ */ jsxRuntimeExports.jsx(
            HistoryItem,
            {
              item: item2,
              searchResult: matchedItems.get(i),
              ref: i === 0 ? firstItemRef : null
            },
            item2.itemId
          );
        }) })
      ] })
    ] });
  };
  const historyCard = "_historyCard_4h12a_1";
  const historyButton = "_historyButton_4h12a_9";
  const styles = {
    historyCard,
    historyButton
  };
  const SidebarEntry = () => {
    const [isDialogOpen, setIsDialogOpen] = require$$1.useState(false);
    require$$1.useEffect(() => {
      const handleKeyDown = (event) => {
        const target = event.target;
        const isEditableTarget = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || target.tagName === "SELECT";
        if (event.key === "h" && !isEditableTarget) {
          setIsDialogOpen((prev) => !prev);
        }
      };
      window.addEventListener("keydown", handleKeyDown);
      return () => {
        window.removeEventListener("keydown", handleKeyDown);
      };
    }, []);
    return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.historyCard, role: "complementary", children: [
      /* @__PURE__ */ jsxRuntimeExports.jsx(
        "button",
        {
          className: styles.historyButton,
          onClick: () => setIsDialogOpen(true),
          "aria-label": "历史记录,打开后按 Esc 关闭",
          "aria-haspopup": "dialog",
          type: "button",
          children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "历史记录" })
        }
      ),
      /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryViewer, { isOpen: isDialogOpen, onClose: () => setIsDialogOpen(false) })
    ] });
  };
  const App = () => {
    return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(SidebarEntry, {}) });
  };
  const clearHistoryCommand = [
    "🗑 清空浏览历史记录",
    () => {
      alert(
        clearHistory().match({
          Ok: () => "清空成功",
          Err: (error) => `清空失败: ${error}`
        })
      );
    }
  ];
  const setHistoryLimitCommand = [
    `🔢 设置数量限制(当前:${HISTORY_LIMIT})`,
    () => {
      const input2 = prompt(`请输入新的历史记录最大数量(默认 ${DEFAULT_HISTORY_LIMIT})`);
      if (!input2) return;
      alert(
        setHistoryLimit(input2).match({
          Ok: () => "设置成功",
          Err: (error) => `设置失败: ${error}`
        })
      );
    }
  ];
  const exportDataCommand = [
    "📤 导出历史记录",
    () => {
      const history = getHistory();
      const blob = new Blob([JSON.stringify(history)], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "zhihu_history.json";
      a.click();
      URL.revokeObjectURL(url);
    }
  ];
  const importDataCommand = [
    "📂 导入历史记录",
    () => {
      const input2 = document.createElement("input");
      input2.type = "file";
      input2.accept = ".json";
      input2.onchange = async (event) => {
        var _a;
        const file = (_a = event.target.files) == null ? void 0 : _a[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = (e) => {
          var _a2;
          const content2 = (_a2 = e.target) == null ? void 0 : _a2.result;
          if (typeof content2 !== "string") return;
          const merge = confirm(
            '⚠️危险操作 - 是否合并数据:\n\n• 点击"确定":合并数据(注意:超出限制的项将被删除)\n• 点击"取消":覆盖现有数据\n📋 建议先导出备份您的历史记录!'
          );
          alert(
            importHistory(content2, merge).match({
              Ok: (msg) => `导入成功: ${msg}`,
              Err: (error) => `导入失败: ${error}`
            })
          );
        };
        reader.readAsText(file);
      };
      input2.click();
    }
  ];
  const registerMenuCommand = (command) => _GM_registerMenuCommand(...command);
  const registerMenuCommands = () => [clearHistoryCommand, setHistoryLimitCommand, exportDataCommand, importDataCommand].forEach(registerMenuCommand);
  const CONTENT_TYPE = ["answer", "article", "pin"];
  const getPageType = (pathname) => {
    switch (pathname) {
      case "/":
      case "/follow":
      case "/hot":
      case "/column-square":
        return "home";
      case "/search":
        return "search";
      default:
        if (pathname.startsWith("/topic")) return "topic";
        return null;
    }
  };
  function isIn(values, x) {
    return values.includes(x);
  }
  const extendMetadata = (item2, rawMetadata) => {
    var _a;
    rawMetadata.visitTime = Date.now();
    const extractMetadata = () => {
      const span = item2.querySelector(".RichText");
      if (!span) return void 0;
      let text = span.innerText.trim();
      if (!text.startsWith(rawMetadata.authorName)) text = `${rawMetadata.authorName}:${text}`;
      return text.length > 120 ? `${text.slice(0, 120)}...` : text;
    };
    switch (rawMetadata.type) {
      case "pin": {
        const userLink = (_a = item2.closest(".Feed")) == null ? void 0 : _a.querySelector(".UserLink-link");
        if (userLink) rawMetadata.authorName = userLink.innerText.trim();
        rawMetadata.url = `https://www.zhihu.com/pin/${rawMetadata.itemId}`;
        break;
      }
      case "article":
      case "answer": {
        if (!rawMetadata.url) {
          const linkEl = item2.querySelector(".ContentItem-title a");
          if (linkEl) rawMetadata.url = linkEl.href;
        }
        rawMetadata.content = extractMetadata();
        break;
      }
    }
    return rawMetadata;
  };
  const extractMetadataFromZop = (item2) => {
    const zop = item2.dataset.zop;
    if (!zop) return Result.Err(`无法读取回答或文章信息:${JSON.stringify(item2.dataset)}`);
    return Result.try(() => JSON.parse(zop)).mapErr((err) => `解析数据失败:${err}`);
  };
  const extractMetadataFromSearch = (item2) => {
    var _a, _b;
    const truncateText = (text, maxLength = 120) => `${text.substring(0, maxLength)}...`;
    const hotLandingId = item2.getAttribute("name");
    const type = item2.getAttribute("itemprop") || // 如果没有 `itemprop` 属性,检查是否有关注按钮,如果有的话,说明是单独的问题
    (item2.querySelector(".FollowButton") ? "answer" : void 0);
    if (!type) return Result.Err(`元素缺少 itemprop 属性:${truncateText(item2.outerHTML)}`);
    if (!isIn(CONTENT_TYPE, type))
      return Result.Err(`元素 itemprop 值不合法:"${type}",支持的类型:${CONTENT_TYPE.join(", ")}`);
    const authorName = ((_a = item2.querySelector("b[data-first-child]")) == null ? void 0 : _a.textContent) || // 未展开的回答
    ((_b = item2.querySelector(".AuthorInfo-name")) == null ? void 0 : _b.textContent) || // 已经展开的回答
    "";
    if (hotLandingId) {
      const newItem = item2.closest(".HotLanding-contentItem");
      if (newItem) {
        item2 = newItem;
      }
    }
    const linkEl = item2.querySelector("a");
    if (!linkEl) return Result.Err(`元素缺少链接标签:${truncateText(item2.outerHTML)}`);
    const url = linkEl.href + (hotLandingId ? `/answer/${hotLandingId}` : "");
    const titleElement = item2.querySelector(".ContentItem-title");
    if (!titleElement) return Result.Err(`元素缺少标题标签:${truncateText(item2.outerHTML)}`);
    const title2 = titleElement.innerText.trim();
    if (!title2) return Result.Err(`元素的标题内容为空`);
    const itemId = url.split("/").pop();
    if (!itemId) return Result.Err(`无法从 URL 中提取 itemId:${url}`);
    return Result.Ok({
      authorName,
      type,
      itemId,
      url,
      title: title2
    });
  };
  const saveHistoryFromElement = (item2, extractMetadata) => extractMetadata(item2).map((data) => extendMetadata(item2, data)).andThen(saveHistory);
  const saveHistoryFromHomePageElement = (item2) => saveHistoryFromElement(item2, extractMetadataFromZop);
  const saveHistoryFromSearchElement = (item2) => saveHistoryFromElement(item2, extractMetadataFromSearch);
  const trackZopHistory = (selector) => {
    const container2 = document.querySelector(selector);
    if (!container2) {
      logger.error("未找到首页推荐容器");
      return;
    }
    const handleContentItem = (getItem) => {
      const item2 = getItem();
      if (!item2) return;
      saveHistoryFromHomePageElement(item2).mapErr((err) => logger.error(err));
    };
    container2.addEventListener("click", (e) => {
      const target = e.target;
      if (!(target instanceof HTMLElement)) return;
      handleContentItem(() => target.closest(".ContentItem"));
    });
    container2.addEventListener("keydown", (e) => {
      if (e.key !== "o") return;
      const target = e.target;
      if (!(target instanceof HTMLElement)) return;
      handleContentItem(() => target.querySelector(".ContentItem"));
    });
  };
  const trackSearchHistory = () => {
    const params = new URLSearchParams(location.search);
    if (params.get("type") !== "content") return;
    const container2 = document.querySelector(".Search-container");
    if (!container2) {
      logger.error("未找到搜索结果容器");
      return;
    }
    const handleContentItem = (getItem) => {
      var _a, _b;
      let item2 = getItem();
      if (!item2) return;
      if (((_a = item2.dataset) == null ? void 0 : _a.zaDetailViewPathModule) === "Content") {
        const newItem = (_b = item2.parentElement) == null ? void 0 : _b.querySelectorAll(".ContentItem")[1];
        if (newItem) item2 = newItem;
      }
      saveHistoryFromSearchElement(item2).mapErr((err) => logger.error(err));
    };
    container2.addEventListener("click", (e) => {
      const target = e.target;
      if (!(target instanceof HTMLElement)) return;
      handleContentItem(() => target.closest(".ContentItem"));
    });
    container2.addEventListener("keydown", (e) => {
      if (e.key !== "o") return;
      const target = e.target;
      if (!(target instanceof HTMLElement)) return;
      handleContentItem(() => target.querySelector(".ContentItem"));
    });
  };
  const getContentSelector = (pageType) => {
    switch (pageType) {
      case "home":
        return "#TopstoryContent";
      case "topic":
        return "#TopicMain";
      default:
        return null;
    }
  };
  const trackHistory = () => {
    const pageType = getPageType(location.pathname);
    if (!pageType) {
      logger.error(`当前页面类型不支持:${location.pathname}`);
      return;
    }
    switch (pageType) {
      case "home":
      case "topic": {
        const selector = getContentSelector(pageType);
        if (selector) trackZopHistory(selector);
        break;
      }
      case "search":
        trackSearchHistory();
        break;
    }
  };
  console.log(
    "%c知乎历史记录",
    "color:#1772F6; font-weight:bold; font-size:3em; padding:5px; text-shadow:1px 1px 3px rgba(0,0,0,0.7)"
  );
  trackHistory();
  const mountApp = () => {
    var _a;
    const container2 = document.createElement("div");
    container2.id = "zh-history-root";
    const target = (_a = document.querySelector('a[aria-label="边栏锚点"]')) == null ? void 0 : _a.parentElement;
    if (!target) {
      logger.warn("未找到挂载点。");
      return;
    }
    target.insertBefore(container2, target.firstChild);
    ReactDOM.render(/* @__PURE__ */ jsxRuntimeExports.jsx(App, {}), container2);
  };
  mountApp();
  registerMenuCommands();
  logger.log(`初始化成功,版本:${_GM_info.script.version}`);

})(unsafeWindow.React, unsafeWindow.ReactDOM);