知乎历史记录

给知乎添加历史记录

// ==UserScript==
// @name       知乎历史记录
// @namespace  https://maxchang.me
// @version    1.0.1
// @author     Max Chang
// @license    MIT
// @icon       https://static.zhihu.com/heifetz/favicon.ico
// @match      https://www.zhihu.com/
// @match      https://www.zhihu.com/search*
// @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)})(' :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}._historyCard_qpr22_19{background:#fff;border-radius:var(--border-radius-sm);box-shadow:0 1px 3px var(--shadow-color);margin-bottom:10px;padding:5px 0}._historyButton_qpr22_27{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)}._dialog_qpr22_42{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%}._dialog_qpr22_42::backdrop{background-color:var(--backdrop-color)}._dialogContent_qpr22_56{padding:var(--spacing-lg) var(--spacing-xl);outline:none}._dialogHeader_qpr22_61{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--spacing-sm);border-bottom:1px solid #eee;padding-bottom:var(--spacing-md)}._dialogTitle_qpr22_70{margin:0;font-size:18px;color:var(--text-color)}._closeButton_qpr22_76{background:none;border:none;cursor:pointer;font-size:16px;color:var(--text-secondary);padding:var(--spacing-sm);border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color .2s}._closeButton_qpr22_76:hover{background-color:#f0f0f0}._dialogBody_qpr22_94{max-height:70vh;overflow-y:auto}._historyList_qpr22_99{list-style:none;margin:0;display:flex;flex-direction:column-reverse;padding:0 1.5em}._historyItem_qpr22_108{padding:var(--spacing-md) 0;border-bottom:1px solid #f0f0f0;display:flex;align-items:baseline;gap:var(--spacing-md)}._historyItem_qpr22_108:last-child{border-bottom:none}._link_qpr22_120{text-decoration:none;color:var(--text-color);font-weight:500;transition:color .2s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;display:inline-block}._link_qpr22_120:hover{color:var(--primary-color)}._authorInfo_qpr22_136{color:var(--text-secondary);font-size:var(--font-size-sm);white-space:nowrap;flex-shrink:0}._answer_qpr22_143:before{content:"\u95EE\u9898";color:#2196f3;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}._article_qpr22_155:before{content:"\u6587\u7AE0";color:#004b87;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}._emptyState_qpr22_167{text-align:center;padding:var(--spacing-xl);color:var(--text-secondary);font-style:italic} ');

(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 historyCard = "_historyCard_qpr22_19";
  const historyButton = "_historyButton_qpr22_27";
  const dialog = "_dialog_qpr22_42";
  const dialogContent = "_dialogContent_qpr22_56";
  const dialogHeader = "_dialogHeader_qpr22_61";
  const dialogTitle = "_dialogTitle_qpr22_70";
  const closeButton = "_closeButton_qpr22_76";
  const dialogBody = "_dialogBody_qpr22_94";
  const historyList = "_historyList_qpr22_99";
  const historyItem = "_historyItem_qpr22_108";
  const link = "_link_qpr22_120";
  const authorInfo = "_authorInfo_qpr22_136";
  const answer = "_answer_qpr22_143";
  const article = "_article_qpr22_155";
  const emptyState = "_emptyState_qpr22_167";
  const styles = {
    historyCard,
    historyButton,
    dialog,
    dialogContent,
    dialogHeader,
    dialogTitle,
    closeButton,
    dialogBody,
    historyList,
    historyItem,
    link,
    authorInfo,
    answer,
    article,
    emptyState
  };
  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)
  };
  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 [true, null];
    }
    return [false, "输入无效,请输入一个正整数"];
  };
  const saveHistory = (item) => {
    try {
      const raw = _GM_getValue(STORAGE_KEY);
      const historyItems = raw ? JSON.parse(raw) : [];
      const existingIndex = historyItems.findIndex((i) => i.itemId === item.itemId);
      if (existingIndex !== -1) {
        historyItems.splice(existingIndex, 1);
      }
      historyItems.push(item);
      if (historyItems.length > HISTORY_LIMIT) {
        historyItems.splice(0, historyItems.length - HISTORY_LIMIT);
      }
      _GM_setValue(STORAGE_KEY, JSON.stringify(historyItems));
    } catch (error) {
      logger.error("保存浏览历史失败:", error);
    }
  };
  const migrateToGMStorage = () => {
    try {
      logger.log("检测到旧的浏览历史数据,正在转换...");
      const raw = localStorage.getItem(STORAGE_KEY);
      if (raw) {
        _GM_setValue(STORAGE_KEY, raw);
        localStorage.removeItem(STORAGE_KEY);
      }
      logger.log("转换浏览历史数据成功");
    } catch (error) {
      logger.error("转换浏览历史失败:", error);
    }
  };
  const getHistory = () => {
    try {
      if (localStorage.getItem(STORAGE_KEY) !== null) {
        migrateToGMStorage();
      }
      const raw = _GM_getValue(STORAGE_KEY);
      return raw ? JSON.parse(raw) : [];
    } catch (error) {
      logger.error("获取浏览历史失败:", error);
      return [];
    }
  };
  const clearHistory = () => {
    try {
      _GM_setValue(STORAGE_KEY, null);
    } catch (error) {
      logger.error("清空浏览历史失败:", error);
    }
  };
  const HistoryItem = ({ item }) => {
    const itemTypeClass = item.type === "answer" ? styles.answer : styles.article;
    return /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: styles.historyItem, children: [
      /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: item.url, className: `${styles.link} ${itemTypeClass}`, children: item.title }),
      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: styles.authorInfo, children: item.authorName })
    ] });
  };
  const HistoryDialog = ({ isOpen, onClose }) => {
    const historyItems = getHistory();
    const dialogRef = require$$1.useRef(null);
    require$$1.useEffect(() => {
      const dialogElement = dialogRef.current;
      if (!dialogElement) return;
      if (isOpen) {
        dialogElement.showModal();
        document.body.style.overflow = "hidden";
      } else if (dialogElement.open) {
        dialogElement.close();
        document.body.style.overflow = "";
      }
    }, [isOpen]);
    const handleClose = () => {
      onClose();
    };
    return /* @__PURE__ */ jsxRuntimeExports.jsx(
      "dialog",
      {
        ref: dialogRef,
        className: styles.dialog,
        onClose: handleClose,
        onClick: (e) => {
          if (e.target === dialogRef.current) {
            handleClose();
          }
        },
        onKeyDown: (e) => {
          if (e.key === "Escape") {
            handleClose();
          }
        },
        children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.dialogContent, children: [
          /* @__PURE__ */ jsxRuntimeExports.jsxs("header", { className: styles.dialogHeader, children: [
            /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: styles.dialogTitle, children: "浏览历史" }),
            /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: styles.closeButton, "aria-label": "关闭", onClick: handleClose, children: "✕" })
          ] }),
          /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: styles.dialogBody, children: historyItems.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: styles.historyList, children: historyItems.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryItem, { item }, item.itemId)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: styles.emptyState, children: "暂无浏览历史" }) })
        ] })
      }
    );
  };
  const HistoryCard = () => {
    const [isDialogOpen, setIsDialogOpen] = require$$1.useState(false);
    const handleOpenDialog = () => setIsDialogOpen(true);
    const handleCloseDialog = () => setIsDialogOpen(false);
    return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.historyCard, children: [
      /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: styles.historyButton, onClick: handleOpenDialog, "aria-label": "查看历史记录", type: "button", children: "历史记录" }),
      /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryDialog, { isOpen: isDialogOpen, onClose: handleCloseDialog })
    ] });
  };
  const useHistoryTracker = () => {
    require$$1.useEffect(() => {
      const bindEvent = (el) => {
        el.addEventListener("click", onClick);
      };
      const onClick = (e) => {
        const target = e.target;
        const item = target.closest(".ContentItem");
        if (!item) return;
        const zop = item.dataset.zop;
        if (!zop) {
          logger.error("无法读取回答或文章信息");
          return;
        }
        try {
          const data = JSON.parse(zop);
          const link2 = item.querySelector(".ContentItem-title a");
          if (link2) data.url = link2.href;
          saveHistory(data);
        } catch (err) {
          logger.error("解析历史记录失败:", err);
        }
      };
      document.querySelectorAll(".ContentItem").forEach(bindEvent);
      const container = document.querySelector(".Topstory-recommend");
      if (!container) return;
      const observer = new MutationObserver((mutations) => {
        for (const m of mutations) {
          m.addedNodes.forEach((node) => {
            var _a;
            if (!(node instanceof HTMLElement)) return;
            const item = (_a = node.querySelector) == null ? void 0 : _a.call(node, ".ContentItem");
            if (item) bindEvent(item);
          });
        }
      });
      observer.observe(container, { childList: true, subtree: true });
      return () => observer.disconnect();
    }, []);
  };
  const App = () => {
    useHistoryTracker();
    return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryCard, {}) });
  };
  const clearHistoryCommand = [
    "🗑 清空浏览历史记录",
    () => {
      clearHistory();
      alert("清空浏览历史成功");
    }
  ];
  const setHistoryLimitCommand = [
    `🔢 设置记录数量限制(当前:${HISTORY_LIMIT})`,
    () => {
      const input = prompt(`请输入新的历史记录最大数量(默认 ${DEFAULT_HISTORY_LIMIT})`);
      if (!input) return;
      const [isOK, message] = setHistoryLimit(input);
      if (isOK) {
        alert("设置成功");
      } else {
        alert(message);
      }
    }
  ];
  const registerMenuCommands = () => {
    Reflect.apply(_GM_registerMenuCommand, null, clearHistoryCommand);
    Reflect.apply(_GM_registerMenuCommand, null, setHistoryLimitCommand);
  };
  console.log(
    "%c知乎历史记录",
    "color:#1772F6; font-weight:bold; font-size:3em; padding:5px; text-shadow:1px 1px 3px rgba(0,0,0,0.7)"
  );
  const mountApp = () => {
    const container = document.createElement("div");
    container.id = "zh-history-root";
    const target = document.querySelector(".Topstory-container > div:nth-child(2) > div:nth-child(2)");
    if (!target) {
      logger.warn("未找到挂载点");
      return;
    }
    target.appendChild(container);
    ReactDOM.render(/* @__PURE__ */ jsxRuntimeExports.jsx(App, {}), container);
  };
  mountApp();
  registerMenuCommands();
  logger.log(`初始化成功,版本:${_GM_info.script.version}`);

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