LINUX DO Timeline

按发帖时间排序的时间线视图,显示真正的最新发布帖子, 按ESC快速唤起

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         LINUX DO Timeline
// @namespace    https://linux.do/
// @version      1.24
// @description  按发帖时间排序的时间线视图,显示真正的最新发布帖子, 按ESC快速唤起
// @author       ccc9527-c
// @match        https://linux.do/*
// @icon         https://linux.do/uploads/default/original/4X/3/5/7/357c4a83c6bc02fb6d72d63d546beb0a198832a3.png
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_notification
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const scriptVersion = "1.24";
  let isDrawerOpen = false;
  let isLoading = false;

  // ========== 跨标签页通信 ==========
  const tabId = Date.now() + Math.random().toString(36).slice(2);
  let drawerChannel = null;
  let otherTabHasDrawer = false;

  function initBroadcastChannel() {
    try {
      drawerChannel = new BroadcastChannel("timeline_drawer_channel");
      drawerChannel.onmessage = (e) => {
        const { type, from } = e.data;
        if (from === tabId) return; // 忽略自己发的消息

        if (type === "drawer_opened") {
          // 其他标签页打开了抽屉,关闭当前标签页的抽屉
          if (isDrawerOpen) {
            closeDrawer();
          }
        } else if (type === "query_drawer_status") {
          // 其他标签页询问抽屉状态
          if (isDrawerOpen) {
            drawerChannel.postMessage({
              type: "drawer_status_response",
              from: tabId,
              isOpen: true,
            });
          }
        } else if (type === "drawer_status_response" && e.data.isOpen) {
          // 收到其他标签页的响应,有标签页已经打开了抽屉
          otherTabHasDrawer = true;
        }
      };
    } catch (e) {
      console.log("[时间线] BroadcastChannel 不可用,跳过跨标签页同步");
    }
  }

  function broadcastDrawerOpened() {
    if (drawerChannel) {
      drawerChannel.postMessage({ type: "drawer_opened", from: tabId });
    }
  }

  async function checkOtherTabHasDrawer() {
    if (!drawerChannel) return false;

    otherTabHasDrawer = false;
    drawerChannel.postMessage({ type: "query_drawer_status", from: tabId });

    // 等待 100ms 收集响应
    await new Promise((resolve) => setTimeout(resolve, 100));
    return otherTabHasDrawer;
  }
  let allTopics = [];
  let usersMap = {};
  let currentPage = 0;
  let hasMorePages = true;
  let isLoadingMore = false;
  let loadedTopicIds = new Set();
  let countdownTimer = null;
  let remainingSeconds = 0;
  let currentTab = "all"; // 当前选中的 Tab: "all" 或具体的 tabId
  let currentCategoryId = null; // 当前分类 ID
  let currentFilter = "all"; // 当前本地过滤条件: "all", "unseen", "read"
  let autoLoadCount = 0; // 自动递归加载次数,防止无限加载
  let myUserName = "";

  function getCsrfToken() {
    return (
      document
        .querySelector('meta[name="csrf-token"]')
        ?.getAttribute("content") || ""
    );
  }

  // ========== 分类配置映射表 ==========
  const CATEGORY_CONFIG = {
    4: { name: "开发调优", icon: "code", color: "#32c3c3", tabId: "develop" },
    20: { name: "开发调优, Lv1", icon: "code", color: "#32c3c3" },
    31: { name: "开发调优, Lv2", icon: "code", color: "#32c3c3" },
    88: { name: "开发调优, Lv3", icon: "code", color: "#32c3c3" },
    98: {
      name: "国产替代",
      icon: "seedling",
      color: "#D12C25",
      tabId: "domestic",
    },
    99: { name: "国产替代, Lv1", icon: "seedling", color: "#D12C25" },
    100: { name: "国产替代, Lv2", icon: "seedling", color: "#D12C25" },
    101: { name: "国产替代, Lv3", icon: "seedling", color: "#D12C25" },
    14: {
      name: "资源荟萃",
      icon: "square-share-nodes",
      color: "#12A89D",
      tabId: "resource",
    },
    83: { name: "资源荟萃, Lv1", icon: "square-share-nodes", color: "#12A89D" },
    84: { name: "资源荟萃, Lv2", icon: "square-share-nodes", color: "#12A89D" },
    85: { name: "资源荟萃, Lv3", icon: "square-share-nodes", color: "#12A89D" },
    94: { name: "网盘资源", icon: "hard-drive", color: "#16b176" },
    95: { name: "网盘资源, Lv1", icon: "hard-drive", color: "#16b176" },
    96: { name: "网盘资源, Lv2", icon: "hard-drive", color: "#16b176" },
    97: { name: "网盘资源, Lv3", icon: "hard-drive", color: "#16b176" },
    42: { name: "文档共建", icon: "book", color: "#9cb6c4", tabId: "wiki" },
    75: { name: "文档共建, Lv1", icon: "book", color: "#9cb6c4" },
    76: { name: "文档共建, Lv2", icon: "book", color: "#9cb6c4" },
    77: { name: "文档共建, Lv3", icon: "book", color: "#9cb6c4" },
    10: { name: "跳蚤市场", icon: "coins", color: "#ED207B", tabId: "trade" },
    106: {
      name: "积分乐园",
      icon: "credit-card",
      color: "#fcca44",
      tabId: "credit",
    },
    107: { name: "积分乐园, Lv1", icon: "credit-card", color: "#fcca44" },
    108: { name: "积分乐园, Lv2", icon: "credit-card", color: "#fcca44" },
    109: { name: "积分乐园, Lv3", icon: "credit-card", color: "#fcca44" },
    27: { name: "非我莫属", icon: "briefcase", color: "#a8c6fe", tabId: "job" },
    72: { name: "非我莫属, Lv1", icon: "briefcase", color: "#a8c6fe" },
    73: { name: "非我莫属, Lv2", icon: "briefcase", color: "#a8c6fe" },
    74: { name: "非我莫属, Lv3", icon: "briefcase", color: "#a8c6fe" },
    32: {
      name: "读书成诗",
      icon: "book-open-reader",
      color: "#e0d900",
      tabId: "reading",
    },
    69: { name: "读书成诗, Lv1", icon: "book-open-reader", color: "#e0d900" },
    70: { name: "读书成诗, Lv2", icon: "book-open-reader", color: "#e0d900" },
    71: { name: "读书成诗, Lv3", icon: "book-open-reader", color: "#e0d900" },
    46: {
      name: "扬帆起航",
      icon: "rocket",
      color: "#ff9838",
      tabId: "startup",
    },
    66: { name: "扬帆起航, Lv1", icon: "rocket", color: "#ff9838" },
    67: { name: "扬帆起航, Lv2", icon: "rocket", color: "#ff9838" },
    68: { name: "扬帆起航, Lv3", icon: "rocket", color: "#ff9838" },
    34: {
      name: "前沿快讯",
      icon: "newspaper",
      color: "#BB8FCE",
      tabId: "news",
    },
    78: { name: "前沿快讯, Lv1", icon: "newspaper", color: "#BB8FCE" },
    79: { name: "前沿快讯, Lv2", icon: "newspaper", color: "#BB8FCE" },
    80: { name: "前沿快讯, Lv3", icon: "newspaper", color: "#BB8FCE" },
    36: {
      name: "福利羊毛",
      icon: "piggy-bank",
      color: "#E45735",
      tabId: "welfare",
    },
    60: { name: "福利羊毛, Lv1", icon: "piggy-bank", color: "#E45735" },
    61: { name: "福利羊毛, Lv2", icon: "piggy-bank", color: "#E45735" },
    62: { name: "福利羊毛, Lv3", icon: "piggy-bank", color: "#E45735" },
    11: {
      name: "搞七捻三",
      icon: "droplet",
      color: "#3AB54A",
      tabId: "gossip",
    },
    35: { name: "搞七捻三, Lv1", icon: "droplet", color: "#3AB54A" },
    89: { name: "搞七捻三, Lv2", icon: "droplet", color: "#3AB54A" },
    21: { name: "搞七捻三, Lv3", icon: "droplet", color: "#3AB54A" },
    102: {
      name: "社区孵化",
      icon: "lightbulb",
      color: "#ffbb00",
      tabId: "incubation",
    },
    103: { name: "社区孵化, Lv1", icon: "lightbulb", color: "#ffbb00" },
    104: { name: "社区孵化, Lv2", icon: "lightbulb", color: "#ffbb00" },
    105: { name: "社区孵化, Lv3", icon: "lightbulb", color: "#ffbb00" },
    110: {
      name: "虫洞广场",
      icon: "hurricane",
      color: "#ff00f7",
      tabId: "square",
    },
    2: {
      name: "运营反馈",
      icon: "comments",
      color: "#808281",
      tabId: "feedback",
    },
    63: { name: "运营反馈, Lv1", icon: "comments", color: "#808281" },
    64: { name: "运营反馈, Lv2", icon: "comments", color: "#808281" },
    65: { name: "运营反馈, Lv3", icon: "comments", color: "#808281" },
    45: { name: "深海幽域", icon: "water", color: "#45B7D1", tabId: "muted" },
    57: { name: "深海幽域, Lv1", icon: "water", color: "#45B7D1" },
    58: { name: "深海幽域, Lv2", icon: "water", color: "#45B7D1" },
    59: { name: "深海幽域, Lv3", icon: "water", color: "#45B7D1" },
  };

  // 获取分类名称
  function getCategoryName(categoryId) {
    return CATEGORY_CONFIG[categoryId]?.name || `分类${categoryId}`;
  }

  // 获取分类图标
  function getCategoryIcon(categoryId) {
    return CATEGORY_CONFIG[categoryId]?.icon || "folder";
  }

  // 获取分类颜色
  function getCategoryColor(categoryId) {
    return CATEGORY_CONFIG[categoryId]?.color || "#888888";
  }

  // ========== 核心价值观点击特效 ==========
  const CORE_VALUES = ["真诚", "友善", "团结", "专业"];
  let coreValueIndex = 0;

  // 初始化核心价值观点击特效
  function initCoreValueEffect() {
    document.addEventListener("click", function (event) {
      if (!GM_getValue("timeline_enable_slogan", false)) return;

      const textElement = document.createElement("span");
      textElement.className = "core-value-text-effect";
      textElement.textContent = CORE_VALUES[coreValueIndex];
      coreValueIndex = (coreValueIndex + 1) % CORE_VALUES.length;
      document.body.appendChild(textElement);

      const xOffset = -textElement.offsetWidth / 2;
      const yOffset = -textElement.offsetHeight;
      textElement.style.left = `${event.pageX + xOffset}px`;
      textElement.style.top = `${event.pageY + yOffset}px`;

      setTimeout(() => {
        textElement.remove();
      }, 500);
    });
  }

  // ========== CSS 样式 ==========
  GM_addStyle(`
        /* 悬浮按钮 */
        .timeline-float-btn {
            position: fixed;
            width: 30px;
            height: 30px;
            border-radius: 50%;
            background: #099dd7; /* 接近 Linux.do Icon 的深色底层 */
            color: white;
            border: 2px solid #099dd7; /* 第一层黑环 (2px) */
            cursor: grab;
            box-shadow:
                0 0 0 2px #099dd7,  /* 第二层白环 (2px) */
                0 0 0 4px #099dd7; /* 第三层黄环 (2px) */
            z-index: 9999;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            user-select: none;
            touch-action: none;
            padding: 0;
            outline: none;
        }

        .timeline-float-btn text {
            transition: fill 1s ease;
        }

        .timeline-float-btn:hover {
            transform: scale(1.1);
            background: #099dd7;
            box-shadow: 
                0 0 0 2px #099dd7,
                0 0 0 4px #099dd7,
                0 0 20px 5px rgba(252, 202, 4, 0.4); /* 发光效果 */
        }

        .timeline-float-btn:hover text {
            /* 移除聚焦变黄,保持渐变色 */
        }

        .timeline-float-btn:active {
            cursor: grabbing;
            transform: scale(0.95);
        }

        .timeline-float-btn.docked {
            opacity: 0.7;
            transform: scale(0.9);
        }

        .timeline-float-btn.docked:hover {
            opacity: 1;
            transform: scale(1.1);
        }

        .timeline-float-btn.dragging {
            cursor: grabbing;
            transform: scale(1.1);
            transition: none !important;
        }

        /* 侧边栏抽屉 - 移除遮罩层相关样式 */
        @keyframes timeline-glow-breath {
            0% { box-shadow: 0 0 3px rgba(252, 202, 4, 0.2); }
            50% { box-shadow: 0 0 12px 3px rgba(252, 202, 4, 0.5); }
            100% { box-shadow: 0 0 3px rgba(252, 202, 4, 0.2); }
        }

        /* 核心价值观特效样式 */
        .core-value-text-effect {
            position: absolute;
            font-size: 20px;
            font-weight: bold;
            color: #ff69b4;
            text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
            z-index: 99999;
            animation: fadeAndMoveUp 1s ease-out forwards;
            pointer-events: none;
            user-select: none;
        }

        @keyframes fadeAndMoveUp {
            from {
                opacity: 1;
                transform: translateY(0);
            }
            to {
                opacity: 0;
                transform: translateY(-50px);
            }
        }

        .timeline-header-icon-glow {
            animation: timeline-glow-breath 4s infinite ease-in-out;
            position: relative;
        }

        .timeline-update-badge {
            position: absolute;
            top: -6px;
            right: -8px;
            background: #ff4d4f;
            color: white;
            font-size: 9px;
            padding: 1px 4px;
            border-radius: 4px;
            font-weight: bold;
            line-height: 1.2;
            display: none;
            animation: timeline-badge-pulse 1.5s infinite ease-in-out;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
            z-index: 10;
            cursor: pointer;
            pointer-events: auto;
        }

        @keyframes timeline-badge-pulse {
            0% { transform: scale(0.9); }
            50% { transform: scale(1.1); }
            100% { transform: scale(0.9); }
        }

        .timeline-drawer {
            position: fixed;
            top: 0;
            right: calc(-1 * var(--timeline-width) - 20px); /* 动态计算偏移,留出阴影缓冲区 */
            width: var(--timeline-width);
            height: 100vh;
            background: var(--secondary, #fff);
            z-index: 300;
            box-shadow: -4px 0 20px rgba(0, 0, 0, 0.2);
            transition: right 0.3s ease;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        /* 调整轴 */
        .timeline-drawer-resizer {
            position: absolute;
            left: 0;
            top: 0;
            width: 4px;
            height: 100%;
            cursor: ew-resize;
            z-index: 10002;
            transition: background 0.2s;
        }

        .timeline-drawer-resizer:hover, .timeline-drawer-resizer.resizing {
            background: var(--tertiary, #08c);
        }

        .timeline-drawer.open {
            right: 0;
        }

        .timeline-drawer-header {
            padding: 16px 20px;
            border-bottom: 1px solid var(--primary-low, #e9e9e9);
            display: flex;
            align-items: center;
            justify-content: space-between;
            flex-shrink: 0;
        }

        .timeline-drawer-title {
            font-size: 18px;
            font-weight: 600;
            color: var(--primary, #222);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .timeline-drawer-actions {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .timeline-drawer-refresh {
            width: 32px;
            height: 32px;
            border: none;
            background: transparent;
            cursor: pointer;
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 16px;
            color: var(--primary-medium, #666);
            transition: background 0.2s;
        }

        .timeline-drawer-refresh:hover {
            background: var(--primary-very-low, #f0f0f0);
        }

        .timeline-drawer-close {
            width: 32px;
            height: 32px;
            border: none;
            background: transparent;
            cursor: pointer;
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 20px;
            color: var(--primary-medium, #666);
            transition: background 0.2s;
        }

        .timeline-drawer-close:hover {
            background: var(--primary-very-low, #f0f0f0);
        }

        .timeline-drawer-content {
            flex: 1;
            overflow-y: auto;
            padding: 0;
            min-height: 200px;
        }

        .timeline-refresh-settings {
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 12px;
            color: var(--primary-medium, #888);
            margin-right: 8px;
            cursor: pointer;
            padding: 4px 8px;
            border-radius: 4px;
            transition: background 0.2s;
        }

        .timeline-refresh-settings:hover {
            background: var(--primary-very-low, #f0f0f0);
            color: var(--tertiary, #08c);
        }

        .timeline-countdown {
            font-variant-numeric: tabular-nums;
            font-weight: 500;
            min-width: 25px;
        }

        .timeline-countdown.active {
            color: var(--tertiary, #08c);
        }

        /* Tabs 样式 */
        .timeline-tabs-container {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 16px;
            background: var(--secondary, #fff);
            border-bottom: 1px solid var(--primary-low, #e9e9e9);
            overflow-x: auto;
            flex-shrink: 0;
            scrollbar-width: none; /* Firefox */
        }

        .timeline-tabs-container::-webkit-scrollbar {
            display: none; /* Chrome/Safari */
        }

        .timeline-tabs-container.grabbing {
            cursor: grabbing;
            scroll-behavior: auto;
        }

        .timeline-tab {
            user-select: none;
        }

        /* 过滤工具栏 */
        .timeline-filter-bar {
            display: flex;
            align-items: center;
            gap: 12px;
            padding: 8px 16px;
            background: var(--primary-very-low, #f8f8f8);
            border-bottom: 1px solid var(--primary-low, #e9e9e9);
            font-size: 12px;
            color: var(--primary-medium, #888);
            flex-shrink: 0;
        }

        .timeline-filter-item {
            cursor: pointer;
            padding: 2px 6px;
            border-radius: 4px;
            transition: all 0.2s;
        }

        .timeline-filter-item:hover {
            color: var(--tertiary, #08c);
            background: var(--primary-low, #eee);
        }

        .timeline-filter-item.active {
            color: #fff;
            background: var(--tertiary, #08c);
        }

        .timeline-tab {
            padding: 4px 12px;
            border-radius: 16px;
            font-size: 13px;
            white-space: nowrap;
            cursor: pointer;
            color: var(--primary-medium, #666);
            background: var(--primary-very-low, #f0f0f0);
            transition: all 0.2s;
            border: 1px solid transparent;
        }

        .timeline-tab:hover {
            color: var(--primary, #222);
            background: var(--primary-low, #e9e9e9);
        }

        .timeline-tab.active {
            color: white;
            background: var(--tertiary, #08c);
            border-color: var(--tertiary, #08c);
        }

        /* 下拉管理相关 */
        .timeline-tabs-wrapper {
            position: relative;
            display: flex;
            align-items: center;
            background: var(--secondary, #fff);
            border-bottom: 1px solid var(--primary-low, #e9e9e9);
        }

        .timeline-tabs-container {
            flex: 1;
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 12px;
            overflow-x: auto;
            scrollbar-width: none;
        }

        .timeline-tabs-more-btn {
            width: 32px;
            height: 38px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            background: var(--secondary, #fff);
            box-shadow: -10px 0 10px -5px rgba(0,0,0,0.05);
            z-index: 10;
            color: var(--primary-medium, #666);
            border-left: 1px solid var(--primary-low, #e9e9e9);
        }

        .timeline-tabs-more-btn:hover {
            color: var(--tertiary, #08c);
        }

        .timeline-tabs-modal {
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            background: var(--secondary, #fff);
            z-index: 1000;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            padding: 16px;
            display: none;
            max-height: 300px;
            overflow-y: auto;
        }

        .timeline-tabs-modal.open {
            display: block;
        }

        .timeline-tabs-modal-header {
            font-size: 12px;
            color: var(--primary-medium, #888);
            margin-bottom: 12px;
            display: flex;
            justify-content: space-between;
        }

        .timeline-tabs-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
            gap: 10px;
        }

        .timeline-grid-item {
            padding: 6px 4px;
            background: var(--primary-very-low, #f0f0f0);
            border-radius: 4px;
            font-size: 12px;
            text-align: center;
            cursor: move;
            user-select: none;
            border: 1px solid transparent;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .timeline-grid-item:hover {
            border-color: var(--tertiary, #08c);
        }

        .timeline-grid-item.dragging {
            opacity: 0.5;
            background: var(--tertiary-low, #e6f3ff);
            border: 1px dashed var(--tertiary, #08c);
        }

        .timeline-grid-item.drop-target {
            border: 1px solid var(--tertiary, #08c);
            transform: scale(1.05);
            background: var(--tertiary-low, #e6f3ff);
        }

        .timeline-grid-item.active {
            color: var(--tertiary, #08c);
            font-weight: bold;
            background: var(--tertiary-low, #e6f3ff);
        }

        .timeline-topic-list {
            list-style: none;
            margin: 0;
            padding: 0;
        }

        .timeline-topic-item {
            padding: 12px 20px;
            border-bottom: 1px solid var(--primary-very-low, #f0f0f0);
            cursor: pointer;
            transition: background 0.2s;
            position: relative;
        }

        .timeline-topic-item:hover {
            background: var(--primary-very-low, #f8f8f8);
        }

        .timeline-unseen-dot {
            position: absolute;
            top: 12px;
            right: 12px;
            width: 8px;
            height: 8px;
            background: var(--tertiary, #08c);
            border-radius: 50%;
        }

        .timeline-topic-header {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 6px;
        }

        .timeline-topic-avatar {
            width: 28px;
            height: 28px;
            border-radius: 50%;
            flex-shrink: 0;
            cursor: pointer;
            object-fit: cover;
        }

        .timeline-topic-avatar.square-avatar,
        .timeline-user-avatar.square-avatar {
            border-radius: 4px;
        }

        .timeline-topic-meta {
            display: flex;
            flex-direction: column;
            min-width: 0;
        }

        .timeline-topic-user-info {
            display: flex;
            align-items: center;
            gap: 6px;
            flex-wrap: wrap;
        }

        .timeline-topic-username {
            font-size: 13px;
            color: var(--primary, #222);
            font-weight: 500;
            cursor: pointer;
        }

        .timeline-topic-username:hover {
            color: var(--tertiary, #08c);
        }

        .timeline-topic-name {
            font-size: 12px;
            color: var(--primary-medium, #888);
        }

        .timeline-topic-time {
            font-size: 12px;
            color: var(--primary-medium, #888);
        }

        .timeline-topic-title {
            font-size: 14px;
            color: var(--primary, #222);
            line-height: 1.4;
            margin: 0;
            word-break: break-word;
        }

        .timeline-topic-title:hover {
            color: var(--tertiary, #08c);
        }

        .timeline-topic-stats {
            display: flex;
            gap: 12px;
            margin-top: 8px;
            font-size: 12px;
            color: var(--primary-medium, #888);
        }

        .timeline-topic-stat {
            display: flex;
            align-items: center;
            gap: 4px;
        }

        .timeline-loading-2 {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 40px 20px;
            color: var(--primary-medium, #888);
            width: 100%;
            box-sizing: border-box;
        }

        .timeline-spinner {
            width: 32px;
            height: 32px;
            border: 3px solid var(--primary-low, #e9e9e9);
            border-top-color: var(--tertiary, #08c);
            border-radius: 50%;
            animation: timeline-spin 0.8s linear infinite;
            margin-bottom: 12px;
        }

        @keyframes timeline-spin {
            to { transform: rotate(360deg); }
        }

        .timeline-load-more {
            padding: 16px 20px;
            text-align: center;
            color: var(--primary-medium, #888);
            font-size: 14px;
        }

        .timeline-load-more-spinner {
            display: inline-block;
            width: 16px;
            height: 16px;
            border: 2px solid var(--primary-low, #e9e9e9);
            border-top-color: var(--tertiary, #08c);
            border-radius: 50%;
            animation: timeline-spin 0.8s linear infinite;
            margin-right: 8px;
            vertical-align: middle;
        }

        .timeline-no-more {
            padding: 16px 20px;
            text-align: center;
            color: var(--primary-low-mid, #aaa);
            font-size: 13px;
        }

        .timeline-error {
            padding: 40px 20px;
            text-align: center;
            color: var(--danger, #e45735);
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 12px;
        }

        .timeline-error-icon {
            font-size: 40px;
        }

        .timeline-error-msg {
            font-size: 16px;
            font-weight: 600;
        }

        .timeline-error-detail {
            font-size: 12px;
            color: var(--primary-medium, #888);
            max-width: 300px;
            word-break: break-word;
        }

        .timeline-retry-btn {
            margin-top: 8px;
            padding: 8px 20px;
            background: var(--tertiary, #08c);
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            transition: opacity 0.2s;
        }

        .timeline-retry-btn:hover {
            opacity: 0.85;
        }

            .timeline-category {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            font-size: 11px;
            padding: 2px 6px;
            border-radius: 3px;
            background: var(--primary-very-low, #f0f0f0);
            color: var(--primary-medium, #666);
            margin-right: 6px;
        }

        .timeline-category-icon {
            width: 12px;
            height: 12px;
            fill: var(--category-color, #888);
        }

        .timeline-topic-category-tags {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 4px;
            margin-top: 6px;
        }

        .timeline-tags {
            display: flex;
            flex-wrap: wrap;
            gap: 4px;
        }

        .timeline-tag {
            display: inline-block;
            font-size: 11px;
            padding: 2px 6px;
            border-radius: 3px;
            background: var(--primary-very-low, #f0f0f0);
            color: var(--primary-medium, #666);
        }

        /* 挤压模式相关样式 */
        :root {
            --timeline-width: 400px;
        }

        body {
            transition: padding-right 0.3s ease;
        }

        body.timeline-drawer-push {
            padding-right: var(--timeline-width) !important;
        }

        /* 适配 Discourse 顶栏 */
        .d-header {
            transition: right 0.3s ease !important;
        }

        body.timeline-drawer-push .d-header {
            right: var(--timeline-width) !important;
        }

        /* 适配移动端或小屏 */
        @media screen and (max-width: 768px) {
            :root {
                --timeline-width: 90vw !important;
            }
            .timeline-drawer {
                width: var(--timeline-width) !important;
                z-index: 10001 !important;
                box-shadow: -10px 0 30px rgba(0,0,0,0.3);
            }
            body.timeline-drawer-push {
                padding-right: 0 !important;
            }
            body.timeline-drawer-push .d-header {
                right: 0 !important;
            }
            .timeline-drawer-resizer {
                display: none; /* 移动端禁用手动调宽 */
            }
        }

        /* 遮罩层 */
        .timeline-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background: rgba(0, 0, 0, 0.5);
            z-index: 10000;
            display: none;
            backdrop-filter: blur(2px);
        }

        .timeline-overlay.active {
            display: block;
        }

        /* 强提醒高亮 - 使用与悬浮按钮相同的黄色 */
        @keyframes timeline-new-pulse {
            0% { 
                box-shadow: inset 0 0 0 2px #fcca04; 
                background: rgba(252, 202, 4, 0.15); 
            }
            100% { 
                box-shadow: inset 0 0 0 0px transparent; 
                background: transparent; 
            }
        }

        .timeline-topic-item.new-topic-highlight {
            animation: timeline-new-pulse 10s ease-out forwards;
            position: relative;
        }

        /* 设置弹窗样式 - 限制在抽屉内 */
        .timeline-settings-modal {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%) scale(0.9);
            width: 90%;
            max-width: 320px;
            background: var(--secondary, #fff);
            border-radius: 12px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.3);
            z-index: 10005;
            display: none;
            opacity: 0;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            padding: 20px;
            color: var(--primary, #222);
            border: 1px solid var(--primary-low, #e9e9e9);
            animation: timeline-glow-breath 4s infinite ease-in-out; /* 添加发光动画 */
        }

        .timeline-settings-modal.open {
            display: block;
            opacity: 1;
            transform: translate(-50%, -50%) scale(1);
        }

        .timeline-settings-header {
            font-size: 18px;
            font-weight: 600;
            margin-bottom: 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .timeline-settings-close {
            cursor: pointer;
            font-size: 20px;
            color: var(--primary-medium, #888);
        }

        .timeline-settings-group {
            margin-bottom: 16px;
        }

        .timeline-following-container {
            margin-top: 10px;
            padding: 8px;
            background: var(--primary-very-low, #f0f0f0);
            border-radius: 6px;
            border: 1px solid var(--primary-low, #e9e9e9);
        }

        .timeline-following-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 8px;
            font-size: 12px;
            color: var(--primary-medium, #888);
        }

        .timeline-following-list {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
            max-height: 160px;
            overflow-y: auto;
        }

        .timeline-following-item {
            position: relative;
            width: 36px;
            height: 36px;
            border-radius: 50%;
            cursor: pointer;
            border: 2px solid transparent;
            transition: all 0.2s;
            background: var(--primary-very-low, #eee);
        }

        .timeline-following-item.selected {
            border-color: var(--tertiary, #3b82f6);
        }

        .timeline-following-item.selected::after {
            content: "✓";
            position: absolute;
            bottom: -2px;
            right: -2px;
            width: 14px;
            height: 14px;
            background: var(--tertiary, #3b82f6);
            color: white;
            font-size: 10px;
            font-weight: bold;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 50%;
            border: 1px solid var(--secondary, #fff);
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
            z-index: 1;
        }

        .timeline-following-item:hover {
            border-color: var(--tertiary, #3b82f6);
            transform: scale(1.1);
        }

        .timeline-following-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            border-radius: 50%;
            display: block;
        }

        .timeline-import-btn {
            font-size: 11px;
            color: var(--tertiary, #08c);
            cursor: pointer;
            text-decoration: underline;
        }

        .timeline-import-btn:hover {
            color: var(--tertiary-hover, #005f88);
        }

        .timeline-settings-label {
            display: block;
            font-size: 14px;
            margin-bottom: 8px;
            color: var(--primary-medium, #666);
        }

        .timeline-settings-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
        }

        /* Toggle Switch */
        .timeline-switch {
            position: relative;
            display: inline-block;
            width: 44px;
            height: 24px;
        }

        .timeline-switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .timeline-slider {
            position: absolute;
            cursor: pointer;
            top: 0; left: 0; right: 0; bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 24px;
        }

        .timeline-slider:before {
            position: absolute;
            content: "";
            height: 18px;
            width: 18px;
            left: 3px;
            bottom: 3px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }

        input:checked + .timeline-slider {
            background-color: var(--tertiary, #08c);
        }

        input:checked + .timeline-slider:before {
            transform: translateX(20px);
        }

        .timeline-input {
            width: 60px;
            padding: 4px 8px;
            border: 1px solid var(--primary-low, #e9e9e9);
            border-radius: 4px;
            font-size: 14px;
            text-align: center;
            background: var(--secondary, #fff);
            color: var(--primary, #222);
        }

        .timeline-settings-btn {
            width: 32px;
            height: 32px;
            border: none;
            background: transparent;
            cursor: pointer;
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 18px;
            color: var(--primary-medium, #666);
            transition: background 0.2s;
        }

        .timeline-settings-btn:hover {
            background: var(--primary-very-low, #f0f0f0);
            color: var(--tertiary, #08c);
        }

        /* 用户搜索和列表 */
        .timeline-user-search-container {
            position: relative;
            display: flex;
            gap: 8px;
            align-items: center;
        }

        .timeline-user-search-input {
            flex: 1;
            padding: 8px 12px;
            border: 1px solid var(--primary-low, #e9e9e9);
            border-radius: 8px;
            background: var(--secondary, #fff);
            color: var(--primary, #222);
            font-size: 13px;
            outline: none;
            margin-bottom: 0 !important;
        }

        /* 关注的人按钮 */
        .timeline-following-trigger {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0 10px;
            height: 34px;
            background: var(--primary-very-low, #f0f0f0);
            border: 1px solid var(--primary-low, #e9e9e9);
            border-radius: 8px;
            cursor: pointer;
            color: var(--primary-medium, #666);
            transition: all 0.2s;
            white-space: nowrap;
            font-size: 13px;
            gap: 4px;
        }

        .timeline-following-trigger:hover {
            background: var(--primary-low, #e9e9e9);
            color: var(--primary, #222);
        }

        /* 子弹窗样式 */
        .timeline-sub-modal {
            position: absolute;
            top: 40px;
            right: 0;
            width: 280px;
            background: var(--secondary, #fff);
            border: 1px solid var(--primary-low, #e9e9e9);
            border-radius: 12px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.15);
            z-index: 1001;
            display: none;
            flex-direction: column;
            overflow: hidden;
            backdrop-filter: blur(10px);
            background: rgba(var(--secondary-rgb, 255, 255, 255), 0.95);
        }

        .timeline-sub-modal.open {
            display: flex;
            animation: timeline-fade-in 0.2s ease-out;
        }

        .timeline-sub-modal-header {
            padding: 10px 12px;
            border-bottom: 1px solid var(--primary-low, #e9e9e9);
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: var(--primary-very-low, #f9f9f9);
        }

        .timeline-sub-modal-title {
            font-size: 13px;
            font-weight: 600;
        }

        .timeline-sub-modal-close {
            cursor: pointer;
            font-size: 14px;
            color: var(--primary-low-mid, #aaa);
        }

        .timeline-sub-modal-content {
            max-height: 300px;
            overflow-y: auto;
            padding: 8px;
        }

        .timeline-user-search-input:focus {
            border-color: var(--tertiary, #08c);
            box-shadow: 0 0 0 2px var(--tertiary-low, rgba(0, 136, 204, 0.2));
        }

        .timeline-user-search-results {
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            background: var(--secondary, #fff);
            border: 1px solid var(--primary-low, #e9e9e9);
            border-radius: 6px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 10006;
            max-height: 200px;
            overflow-y: auto;
            display: none;
            margin-top: 4px;
        }

        .timeline-user-search-results.active {
            display: block;
        }

        .timeline-user-search-item {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 8px 12px;
            cursor: pointer;
            transition: background 0.2s;
        }

        .timeline-user-search-item:hover {
            background: var(--primary-very-low, #f0f0f0);
        }

        .timeline-user-list {
            display: flex;
            flex-direction: column;
            gap: 8px;
            margin-top: 12px;
            max-height: 160px;
            overflow-y: auto;
            padding-right: 4px;
        }

        /* 滚动条美化 */
        .timeline-user-list::-webkit-scrollbar {
            width: 4px;
        }
        .timeline-user-list::-webkit-scrollbar-thumb {
            background: var(--primary-low, #eee);
            border-radius: 4px;
        }

        .timeline-selected-user {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 8px 10px;
            background: var(--primary-very-low, #f8f8f8);
            border-radius: 8px;
            font-size: 13px;
            border: 1px solid var(--primary-low, #eee);
        }

        .timeline-selected-user-info {
            flex: 1;
            display: flex;
            flex-direction: column;
            min-width: 0;
        }

        .timeline-selected-user-name {
            font-weight: 600;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            color: var(--primary, #222);
        }

        .timeline-selected-user-username {
            font-size: 11px;
            color: var(--primary-medium, #666);
        }

        .timeline-user-remove {
            cursor: pointer;
            color: var(--primary-medium, #999);
            font-size: 14px;
            padding: 4px;
            transition: color 0.2s;
        }

        .timeline-user-remove:hover {
            color: var(--danger, #e45735);
        }

        .timeline-user-avatar {
            width: 28px;
            height: 28px;
            border-radius: 50%;
            object-fit: cover;
            background: #eee;
        }
    `);

  // ========== 悬浮按钮相关变量 ==========
  let btn = null;

  // ========== 创建悬浮按钮 ==========
  function createFloatButton() {
    if (document.querySelector(".timeline-float-btn")) return;

    btn = document.createElement("button");
    btn.className = "timeline-float-btn";
    // 艺术体 L 字母 SVG,使用三层颜色渐变
    btn.innerHTML = `<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
        <defs>
            <linearGradient id="l-gradient-btn" x1="0" y1="0" x2="0" y2="1">
                <stop offset="33.33%" stop-color="#000" />
                <stop offset="33.33%" stop-color="#fff" />
                <stop offset="66.66%" stop-color="#fff" />
                <stop offset="66.66%" stop-color="#fcca04" />
            </linearGradient>
        </defs>
        <text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-family="'Georgia', serif" font-weight="bold" font-style="italic" font-size="20" fill="url(#l-gradient-btn)">L</text>
    </svg>`;
    btn.title = "时间线视图 (ESC)";

    document.body.appendChild(btn);

    // 应用保存的位置
    applyBtnPosFromStore();

    // 绑定拖拽事件
    bindBtnDrag();
  }

  // 从存储中读取并应用按钮位置
  function applyBtnPosFromStore() {
    const pos = GM_getValue("timeline_btn_pos", null);

    if (pos && typeof pos.x === "number" && typeof pos.y === "number") {
      const clamped = clampBtnPos(pos.x, pos.y);
      btn.style.left = `${clamped.x}px`;
      btn.style.top = `${clamped.y}px`;
    } else {
      // 默认位置:右上角
      const w = btn.offsetWidth || 30;
      const margin = 18;
      const x = Math.max(margin, window.innerWidth - w - margin);
      const y = 100;
      btn.style.left = `${x}px`;
      btn.style.top = `${y}px`;
      saveBtnPos(x, y);
    }
  }

  // 保存按钮位置
  function saveBtnPos(x, y) {
    GM_setValue("timeline_btn_pos", { x, y });
  }

  // 限制按钮位置在屏幕内
  function clampBtnPos(x, y) {
    const w = btn.offsetWidth || 30;
    const h = btn.offsetHeight || 30;
    const margin = 8;
    const maxX = Math.max(margin, window.innerWidth - w - margin);
    const maxY = Math.max(margin, window.innerHeight - h - margin);
    return {
      x: Math.max(margin, Math.min(maxX, x)),
      y: Math.max(margin, Math.min(maxY, y)),
    };
  }

  // 绑定按钮拖拽事件
  function bindBtnDrag() {
    let dragging = false;
    let moved = false;
    let startX = 0,
      startY = 0;
    let origLeft = 0,
      origTop = 0;
    let pointerId = null;
    let dockTimer = null;
    let lastPointerUpTime = 0; // 记录上次 pointerup 触发 toggleDrawer 的时间
    const DOCK_DELAY = 500; // 500毫秒后自动吸附
    const DOCK_OFFSET = 20; // 大屏幕下吸附时露出的像素
    // 小屏幕下不遮挡,返回按钮尺寸;大屏幕下半隐藏
    const isSmallScreen = () => window.innerWidth <= 768;

    const getLeftTop = () => {
      const r = btn.getBoundingClientRect();
      return { left: r.left, top: r.top };
    };

    // 清除吸附状态
    const clearDockState = () => {
      btn.classList.remove(
        "docked",
        "docked-left",
        "docked-right",
        "docked-top",
        "docked-bottom",
      );
    };

    // 计算并应用吸附
    const applyDock = () => {
      const lt = getLeftTop();
      const w = btn.offsetWidth || 30;
      const h = btn.offsetHeight || 30;
      const vw = window.innerWidth;
      const vh = window.innerHeight;
      const margin = 8;

      // 计算到各边的距离
      const distLeft = lt.left;
      const distRight = vw - lt.left - w;
      const distTop = lt.top;
      const distBottom = vh - lt.top - h;

      // 找最近的边
      const minDist = Math.min(distLeft, distRight, distTop, distBottom);

      clearDockState();
      btn.classList.add("docked");

      // 小屏幕下不遮挡,只贴边;大屏幕下半隐藏
      if (isSmallScreen()) {
        // 小屏幕:贴边但完全露出
        if (minDist === distLeft) {
          btn.style.left = `${margin}px`;
          btn.classList.add("docked-left");
        } else if (minDist === distRight) {
          btn.style.left = `${vw - w - margin}px`;
          btn.classList.add("docked-right");
        } else if (minDist === distTop) {
          btn.style.top = `${margin}px`;
          btn.classList.add("docked-top");
        } else {
          btn.style.top = `${vh - h - margin}px`;
          btn.classList.add("docked-bottom");
        }
      } else {
        // 大屏幕:半隐藏
        if (minDist === distLeft) {
          btn.style.left = `${-w + DOCK_OFFSET}px`;
          btn.classList.add("docked-left");
        } else if (minDist === distRight) {
          btn.style.left = `${vw - DOCK_OFFSET}px`;
          btn.classList.add("docked-right");
        } else if (minDist === distTop) {
          btn.style.top = `${-h + DOCK_OFFSET}px`;
          btn.classList.add("docked-top");
        } else {
          btn.style.top = `${vh - DOCK_OFFSET}px`;
          btn.classList.add("docked-bottom");
        }
      }

      // 保存吸附后的位置
      saveBtnPos(parseFloat(btn.style.left), parseFloat(btn.style.top));
    };

    // 取消吸附,恢复到正常位置
    const undock = () => {
      if (!btn.classList.contains("docked")) return;

      const w = btn.offsetWidth || 30;
      const h = btn.offsetHeight || 30;
      const vw = window.innerWidth;
      const vh = window.innerHeight;
      const margin = 8;

      let x = parseFloat(btn.style.left) || 0;
      let y = parseFloat(btn.style.top) || 0;

      // 根据吸附方向恢复位置
      if (btn.classList.contains("docked-left")) {
        x = margin;
      } else if (btn.classList.contains("docked-right")) {
        x = vw - w - margin;
      } else if (btn.classList.contains("docked-top")) {
        y = margin;
      } else if (btn.classList.contains("docked-bottom")) {
        y = vh - h - margin;
      }

      clearDockState();
      btn.style.left = `${x}px`;
      btn.style.top = `${y}px`;
      saveBtnPos(x, y);

      // 重新启动吸附定时器
      startDockTimer();
    };

    // 启动吸附定时器
    const startDockTimer = () => {
      clearTimeout(dockTimer);
      dockTimer = setTimeout(() => {
        if (!dragging && !btn.matches(":hover") && !isDrawerOpen) {
          applyDock();
        }
      }, DOCK_DELAY);
    };

    // 鼠标进入时取消吸附
    btn.addEventListener("mouseenter", () => {
      clearTimeout(dockTimer);
      undock();
    });

    // 鼠标离开时启动吸附定时器
    btn.addEventListener("mouseleave", () => {
      if (!dragging && !isDrawerOpen) {
        startDockTimer();
      }
    });

    const onPointerDown = (e) => {
      if (e.button !== undefined && e.button !== 0) return;
      if (dragging) return; // 防止重复触发

      clearTimeout(dockTimer);
      undock(); // 取消吸附状态

      dragging = true;
      moved = false;
      pointerId = e.pointerId;
      btn.classList.add("dragging");

      const lt = getLeftTop();
      origLeft = lt.left;
      origTop = lt.top;

      startX = e.clientX;
      startY = e.clientY;

      try {
        btn.setPointerCapture(e.pointerId);
      } catch {}
      e.preventDefault();
      e.stopPropagation();
    };

    const onPointerMove = (e) => {
      if (!dragging || e.pointerId !== pointerId) return;
      const dx = e.clientX - startX;
      const dy = e.clientY - startY;

      // 检测是否真的移动了(增加阈值)
      if (!moved && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
        moved = true;
      }

      if (moved) {
        const nx = origLeft + dx;
        const ny = origTop + dy;
        const clamped = clampBtnPos(nx, ny);

        btn.style.left = `${clamped.x}px`;
        btn.style.top = `${clamped.y}px`;
      }

      e.preventDefault();
      e.stopPropagation();
    };

    const onPointerUp = (e) => {
      if (!dragging || e.pointerId !== pointerId) return;

      dragging = false;
      btn.classList.remove("dragging");

      if (moved) {
        // 只有在拖动后才保存位置
        const lt = getLeftTop();
        const clamped = clampBtnPos(lt.left, lt.top);
        btn.style.left = `${clamped.x}px`;
        btn.style.top = `${clamped.y}px`;
        saveBtnPos(clamped.x, clamped.y);
        // 拖动结束后启动吸附定时器
        if (!isDrawerOpen) {
          startDockTimer();
        }
      } else {
        // 只有在没有移动时才触发点击
        lastPointerUpTime = Date.now();
        toggleDrawer();
      }

      try {
        btn.releasePointerCapture(e.pointerId);
      } catch {}

      pointerId = null;
      e.preventDefault();
      e.stopPropagation();
    };

    // 移动端可能触发 pointercancel 而不是 pointerup
    const onPointerCancel = (e) => {
      if (!dragging || e.pointerId !== pointerId) return;

      dragging = false;
      btn.classList.remove("dragging");

      // 如果没有移动,视为点击
      if (!moved) {
        lastPointerUpTime = Date.now();
        toggleDrawer();
      }

      try {
        btn.releasePointerCapture(e.pointerId);
      } catch {}

      pointerId = null;
    };

    const onResize = () => {
      if (dragging) return; // 拖动时不触发resize调整

      // 如果处于吸附状态,重新计算吸附位置
      if (btn.classList.contains("docked")) {
        const w = btn.offsetWidth || 30;
        const h = btn.offsetHeight || 30;
        const vw = window.innerWidth;
        const vh = window.innerHeight;
        const margin = 8;

        if (isSmallScreen()) {
          // 小屏幕:贴边但完全露出
          if (btn.classList.contains("docked-right")) {
            btn.style.left = `${vw - w - margin}px`;
          } else if (btn.classList.contains("docked-bottom")) {
            btn.style.top = `${vh - h - margin}px`;
          }
        } else {
          // 大屏幕:半隐藏
          if (btn.classList.contains("docked-right")) {
            btn.style.left = `${vw - DOCK_OFFSET}px`;
          } else if (btn.classList.contains("docked-bottom")) {
            btn.style.top = `${vh - DOCK_OFFSET}px`;
          }
        }
      } else {
        const lt = getLeftTop();
        const clamped = clampBtnPos(lt.left, lt.top);
        btn.style.left = `${clamped.x}px`;
        btn.style.top = `${clamped.y}px`;
        saveBtnPos(clamped.x, clamped.y);
      }
    };

    btn.addEventListener("pointerdown", onPointerDown, { passive: false });
    window.addEventListener("pointermove", onPointerMove, { passive: false });
    window.addEventListener("pointerup", onPointerUp, { passive: false });
    window.addEventListener("pointercancel", onPointerCancel, {
      passive: false,
    });
    window.addEventListener("resize", onResize);

    // 初始化时启动吸附定时器
    startDockTimer();
  }

  // ========== 创建抽屉 ==========
  function createDrawer() {
    if (document.querySelector(".timeline-drawer")) return;

    // 抽屉
    const drawer = document.createElement("div");
    drawer.className = "timeline-drawer";

    // 初始化宽度
    const savedWidth = GM_getValue("timeline_width", 400);
    document.documentElement.style.setProperty(
      "--timeline-width",
      `${savedWidth}px`,
    );

    drawer.innerHTML = `
            <div class="timeline-drawer-resizer"></div>
            <div class="timeline-drawer-header">
                <div class="timeline-drawer-title">
                    <span class="timeline-header-icon-glow" style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: #099dd7; border-radius: 50%; cursor: pointer;" title="回到顶部">
                        <svg viewBox="0 0 24 24" width="22" height="22" xmlns="http://www.w3.org/2000/svg">
                            <defs>
                                <linearGradient id="l-gradient-header" x1="0" y1="0" x2="0" y2="1">
                                    <stop offset="33.33%" stop-color="#000" />
                                    <stop offset="33.33%" stop-color="#fff" />
                                    <stop offset="66.66%" stop-color="#fff" />
                                    <stop offset="66.66%" stop-color="#fcca04" />
                                </linearGradient>
                            </defs>
                            <text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-family="'Georgia', serif" font-weight="bold" font-style="italic" font-size="18" fill="url(#l-gradient-header)">L</text>
                        </svg>
                        <div class="timeline-update-badge">new</div>
                    </span>
                </div>
                <div class="timeline-drawer-actions">
                    <div class="timeline-refresh-settings" title="点击设置自动刷新间隔 (秒)">
                        <span>⏱️</span>
                        <span class="timeline-countdown"></span>
                    </div>
                    <button class="timeline-drawer-refresh" title="刷新">🔄</button>
                    <button class="timeline-settings-btn" title="设置">⚙️</button>
                    <button class="timeline-drawer-close" title="关闭">×</button>
                </div>
            </div>
            <div class="timeline-tabs-wrapper">
                <div class="timeline-tabs-container">
                    <div class="timeline-tab ${
                      currentTab === "all" ? "active" : ""
                    }" data-tab="all">全部</div>
                    ${(function () {
                      const savedOrder = GM_getValue("timeline_tabs_order", []);
                      const tabConfigs = Object.entries(CATEGORY_CONFIG)
                        .filter(([_, cfg]) => cfg.tabId)
                        .map(([id, cfg]) => ({ id, ...cfg }));

                      // 排序逻辑
                      if (savedOrder.length > 0) {
                        tabConfigs.sort((a, b) => {
                          const idxA = savedOrder.indexOf(a.id);
                          const idxB = savedOrder.indexOf(b.id);
                          if (idxA === -1 && idxB === -1) return 0;
                          if (idxA === -1) return 1;
                          if (idxB === -1) return -1;
                          return idxA - idxB;
                        });
                      }

                      return tabConfigs
                        .map(
                          (cfg) =>
                            `<div class="timeline-tab" data-tab="${cfg.tabId}" data-id="${cfg.id}">${cfg.name}</div>`,
                        )
                        .join("");
                    })()}
                </div>
                <div class="timeline-tabs-more-btn" title="更多分类/排序">
                    <span style="transform: rotate(90deg); display: inline-block;">❯</span>
                </div>
                <div class="timeline-tabs-modal">
                    <div class="timeline-tabs-modal-header">
                        <span>按住并拖动以排序</span>
                        <span class="timeline-tabs-close-modal" style="cursor:pointer">✕</span>
                    </div>
                    <div class="timeline-tabs-grid">
                        <div class="timeline-grid-item-fixed ${
                          currentTab === "all" ? "active" : ""
                        }" data-tab="all" style="padding: 6px 4px; background: var(--primary-low, #eee); border-radius: 4px; font-size: 12px; text-align: center; color: var(--primary-medium);">全部</div>
                        ${(function () {
                          const savedOrder = GM_getValue(
                            "timeline_tabs_order",
                            [],
                          );
                          const tabConfigs = Object.entries(CATEGORY_CONFIG)
                            .filter(([_, cfg]) => cfg.tabId)
                            .map(([id, cfg]) => ({ id, ...cfg }));

                          if (savedOrder.length > 0) {
                            tabConfigs.sort((a, b) => {
                              const idxA = savedOrder.indexOf(a.id);
                              const idxB = savedOrder.indexOf(b.id);
                              if (idxA === -1 && idxB === -1) return 0;
                              if (idxA === -1) return 1;
                              if (idxB === -1) return -1;
                              return idxA - idxB;
                            });
                          }

                          return tabConfigs
                            .map(
                              (cfg) =>
                                `<div class="timeline-grid-item" draggable="true" data-id="${cfg.id}" title="${cfg.name}">${cfg.name}</div>`,
                            )
                            .join("");
                        })()}
                    </div>
                </div>
            </div>
            <div class="timeline-filter-bar">
                <span class="timeline-filter-item ${
                  currentFilter === "all" ? "active" : ""
                }" data-filter="all">全部</span>
                <span class="timeline-filter-item ${
                  currentFilter === "unseen" ? "active" : ""
                }" data-filter="unseen">未读</span>
                <span class="timeline-filter-item ${
                  currentFilter === "read" ? "active" : ""
                }" data-filter="read">已读</span>
            </div>
            <div class="timeline-drawer-content">
                <div class="timeline-loading-2">
                    <div class="timeline-spinner"></div>
                    <span>加载中...</span>
                </div>
            </div>
        `;

    const refreshSettings = drawer.querySelector(".timeline-refresh-settings");
    refreshSettings.addEventListener("click", () => {
      showSettingsModal();
    });

    const settingsBtn = drawer.querySelector(".timeline-settings-btn");
    settingsBtn.addEventListener("click", () => {
      showSettingsModal();
    });

    updateCountdownDisplay();
    startAutoRefresh();

    // 绑定标题图标点击回顶事件
    const titleIcon = drawer.querySelector(".timeline-header-icon-glow");
    if (titleIcon) {
      titleIcon.addEventListener("click", () => {
        const content = drawer.querySelector(".timeline-drawer-content");
        if (content) {
          content.scrollTo({ top: 0, behavior: "smooth" });
        }
      });
    }

    // 拖拽缩放逻辑
    const resizer = drawer.querySelector(".timeline-drawer-resizer");
    let isResizing = false;

    resizer.addEventListener("mousedown", (e) => {
      isResizing = true;
      resizer.classList.add("resizing");
      document.body.style.cursor = "ew-resize";
      document.body.style.userSelect = "none";
      // 拖拽时暂时关闭 transition
      drawer.style.transition = "none";
      document.body.style.transition = "none";
      const header = document.querySelector(".d-header");
      if (header) header.style.transition = "none";
    });

    document.addEventListener("mousemove", (e) => {
      if (!isResizing) return;
      const width = window.innerWidth - e.clientX;
      const finalWidth = Math.min(
        Math.max(width, 300),
        window.innerWidth * 0.8,
      );
      document.documentElement.style.setProperty(
        "--timeline-width",
        `${finalWidth}px`,
      );
    });

    document.addEventListener("mouseup", () => {
      if (!isResizing) return;
      isResizing = false;
      resizer.classList.remove("resizing");
      document.body.style.cursor = "";
      document.body.style.userSelect = "";
      drawer.style.transition = "right 0.3s ease";
      document.body.style.transition = "padding-right 0.3s ease";
      const header = document.querySelector(".d-header");
      if (header) header.style.transition = "right 0.3s ease";

      const currentWidth = parseInt(
        getComputedStyle(document.documentElement).getPropertyValue(
          "--timeline-width",
        ),
      );
      GM_setValue("timeline_width", currentWidth);
    });

    drawer
      .querySelector(".timeline-drawer-close")
      .addEventListener("click", closeDrawer);
    drawer
      .querySelector(".timeline-drawer-refresh")
      .addEventListener("click", () => {
        loadTimelineTopics();
      });

    // Tab 切换逻辑
    function bindTabEvents() {
      drawer
        .querySelectorAll(
          ".timeline-tab, .timeline-grid-item, .timeline-grid-item-fixed",
        )
        .forEach((tab) => {
          tab.addEventListener("click", (e) => {
            const tabIdValue = tab.dataset.tab;
            const idValue = tab.dataset.id;

            // 处理 Modal 里的点击
            if (
              tab.classList.contains("timeline-grid-item") ||
              tab.classList.contains("timeline-grid-item-fixed")
            ) {
              modal.classList.remove("open");
            }

            if (tab.classList.contains("active")) return;

            drawer
              .querySelectorAll(".timeline-tab.active")
              .forEach((el) => el.classList.remove("active"));
            drawer
              .querySelectorAll(".timeline-grid-item.active")
              .forEach((el) => el.classList.remove("active"));

            // 同步高亮
            let targetTab, targetGrid;
            if (idValue) {
              targetTab = drawer.querySelector(
                `.timeline-tab[data-id="${idValue}"]`,
              );
              targetGrid = drawer.querySelector(
                `.timeline-grid-item[data-id="${idValue}"]`,
              );
            } else {
              targetTab = drawer.querySelector(
                `.timeline-tab[data-tab="${tabIdValue}"]`,
              );
              targetGrid = drawer.querySelector(
                `.timeline-grid-item-fixed[data-tab="${tabIdValue}"]`,
              );
            }

            if (targetTab) {
              targetTab.classList.add("active");
              // 关键:点击 Modal 里的 Tab 时,让顶部的 Tab 栏同步滚动到可视区域
              if (
                tab.classList.contains("timeline-grid-item") ||
                tab.classList.contains("timeline-grid-item-fixed")
              ) {
                targetTab.scrollIntoView({
                  behavior: "smooth",
                  block: "nearest",
                  inline: "center",
                });
              }
            }
            if (targetGrid) targetGrid.classList.add("active");

            currentTab =
              tabIdValue || (idValue ? CATEGORY_CONFIG[idValue].tabId : "all");
            if (!currentTab) currentTab = "all"; // 兜底处理
            currentCategoryId = idValue || null;

            loadTimelineTopics(0, false, true); // 切换 Tab 时也触发自动补全(如果当前有过滤条件)
            startAutoRefresh(); // 切换 Tab 后重新计算定时器状态
          });
        });

      // 绑定过滤事件
      drawer.querySelectorAll(".timeline-filter-item").forEach((item) => {
        item.addEventListener("click", () => {
          if (item.classList.contains("active")) return;
          drawer
            .querySelectorAll(".timeline-filter-item")
            .forEach((el) => el.classList.remove("active"));
          item.classList.add("active");
          currentFilter = item.dataset.filter;
          loadTimelineTopics(0, false, true); // 切换过滤条件时重新请求并触发补全
        });
      });
    }

    bindTabEvents();

    // 更多 Tab 弹窗逻辑
    const modal = drawer.querySelector(".timeline-tabs-modal");
    const moreBtn = drawer.querySelector(".timeline-tabs-more-btn");
    const closeBtn = drawer.querySelector(".timeline-tabs-close-modal");

    moreBtn.addEventListener("click", () => modal.classList.toggle("open"));
    closeBtn.addEventListener("click", () => modal.classList.remove("open"));

    // 拖拽排序逻辑
    const grid = drawer.querySelector(".timeline-tabs-grid");
    let dragItem = null;

    grid.addEventListener("dragstart", (e) => {
      if (!e.target.classList.contains("timeline-grid-item")) return;
      dragItem = e.target;
      e.target.classList.add("dragging");
      e.dataTransfer.effectAllowed = "move";
    });

    grid.addEventListener("dragend", (e) => {
      if (!dragItem) return;
      dragItem.classList.remove("dragging");
      grid
        .querySelectorAll(".drop-target")
        .forEach((el) => el.classList.remove("drop-target"));
      dragItem = null;
    });

    grid.addEventListener("dragover", (e) => {
      e.preventDefault();
      const target = e.target.closest(".timeline-grid-item");
      if (target && target !== dragItem) {
        grid
          .querySelectorAll(".drop-target")
          .forEach((el) => el.classList.remove("drop-target"));
        target.classList.add("drop-target");
      }
    });

    grid.addEventListener("dragleave", (e) => {
      const target = e.target.closest(".timeline-grid-item");
      if (target) target.classList.remove("drop-target");
    });

    grid.addEventListener("drop", (e) => {
      e.preventDefault();
      const target = e.target.closest(".timeline-grid-item");
      if (!target || !dragItem || target === dragItem) return;

      const orderArr = Array.from(
        grid.querySelectorAll(".timeline-grid-item"),
      ).map((item) => item.dataset.id);

      const idxSource = orderArr.indexOf(dragItem.dataset.id);
      const idxTarget = orderArr.indexOf(target.dataset.id);

      // 互换位置
      [orderArr[idxSource], orderArr[idxTarget]] = [
        orderArr[idxTarget],
        orderArr[idxSource],
      ];

      GM_setValue("timeline_tabs_order", orderArr);

      // 重新刷新显示
      refreshTabLayout();
      modal.classList.add("open"); // 保持 Modal 开启状态以查看结果
    });

    function refreshTabLayout() {
      const barContainer = drawer.querySelector(".timeline-tabs-container");
      const gridContainer = drawer.querySelector(".timeline-tabs-grid");
      const activeId = currentCategoryId;
      const activeTab = currentTab;

      const order = GM_getValue("timeline_tabs_order", []);
      const configs = Object.entries(CATEGORY_CONFIG)
        .filter(([_, cfg]) => cfg.tabId)
        .map(([id, cfg]) => ({ id, ...cfg }));

      if (order.length > 0) {
        configs.sort((a, b) => {
          const idxA = order.indexOf(a.id);
          const idxB = order.indexOf(b.id);
          if (idxA === -1 && idxB === -1) return 0;
          if (idxA === -1) return 1;
          if (idxB === -1) return -1;
          return idxA - idxB;
        });
      }

      // 刷新顶部 Tab 栏
      let barHtml = `<div class="timeline-tab ${
        activeTab === "all" ? "active" : ""
      }" data-tab="all">全部</div>`;
      barHtml += configs
        .map(
          (cfg) =>
            `<div class="timeline-tab ${
              activeId == cfg.id ? "active" : ""
            }" data-tab="${cfg.tabId}" data-id="${cfg.id}">${cfg.name}</div>`,
        )
        .join("");
      barContainer.innerHTML = barHtml;

      // 同时更新弹窗 Grid 里的列表以匹配新顺序
      let gridHtml = `<div class="timeline-grid-item-fixed ${
        activeTab === "all" ? "active" : ""
      }" data-tab="all" style="padding: 6px 4px; background: var(--primary-low, #eee); border-radius: 4px; font-size: 12px; text-align: center; color: var(--primary-medium);">全部</div>`;
      gridHtml += configs
        .map(
          (cfg) =>
            `<div class="timeline-grid-item ${
              activeId == cfg.id ? "active" : ""
            }" draggable="true" data-id="${cfg.id}" title="${cfg.name}">${
              cfg.name
            }</div>`,
        )
        .join("");
      gridContainer.innerHTML = gridHtml;

      bindTabEvents(); // 重新绑定事件
    }

    // Tab 栏拖拽滚动逻辑
    const tabsContainer = drawer.querySelector(".timeline-tabs-container");
    let isMoving = false;
    let startX;
    let scrollLeft;
    let hasMoved = false;

    tabsContainer.addEventListener("mousedown", (e) => {
      isMoving = true;
      hasMoved = false;
      startX = e.pageX - tabsContainer.offsetLeft;
      scrollLeft = tabsContainer.scrollLeft;
      // 延迟添加样式,避免误触
      tabsContainer.classList.add("grabbing");
    });

    document.addEventListener("mousemove", (e) => {
      if (!isMoving) return;
      const x = e.pageX - tabsContainer.offsetLeft;
      const walk = (x - startX) * 1.5;
      if (Math.abs(walk) > 5) {
        hasMoved = true;
      }
      tabsContainer.scrollLeft = scrollLeft - walk;
    });

    document.addEventListener("mouseup", () => {
      if (!isMoving) return;
      isMoving = false;
      tabsContainer.classList.remove("grabbing");
    });

    // 防止拖拽时触发 Tab 点击
    tabsContainer.addEventListener(
      "click",
      (e) => {
        if (hasMoved) {
          e.preventDefault();
          e.stopImmediatePropagation();
        }
      },
      { capture: true },
    );

    // 鼠标滚轮左右滚动
    tabsContainer.addEventListener(
      "wheel",
      (e) => {
        if (e.deltaY !== 0) {
          e.preventDefault();
          tabsContainer.scrollLeft += e.deltaY;
        }
      },
      { passive: false },
    );

    refreshTabLayout(); // 初始化布局
    // 滚动加载更多
    const content = drawer.querySelector(".timeline-drawer-content");
    content.addEventListener("scroll", () => {
      if (!isDrawerOpen || isLoadingMore || !hasMorePages) return;

      const scrollTop = content.scrollTop;
      const scrollHeight = content.scrollHeight;
      const clientHeight = content.clientHeight;

      if (scrollTop + clientHeight >= scrollHeight - 100) {
        autoLoadCount = 0; // 手动触发滚动加载时,重置自动计数
        loadMoreTopics();
      }
    });

    document.body.appendChild(drawer);

    // 遮罩层 (小屏使用)
    let overlay = document.querySelector(".timeline-overlay");
    if (!overlay) {
      overlay = document.createElement("div");
      overlay.className = "timeline-overlay";
      // overlay.addEventListener("click", (e) => {
      //   // 只有点击 overlay 本身才关闭,防止事件冒泡导致误触发
      //   if (e.target === overlay) {
      //     closeDrawer();
      //   }
      // });
      document.body.appendChild(overlay);
    }

    createSettingsModal(drawer);
  }

  // ========== 创建设置弹窗 ==========
  function createSettingsModal(parent = document.body) {
    if (document.querySelector(".timeline-settings-modal")) return;

    const modal = document.createElement("div");
    modal.className = "timeline-settings-modal";

    const enableNotif = GM_getValue("timeline_enable_notification", false);
    const enableSlogan = GM_getValue("timeline_enable_slogan", false);
    const interval = GM_getValue("timeline_refresh_interval", 0);
    const notifMode = GM_getValue("timeline_notif_mode", "all"); // "all" 或 "specified"
    const notifUsersRaw = GM_getValue("timeline_notif_users", "");

    let selectedUsers = [];
    const migrateUsers = (raw) => {
      if (!raw) return [];
      if (typeof raw === "string" && raw.trim().startsWith("[")) {
        try {
          return JSON.parse(raw);
        } catch (e) {
          return [];
        }
      } else if (typeof raw === "string") {
        return raw
          .split(",")
          .map((u) => ({
            username: u.trim(),
            name: u.trim(),
            avatar_template: "",
          }))
          .filter((u) => u.username);
      }
      return Array.isArray(raw) ? raw : [];
    };

    selectedUsers = migrateUsers(notifUsersRaw);

    modal.innerHTML = `
      <div class="timeline-settings-header">
        <span>偏好设置</span>
        <span class="timeline-settings-close">✕</span>
      </div>
      <div class="timeline-settings-group">
        <div class="timeline-settings-row">
          <span class="timeline-settings-label" style="margin-bottom: 0;">自动刷新通知</span>
          <label class="timeline-switch">
            <input type="checkbox" id="timeline-notif-toggle" ${
              enableNotif ? "checked" : ""
            }>
            <span class="timeline-slider"></span>
          </label>
        </div>
        <p style="font-size: 11px; color: var(--primary-low-mid, #aaa); margin-top: 4px;">开启后,后台自动刷出的新帖将通过系统通知提醒</p>
      </div>
      <div class="timeline-settings-group timeline-notif-mode-group" style="display: ${
        enableNotif ? "block" : "none"
      };">
        <label class="timeline-settings-label">通知范围</label>
        <div class="timeline-settings-row" style="gap: 16px;">
          <label style="display: flex; align-items: center; gap: 4px; cursor: pointer;">
            <input type="radio" name="timeline-notif-mode" value="all" ${
              notifMode === "all" ? "checked" : ""
            }>
            <span style="font-size: 13px;">全部用户</span>
          </label>
          <label style="display: flex; align-items: center; gap: 4px; cursor: pointer;">
            <input type="radio" name="timeline-notif-mode" value="specified" ${
              notifMode === "specified" ? "checked" : ""
            }>
            <span style="font-size: 13px;">指定用户</span>
          </label>
        </div>
      </div>
      <div class="timeline-settings-group timeline-notif-users-group" style="display: ${
        enableNotif && notifMode === "specified" ? "block" : "none"
      };">
        <label class="timeline-settings-label">指定用户通知</label>
        <div class="timeline-user-search-container">
          <input type="text" class="timeline-user-search-input" placeholder="搜索用户名添加...">
          <div class="timeline-following-trigger" title="关注的人">
            <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
            <span>关注</span>
          </div>
          <div class="timeline-user-search-results"></div>
          
          <!-- 关注的人子弹窗 -->
          <div class="timeline-sub-modal" id="timeline-following-modal">
            <div class="timeline-sub-modal-header">
              <span class="timeline-sub-modal-title">关注的人</span>
              <div style="display: flex; gap: 8px; align-items: center;">
                <span class="timeline-import-btn" id="timeline-import-all-following" style="font-size: 11px; padding: 2px 6px;">一键导入</span>
                <span class="timeline-sub-modal-close">✕</span>
              </div>
            </div>
            <div class="timeline-sub-modal-content">
              <div class="timeline-following-list" id="timeline-following-list">
                <div style="font-size: 11px; color: var(--primary-low-mid); padding: 12px; text-align: center; width: 100%;">正在获取关注列表...</div>
              </div>
            </div>
          </div>
        </div>
        <div class="timeline-user-list" id="timeline-notif-user-list"></div>
        <p style="font-size: 11px; color: var(--primary-low-mid, #aaa); margin-top: 8px;">只有这些用户发帖时才会收到通知</p>
      </div>
      <div class="timeline-settings-group">
        <div class="timeline-settings-row">
          <span class="timeline-settings-label" style="margin-bottom: 0;">核心价值观特效</span>
          <label class="timeline-switch">
            <input type="checkbox" id="timeline-slogan-toggle" ${
              enableSlogan ? "checked" : ""
            }>
            <span class="timeline-slider"></span>
          </label>
        </div>
        <p style="font-size: 11px; color: var(--primary-low-mid, #aaa); margin-top: 4px;">开启后,点击鼠标将显示"真诚、友善、团结、专业"特效</p>
      </div>
      <div class="timeline-settings-group">
        <label class="timeline-settings-label">自动刷新间隔 (秒)</label>
        <div class="timeline-settings-row">
          <input type="number" id="timeline-interval-input" class="timeline-input" min="0" value="${interval}">
          <span style="font-size: 13px; color: var(--primary-medium, #888);">0 为关闭</span>
        </div>
      </div>
      <div style="margin-top: 24px; display: flex; justify-content: flex-end;">
        <button class="timeline-retry-btn" id="timeline-settings-save" style="margin-top: 0; padding: 6px 16px;">确定</button>
      </div>
    `;

    const getAvatarUrl = (template, size = 60) => {
      if (!template)
        return "https://linux.do/letter_avatar_proxy/v4/letter/n/48db5f/64.png";
      if (template.startsWith("http")) return template;
      return "https://linux.do" + template.replace("{size}", size);
    };

    // 预定义引用,供后续逻辑和回调使用
    const followingModal = modal.querySelector("#timeline-following-modal");
    const followingTrigger = modal.querySelector(".timeline-following-trigger");
    const followingList = modal.querySelector("#timeline-following-list");
    let renderFollowing = () => {};

    const renderUsers = () => {
      const list = modal.querySelector("#timeline-notif-user-list");
      if (!list) return;
      list.innerHTML = selectedUsers
        .map(
          (user) => `
        <div class="timeline-selected-user">
          <img src="${getAvatarUrl(
            user.avatar_template,
          )}" class="timeline-user-avatar ${
            user.username === "neo" ? "square-avatar" : ""
          }">
          <div class="timeline-selected-user-info">
            <div class="timeline-selected-user-name">${escapeHtml(
              user.name || user.username,
            )}</div>
            <div class="timeline-selected-user-username">@${escapeHtml(
              user.username,
            )}</div>
          </div>
          <div class="timeline-user-remove" data-username="${escapeHtml(
            user.username,
          )}">✕</div>
        </div>
      `,
        )
        .join("");

      list.querySelectorAll(".timeline-user-remove").forEach((btn) => {
        btn.addEventListener("click", () => {
          const username = btn.dataset.username;
          selectedUsers = selectedUsers.filter((u) => u.username !== username);
          renderUsers();
          if (followingModal && followingModal.classList.contains("open")) {
            renderFollowing();
          }
        });
      });
    };

    // 将更新函数挂载到 DOM 元素上,方便 showSettingsModal 调用
    modal.refreshUsers = (raw) => {
      selectedUsers = migrateUsers(raw);
      renderUsers();
      if (followingModal && followingModal.classList.contains("open")) {
        renderFollowing();
      }
    };

    renderUsers();

    const searchInput = modal.querySelector(".timeline-user-search-input");
    const resultsContainer = modal.querySelector(
      ".timeline-user-search-results",
    );
    let searchTimer = null;

    searchInput.addEventListener("input", () => {
      clearTimeout(searchTimer);
      const term = searchInput.value.trim();
      if (!term) {
        resultsContainer.classList.remove("active");
        return;
      }

      searchTimer = setTimeout(async () => {
        try {
          const response = await fetch(
            `/search/query.json?term=${encodeURIComponent(
              term,
            )}&type_filter=exclude_topics`,
            {
              headers: {
                // "X-CSRF-Token": getCsrfToken(),
                "X-Requested-With": "XMLHttpRequest",
              },
            },
          );
          const data = await response.json();
          const users = data.users || [];

          if (users.length > 0) {
            resultsContainer.innerHTML = users
              .map(
                (user) => `
              <div class="timeline-user-search-item" data-user='${JSON.stringify(
                {
                  username: user.username,
                  name: user.name,
                  avatar_template: user.avatar_template,
                },
              ).replace(/'/g, "&apos;")}'>
                <img src="${getAvatarUrl(
                  user.avatar_template,
                )}" class="timeline-user-avatar ${
                  user.username === "neo" ? "square-avatar" : ""
                }">
                <div style="display:flex; flex-direction:column; min-width:0;">
                  <div style="font-weight:600; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(
                    user.name || user.username,
                  )}</div>
                  <div style="font-size:11px; color:var(--primary-medium);">@${escapeHtml(
                    user.username,
                  )}</div>
                </div>
              </div>
            `,
              )
              .join("");
            resultsContainer.classList.add("active");

            resultsContainer
              .querySelectorAll(".timeline-user-search-item")
              .forEach((item) => {
                item.addEventListener("click", () => {
                  const user = JSON.parse(item.dataset.user);
                  if (
                    !selectedUsers.find((u) => u.username === user.username)
                  ) {
                    selectedUsers.push(user);
                    renderUsers();
                  }
                  searchInput.value = "";
                  resultsContainer.classList.remove("active");
                });
              });
          } else {
            resultsContainer.innerHTML =
              '<div style="padding: 10px; text-align: center; font-size: 13px; color: var(--primary-medium);">未找到用户</div>';
            resultsContainer.classList.add("active");
          }
        } catch (e) {
          console.error("Search failed", e);
        }
      }, 300);
    });

    document.addEventListener("click", (e) => {
      if (
        !searchInput.contains(e.target) &&
        !resultsContainer.contains(e.target)
      ) {
        resultsContainer.classList.remove("active");
      }
    });

    // 关注的人逻辑处理
    let followingUsers = [];
    // 移除重复声明,直接使用上方定义的变量

    followingTrigger.addEventListener("click", (e) => {
      e.stopPropagation();
      const isOpen = followingModal.classList.contains("open");

      // 关闭结果搜索
      resultsContainer.classList.remove("active");

      if (!isOpen) {
        followingModal.classList.add("open");
        fetchFollowing();
      } else {
        followingModal.classList.remove("open");
      }
    });

    modal
      .querySelector(".timeline-sub-modal-close")
      .addEventListener("click", (e) => {
        e.stopPropagation();
        followingModal.classList.remove("open");
      });

    const fetchFollowing = async () => {
      try {
        // 获取当前用户名
        const username = myUserName;

        if (!username) {
          followingList.innerHTML =
            '<div style="font-size: 11px; color: var(--primary-medium); padding: 12px; text-align: center; width: 100%;">未登录或无法获取用户名</div>';
          return;
        }

        const followRes = await fetch(`/u/${username}/follow/following`, {
          headers: {
            "X-Requested-With": "XMLHttpRequest",
          },
        });

        if (!followRes.ok) throw new Error("Fetch failed");

        const data = await followRes.json();
        // 适配数据结构:Discourse 接口通常返回 {following: [...]}
        followingUsers = data.following || (Array.isArray(data) ? data : []);

        if (followingUsers.length > 0) {
          renderFollowing();
        } else {
          followingList.innerHTML =
            '<div style="font-size: 11px; color: var(--primary-medium); padding: 12px; text-align: center; width: 100%;">暂无关注的人</div>';
        }
      } catch (e) {
        console.error("[时间线] 获取关注列表失败", e);
        followingList.innerHTML =
          '<div style="font-size: 11px; color: var(--danger); padding: 12px; text-align: center; width: 100%;">加载失败,请重试</div>';
      }
    };

    // 真正的 renderFollowing 定义
    renderFollowing = () => {
      followingList.innerHTML = followingUsers
        .map((user) => {
          const isSelected = selectedUsers.some(
            (u) => u.username === user.username,
          );
          return `
        <div class="timeline-following-item ${
          isSelected ? "selected" : ""
        }" title="${escapeHtml(user.name || user.username)} (@${escapeHtml(
          user.username,
        )})" data-user='${JSON.stringify({
          username: user.username,
          name: user.name,
          avatar_template: user.avatar_template,
        }).replace(/'/g, "&apos;")}'>
          <img src="${getAvatarUrl(user.avatar_template, 48)}" class="${
            user.username === "neo" ? "square-avatar" : ""
          }">
        </div>
      `;
        })
        .join("");

      followingList
        .querySelectorAll(".timeline-following-item")
        .forEach((item) => {
          item.addEventListener("click", (e) => {
            e.stopPropagation(); // 阻止冒泡,防止全局点击逻辑误关弹窗
            const user = JSON.parse(item.dataset.user);
            const index = selectedUsers.findIndex(
              (u) => u.username === user.username,
            );

            if (index > -1) {
              // 已存在则移除 (取消勾选)
              selectedUsers.splice(index, 1);
            } else {
              // 不存在则添加 (新增勾选)
              selectedUsers.push(user);
            }

            renderUsers();
            renderFollowing(); // 点击后实时更新勾选状态
          });
        });
    };

    // 绑定一键导入
    modal
      .querySelector("#timeline-import-all-following")
      .addEventListener("click", () => {
        let addedCount = 0;
        followingUsers.forEach((user) => {
          if (!selectedUsers.find((u) => u.username === user.username)) {
            selectedUsers.push({
              username: user.username,
              name: user.name,
              avatar_template: user.avatar_template,
            });
            addedCount++;
          }
        });
        if (addedCount > 0) {
          renderUsers();
          renderFollowing(); // 刷新勾选状态
        }
      });

    parent.appendChild(modal);

    // 通知开关联动显示/隐藏通知模式选项
    const notifToggle = modal.querySelector("#timeline-notif-toggle");
    const notifModeGroup = modal.querySelector(".timeline-notif-mode-group");
    const notifUsersGroup = modal.querySelector(".timeline-notif-users-group");

    notifToggle.addEventListener("change", () => {
      notifModeGroup.style.display = notifToggle.checked ? "block" : "none";
      const selectedMode =
        modal.querySelector('input[name="timeline-notif-mode"]:checked')
          ?.value || "all";
      notifUsersGroup.style.display =
        notifToggle.checked && selectedMode === "specified" ? "block" : "none";
    });

    // 通知模式切换联动显示/隐藏用户输入框
    modal
      .querySelectorAll('input[name="timeline-notif-mode"]')
      .forEach((radio) => {
        radio.addEventListener("change", () => {
          const isSpecified = radio.value === "specified";
          notifUsersGroup.style.display = isSpecified ? "block" : "none";
          // 移除自动触发 fetchFollowing,改为由用户点击按钮触发
        });
      });

    // 点击外部关闭子弹窗
    document.addEventListener("click", (e) => {
      if (
        followingModal.classList.contains("open") &&
        !followingModal.contains(e.target) &&
        e.target !== followingTrigger
      ) {
        followingModal.classList.remove("open");
      }
    });

    modal
      .querySelector(".timeline-settings-close")
      .addEventListener("click", () => {
        modal.classList.remove("open");
      });

    modal
      .querySelector("#timeline-settings-save")
      .addEventListener("click", () => {
        const newEnableNotif = modal.querySelector(
          "#timeline-notif-toggle",
        ).checked;
        const newEnableSlogan = modal.querySelector(
          "#timeline-slogan-toggle",
        ).checked;
        const newInterval =
          parseInt(modal.querySelector("#timeline-interval-input").value) || 0;
        const newNotifMode =
          modal.querySelector('input[name="timeline-notif-mode"]:checked')
            ?.value || "all";
        GM_setValue("timeline_enable_notification", newEnableNotif);
        GM_setValue("timeline_enable_slogan", newEnableSlogan);
        GM_setValue(
          "timeline_refresh_interval",
          newInterval >= 0 ? newInterval : 0,
        );
        GM_setValue("timeline_notif_mode", newNotifMode);
        GM_setValue("timeline_notif_users", JSON.stringify(selectedUsers));

        modal.classList.remove("open");
        updateCountdownDisplay();
        startAutoRefresh();
      });
  }

  // ========== 显示设置弹窗 ==========
  function showSettingsModal() {
    const modal = document.querySelector(".timeline-settings-modal");
    if (!modal) {
      createSettingsModal();
    }
    const m = document.querySelector(".timeline-settings-modal");
    // 更新当前值
    const enableNotif = GM_getValue("timeline_enable_notification", false);
    const notifMode = GM_getValue("timeline_notif_mode", "all");
    const notifUsers = GM_getValue("timeline_notif_users", "");

    m.querySelector("#timeline-notif-toggle").checked = enableNotif;
    m.querySelector("#timeline-slogan-toggle").checked = GM_getValue(
      "timeline_enable_slogan",
      false,
    );
    m.querySelector("#timeline-interval-input").value = GM_getValue(
      "timeline_refresh_interval",
      0,
    );

    // 更新通知模式相关
    const notifModeGroup = m.querySelector(".timeline-notif-mode-group");
    const notifUsersGroup = m.querySelector(".timeline-notif-users-group");
    notifModeGroup.style.display = enableNotif ? "block" : "none";

    const modeRadios = m.querySelectorAll('input[name="timeline-notif-mode"]');
    modeRadios.forEach((radio) => {
      radio.checked = radio.value === notifMode;
    });

    // 更新用户列表
    if (m.refreshUsers) {
      m.refreshUsers(notifUsers);
    }
    notifUsersGroup.style.display =
      enableNotif && notifMode === "specified" ? "block" : "none";

    m.classList.add("open");
  }

  // ========== 打开/关闭抽屉 ==========
  let lastToggleTime = 0;
  function toggleDrawer() {
    // 防抖:300ms 内不重复触发
    const now = Date.now();
    if (now - lastToggleTime < 300) {
      return;
    }
    lastToggleTime = now;
    if (isDrawerOpen) {
      closeDrawer();
    } else {
      openDrawer();
    }
  }

  // ========== 版本比较 ==========
  function compareVersions(v1, v2) {
    const a = v1.split(".");
    const b = v2.split(".");
    for (let i = 0; i < Math.max(a.length, b.length); i++) {
      const n1 = parseInt(a[i] || 0);
      const n2 = parseInt(b[i] || 0);
      if (n1 > n2) return 1;
      if (n1 < n2) return -1;
    }
    return 0;
  }

  // ========== 检查更新 ==========
  function checkUpdate() {
    fetch(
      "https://update.greasyfork.org/scripts/564655/LINUX%20DO%20Timeline.meta.js",
    )
      .then((r) => r.text())
      .then((text) => {
        const match = text.match(/@version\s+([\d.]+)/);
        if (match) {
          const latestVersion = match[1];
          if (compareVersions(latestVersion, scriptVersion) > 0) {
            showUpdateBadge(latestVersion);
          }
        }
      })
      .catch((e) => console.log("[时间线] 检查更新失败", e));
  }

  // ========== 显示更新标记 ==========
  function showUpdateBadge(latestVersion) {
    const badge = document.querySelector(".timeline-update-badge");
    if (badge) {
      badge.style.display = "block";
      badge.title = `发现新版本 v${latestVersion},点击前往更新`;
      badge.onclick = (e) => {
        e.stopPropagation();
        window.open(
          "https://greasyfork.org/zh-CN/scripts/564655-linux-do-timeline",
        );
      };
    }
  }

  // ========== 更新倒计时显示 ==========
  function updateCountdownDisplay() {
    const el = document.querySelector(".timeline-countdown");
    if (!el) return;
    const interval = GM_getValue("timeline_refresh_interval", 0);
    if (interval <= 0) {
      el.textContent = "手动";
      el.classList.remove("active");
    } else {
      el.textContent = `${remainingSeconds}s`;
      el.classList.add("active");
    }
  }

  // ========== 开启自动刷新 ==========
  function startAutoRefresh() {
    stopAutoRefresh();

    const interval = GM_getValue("timeline_refresh_interval", 0);
    if (interval > 0) {
      remainingSeconds = interval;
      updateCountdownDisplay();

      countdownTimer = setInterval(() => {
        remainingSeconds--;
        if (remainingSeconds <= 0) {
          remainingSeconds = interval;
          if (isDrawerOpen && !isLoading && !isLoadingMore) {
            console.log("[时间线] 自动刷新中...");
            loadTimelineTopics(0, true);
          }
        }
        updateCountdownDisplay();
      }, 1000);
    } else {
      updateCountdownDisplay();
    }
  }

  // ========== 停止自动刷新 ==========
  function stopAutoRefresh() {
    if (countdownTimer) {
      clearInterval(countdownTimer);
      countdownTimer = null;
    }
  }

  // ========== 打开抽屉 ==========
  async function openDrawer() {
    createDrawer();
    isDrawerOpen = true;
    GM_setValue("timeline_drawer_open", true);

    // 通知其他标签页关闭抽屉
    broadcastDrawerOpened();

    document.querySelector(".timeline-drawer").classList.add("open");

    // 如果是小屏,显示遮罩层,且不应用 push 类
    if (window.innerWidth <= 768) {
      document.querySelector(".timeline-overlay")?.classList.add("active");
    } else {
      document.body.classList.add("timeline-drawer-push");
    }

    // 加载数据
    if (allTopics.length > 0) {
      renderTopics();
      const savedScroll = GM_getValue("timeline_last_scroll_top", 0);
      const content = document.querySelector(".timeline-drawer-content");
      if (content && savedScroll > 0) {
        // 使用 setTimeout 确保渲染完成后滚动
        setTimeout(() => {
          content.scrollTop = savedScroll;
        }, 50);
      }
      loadTimelineTopics(0, true); // 静默刷新
    } else {
      loadTimelineTopics();
    }

    updateCountdownDisplay();
    startAutoRefresh();
    checkUpdate();
  }

  // ========== 关闭抽屉 ==========
  function closeDrawer() {
    const content = document.querySelector(".timeline-drawer-content");
    if (content) {
      GM_setValue("timeline_last_scroll_top", content.scrollTop);
    }

    isDrawerOpen = false;
    GM_setValue("timeline_drawer_open", false);

    const drawer = document.querySelector(".timeline-drawer");
    if (drawer) drawer.classList.remove("open");
    document.body.classList.remove("timeline-drawer-push");
    document.querySelector(".timeline-overlay")?.classList.remove("active");
    updateCountdownDisplay();
    stopAutoRefresh();
  }

  // ========== 加载帖子 ==========
  async function loadTimelineTopics(
    retryCount = 0,
    isSilent = false,
    triggerAuto = false,
  ) {
    const MAX_RETRIES = 3;
    const RETRY_DELAY = 500;

    if (isLoading) return;
    isLoading = true;

    const content = document.querySelector(".timeline-drawer-content");
    if (!content) {
      isLoading = false;
      return;
    }

    if (!isSilent) {
      currentPage = 0;
      hasMorePages = true;
      allTopics = [];
      loadedTopicIds.clear();
      autoLoadCount = 0; // 重置自动加载计数

      content.innerHTML = `
            <div class="timeline-loading-2">
                <div class="timeline-spinner"></div>
                <span>${
                  retryCount > 0
                    ? `重试中 (${retryCount}/${MAX_RETRIES})...`
                    : "加载中..."
                }</span>
            </div>
        `;
    }

    try {
      let url = "/latest.json?order=created";
      if (currentTab !== "all" && currentCategoryId) {
        url = `/c/${currentTab}/${currentCategoryId}/l/latest.json?filter=latest`;
      }
      const response = await fetch(url);

      // 获取响应头的x-discourse-username
      myUserName = response.headers.get("x-discourse-username");

      // 检查响应状态
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const data = await response.json();

      if (data?.users) {
        data.users.forEach((user) => {
          usersMap[user.id] = user;
        });
      }

      if (data?.topic_list?.topics) {
        const topics = data.topic_list.topics;

        if (isSilent) {
          // 静默刷新:添加新帖子 + 更新旧帖未读状态
          let hasNew = false;
          let hasStatusChange = false;
          let newTopicsList = [];

          // 构建最新数据的映射表,用于对比和同步旧帖子数据
          const topicUpdateMap = new Map();
          topics.forEach((t) => {
            topicUpdateMap.set(t.id, t);
          });

          // 更新已有帖子的各种动态指标(阅读、回复、点赞、未读状态)
          allTopics.forEach((existingTopic) => {
            if (topicUpdateMap.has(existingTopic.id)) {
              const latest = topicUpdateMap.get(existingTopic.id);
              // 使用 Object.assign 同步所有属性,同时保持对象引用不变
              Object.assign(existingTopic, latest);
              hasStatusChange = true;
            }
          });

          // 添加新帖子
          topics.forEach((t) => {
            if (!loadedTopicIds.has(t.id)) {
              loadedTopicIds.add(t.id);
              allTopics.unshift(t);
              newTopicsList.push(t);
              hasNew = true;
            }
          });

          if (hasNew || hasStatusChange) {
            allTopics.sort(
              (a, b) => new Date(b.created_at) - new Date(a.created_at),
            );
            renderTopics(newTopicsList.map((t) => t.id));

            // 发送系统通知(仅新帖子)
            if (hasNew && GM_getValue("timeline_enable_notification", false)) {
              newTopicsList.forEach((topic) => {
                notifyNewPost(topic);
              });
            }
          }
        } else {
          allTopics = topics.filter((t) => {
            if (loadedTopicIds.has(t.id)) return false;
            loadedTopicIds.add(t.id);
            return true;
          });

          // 按发帖时间排序
          allTopics.sort(
            (a, b) => new Date(b.created_at) - new Date(a.created_at),
          );
          renderTopics();
        }
      }
    } catch (e) {
      console.error(
        `[时间线] 加载失败 (尝试 ${retryCount + 1}/${MAX_RETRIES + 1}):`,
        e,
      );

      // 自动重试
      if (retryCount < MAX_RETRIES) {
        isLoading = false;
        await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
        return loadTimelineTopics(retryCount + 1, isSilent, triggerAuto);
      }

      showError(e.message || "未知错误");
    }

    isLoading = false;

    // 如果过滤后的列表太少(少于10条),且还有更多页,自动加载下一页
    if (
      hasMorePages &&
      !isLoadingMore &&
      (triggerAuto || currentFilter !== "all")
    ) {
      const filteredCount = (function () {
        if (currentFilter === "unseen")
          return allTopics.filter((t) => t.unseen).length;
        if (currentFilter === "read")
          return allTopics.filter((t) => !t.unseen).length;
        return allTopics.length;
      })();

      if (filteredCount < 10) {
        console.log(
          `[时间线] 过滤后仅 ${filteredCount} 条,自动尝试加载更多 (autoLoadCount: ${autoLoadCount})`,
        );
        loadMoreTopics(true);
      }
    }
  }

  // ========== 显示错误信息 ==========
  function showError(errorMsg) {
    const content = document.querySelector(".timeline-drawer-content");
    if (!content) return;

    content.innerHTML = `
            <div class="timeline-error">
                <div class="timeline-error-icon">⚠️</div>
                <div class="timeline-error-msg">加载失败</div>
                <div class="timeline-error-detail">${escapeHtml(errorMsg)}</div>
                <button class="timeline-retry-btn">重试</button>
            </div>
        `;

    content
      .querySelector(".timeline-retry-btn")
      ?.addEventListener("click", () => {
        loadTimelineTopics();
      });
  }

  // ========== 加载更多 ==========
  async function loadMoreTopics(fromAuto = false) {
    if (isLoadingMore || !hasMorePages) return;
    isLoadingMore = true;
    currentPage++;

    const content = document.querySelector(".timeline-drawer-content");
    const list = content?.querySelector(".timeline-topic-list");
    if (!list) {
      isLoadingMore = false;
      return;
    }

    // 添加加载提示
    let loadingEl = document.querySelector(".timeline-load-more");
    if (!loadingEl) {
      loadingEl = document.createElement("div");
      loadingEl.className = "timeline-load-more";
      loadingEl.innerHTML =
        '<span class="timeline-load-more-spinner"></span>加载更多...';
      content.appendChild(loadingEl);
    }

    try {
      let url = `/latest.json?order=created&page=${currentPage}`;
      if (currentTab !== "all" && currentCategoryId) {
        url = `/c/${currentTab}/${currentCategoryId}/l/latest.json?filter=latest&page=${currentPage}`;
      }
      const data = await fetch(url).then((r) => r.json());

      loadingEl?.remove();

      if (!data?.topic_list?.topics || data.topic_list.topics.length === 0) {
        hasMorePages = false;
        showNoMore();
        isLoadingMore = false;
        return;
      }

      if (data?.users) {
        data.users.forEach((user) => {
          usersMap[user.id] = user;
        });
      }

      const newTopics = data.topic_list.topics.filter((t) => {
        if (loadedTopicIds.has(t.id)) return false;
        loadedTopicIds.add(t.id);
        return true;
      });

      if (newTopics.length === 0) {
        hasMorePages = false;
        showNoMore();
        isLoadingMore = false;
        return;
      }

      newTopics.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
      allTopics = allTopics.concat(newTopics);

      renderTopics(); // 渲染完整列表

      // 如果是自动加载触发的,且加载后过滤结果依然不足10条,继续递归加载
      if (fromAuto && hasMorePages) {
        const filteredCount = (function () {
          if (currentFilter === "unseen")
            return allTopics.filter((t) => t.unseen).length;
          if (currentFilter === "read")
            return allTopics.filter((t) => !t.unseen).length;
          return allTopics.length;
        })();

        if (filteredCount < 10) {
          if (autoLoadCount >= 5) {
            console.log(
              `[时间线] 已连续自动加载 ${autoLoadCount} 次,停止递归。`,
            );
            autoLoadCount = 0;
            isLoadingMore = false;
            return;
          }
          console.log(
            `[时间线] 加载后过滤结果 ${filteredCount} 条,继续递归加载 (第 ${
              autoLoadCount + 1
            } 次)...`,
          );
          autoLoadCount++;
          isLoadingMore = false; // 暂时重置以允许下一次调用
          return loadMoreTopics(true);
        } else {
          autoLoadCount = 0; // 满足条件,重置计数
        }
      }
    } catch (e) {
      console.error("[时间线] 加载更多失败:", e);
      loadingEl?.remove();
    }

    isLoadingMore = false;
  }

  // ========== 显示没有更多 ==========
  function showNoMore() {
    const content = document.querySelector(".timeline-drawer-content");
    if (content && !content.querySelector(".timeline-no-more")) {
      const noMore = document.createElement("div");
      noMore.className = "timeline-no-more";
      noMore.textContent = "没有更多了";
      content.appendChild(noMore);
    }
  }

  // ========== 渲染帖子列表 ==========
  function renderTopics(newTopicIds = []) {
    const content = document.querySelector(".timeline-drawer-content");
    if (!content) return;

    content.innerHTML = "";

    // 应用本地过滤
    let filteredTopics = allTopics;
    if (currentFilter === "unseen") {
      filteredTopics = allTopics.filter((t) => t.unseen);
    } else if (currentFilter === "read") {
      // 这里的逻辑要严谨:Discourse 的 unseen 可能为 undefined/false,两者都代表已读
      filteredTopics = allTopics.filter((t) => !t.unseen);
    }

    if (filteredTopics.length === 0) {
      content.innerHTML = `
                <div class="timeline-empty-hint" style="padding: 40px 20px; text-align: center; color: var(--primary-medium, #888);">
                    ${
                      currentFilter === "unseen"
                        ? "暂无未读帖子"
                        : currentFilter === "read"
                          ? "暂无已读帖子"
                          : "暂无帖子"
                    }
                    ${
                      hasMorePages
                        ? '<div style="font-size: 11px; margin-top: 8px; opacity: 0.7;">正在尝试加载更多...</div>'
                        : ""
                    }
                </div>
            `;
      return;
    }

    const list = document.createElement("ul");
    list.className = "timeline-topic-list";

    filteredTopics.forEach((topic) => {
      const isNew = newTopicIds.includes(topic.id);
      const item = createTopicItem(topic, isNew);
      list.appendChild(item);
    });

    content.appendChild(list);
  }

  // ========== 发送系统通知 ==========
  function notifyNewPost(topic) {
    let name = "";
    let username = "";
    let avatarUrl = "";
    if (topic.posters && topic.posters.length > 0) {
      const userId = topic.posters[0].user_id;
      const user = usersMap[userId];
      if (user) {
        name = user.name || "";
        username = user.username;
        if (user.avatar_template) {
          avatarUrl = user.avatar_template.replace("{size}", "120");
          if (!avatarUrl.startsWith("http")) {
            avatarUrl = "https://linux.do" + avatarUrl;
          }
        }
      }
    }

    // 检查通知模式
    const notifMode = GM_getValue("timeline_notif_mode", "all");
    if (notifMode === "specified") {
      const notifUsersVal = GM_getValue("timeline_notif_users", "");
      let specifiedUsernames = [];
      if (notifUsersVal.trim().startsWith("[")) {
        try {
          specifiedUsernames = JSON.parse(notifUsersVal).map((u) =>
            u.username.toLowerCase(),
          );
        } catch (e) {
          specifiedUsernames = [];
        }
      } else {
        specifiedUsernames = notifUsersVal
          .split(",")
          .map((u) => u.trim().toLowerCase())
          .filter((u) => u.length > 0);
      }

      // 如果指定用户列表为空,或者当前用户不在列表中,则不发送通知
      if (
        specifiedUsernames.length === 0 ||
        !specifiedUsernames.includes(username.toLowerCase())
      ) {
        return;
      }
    }

    GM_notification({
      title: `${name} (@${username}) 发布了新帖子`,
      text: `【${getCategoryName(topic.category_id)}】${topic.title}`,
      image: avatarUrl,
      highlight: false, // 修复:设置为 false 防止通知发出时自动夺取浏览器焦点
      silent: false,
      timeout: 20000,
      onclick: () => {
        window.focus();
        const path = `/t/${topic.slug}/${topic.id}`;
        navigateTo(path);
      },
    });
  }

  // ========== 创建帖子项 ==========
  function createTopicItem(topic, isNew = false) {
    const item = document.createElement("li");
    item.className = "timeline-topic-item";
    if (isNew) {
      item.classList.add("new-topic-highlight");
      // 10秒后移除高亮类
      setTimeout(() => {
        item.classList.remove("new-topic-highlight");
      }, 10000);
    }

    // 获取用户信息
    let avatarUrl = "";
    let name = "";
    let username = "";
    if (topic.posters && topic.posters.length > 0) {
      const userId = topic.posters[0].user_id;
      const user = usersMap[userId];
      if (user) {
        name = user.name;
        username = user.username;
        if (user.avatar_template) {
          avatarUrl = user.avatar_template.replace("{size}", "45");
          if (!avatarUrl.startsWith("http")) {
            avatarUrl = "https://linux.do" + avatarUrl;
          }
        }
      }
    }

    const createdTime = formatRelativeTime(new Date(topic.created_at));
    const views =
      topic.views >= 1000 ? (topic.views / 1000).toFixed(1) + "k" : topic.views;
    const replies = topic.posts_count - 1;

    // 获取分类名称、图标和颜色(优先使用映射表)
    const categoryName = getCategoryName(topic.category_id);
    const categoryIcon = getCategoryIcon(topic.category_id);
    const categoryColor = getCategoryColor(topic.category_id);

    // 生成分类 HTML(带 SVG 图标和颜色)
    let categoryHtml = "";
    if (categoryName) {
      categoryHtml = `<span class="timeline-category" style="--category-color: ${categoryColor}"><svg class="timeline-category-icon"><use href="#${categoryIcon}"></use></svg>${escapeHtml(
        categoryName,
      )}</span>`;
    }

    // 生成标签 HTML
    let tagsHtml = "";
    if (topic.tags && topic.tags.length > 0) {
      const tagItems = topic.tags
        .map(
          (tag) =>
            `<span class="timeline-tag">${escapeHtml(
              typeof tag === "string" ? tag : tag.name,
            )}</span>`,
        )
        .join("");
      tagsHtml = `<div class="timeline-tags">${tagItems}</div>`;
    }

    // 未读标识
    const unseenDot = topic.unseen
      ? '<span class="timeline-unseen-dot"></span>'
      : "";

    // 显示名称(如果 name 不为空且与 username 不同)
    const displayName =
      name && name !== username
        ? `<span class="timeline-topic-name" data-user-card="${username}">${escapeHtml(
            name,
          )}</span>`
        : "";

    item.innerHTML = `
            ${unseenDot}
            <div class="timeline-topic-header">
                ${
                  avatarUrl
                    ? `<img class="timeline-topic-avatar ${
                        username === "neo" ? "square-avatar" : ""
                      }" src="${avatarUrl}" alt="${username}" data-user-card="${username}">`
                    : ""
                }
                <div class="timeline-topic-meta">
                    <div class="timeline-topic-user-info">
                        ${displayName}
                        <span class="timeline-topic-username" data-user-card="${username}">${escapeHtml(
                          username,
                        )}</span>
                    </div>
                    <span class="timeline-topic-time">${createdTime}</span>
                </div>
            </div>
            <h4 class="timeline-topic-title">${escapeHtml(topic.title)}</h4>
            <div class="timeline-topic-category-tags">
                ${categoryHtml}
                ${tagsHtml}
            </div>
            <div class="timeline-topic-stats">
                <span class="timeline-topic-stat">💬 ${replies}</span>
                <span class="timeline-topic-stat">👁 ${views}</span>
                <span class="timeline-topic-stat">❤️ ${
                  topic.like_count || 0
                }</span>
            </div>
        `;

    // 点击头像或用户名时显示用户卡片
    item.querySelectorAll("[data-user-card]").forEach((el) => {
      el.addEventListener("click", (e) => {
        e.stopPropagation();
        const username = el.getAttribute("data-user-card");
        showUserCard(username);
      });
    });

    // 左键点击:当前页面跳转
    item.addEventListener("click", (e) => {
      if (e.button !== 0) return;
      // 立即标记为已读
      markTopicAsRead(topic, item);
      const path = `/t/${topic.slug}/${topic.id}`;
      navigateTo(path);
      // 如果是小屏,点击后自动关闭抽屉
      if (window.innerWidth <= 768) {
        console.log("closeDrawer1111");
        closeDrawer();
      }
    });

    // 中键点击:新标签页打开
    item.addEventListener("mousedown", (e) => {
      if (e.button === 1) {
        e.preventDefault(); // 阻止自动滚动
      }
    });
    item.addEventListener("mouseup", (e) => {
      if (e.button === 1) {
        e.preventDefault();
        // 立即标记为已读
        markTopicAsRead(topic, item);
        const url = `https://linux.do/t/${topic.slug}/${topic.id}`;
        window.open(url, "_blank");
      }
    });

    return item;
  }

  // ========== 标记帖子为已读 ==========
  function markTopicAsRead(topic, itemElement) {
    if (!topic.unseen) return; // 已经是已读状态

    // 更新数据
    topic.unseen = false;

    // 更新 allTopics 中对应的帖子
    const existingTopic = allTopics.find((t) => t.id === topic.id);
    if (existingTopic) {
      existingTopic.unseen = false;
    }

    // 移除未读小蓝点
    const unseenDot = itemElement.querySelector(".timeline-unseen-dot");
    if (unseenDot) {
      unseenDot.remove();
    }
  }

  // ========== 显示用户卡片 ==========
  function showUserCard(username) {
    const path = `/u/${username}/summary`;
    navigateTo(path);
  }

  // ========== Discourse 路由跳转 ==========
  function navigateTo(path) {
    const script = document.createElement("script");
    script.textContent = `window.require("discourse/lib/url").default.routeTo("${path}");`;
    document.documentElement.appendChild(script);
    script.remove();
  }

  // ========== 格式化时间 ==========
  function formatRelativeTime(date) {
    const now = new Date();
    const diff = now - date;
    const seconds = Math.floor(diff / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);
    const days = Math.floor(hours / 24);

    if (seconds < 60) return `${Math.max(1, seconds)}秒前`;
    if (minutes < 60) return `${minutes}分钟前`;
    if (hours < 24) return `${hours}小时前`;
    if (days < 30) return `${days}天前`;

    const months = Math.floor(days / 30);
    if (months < 12) return `${months}个月前`;

    return `${Math.floor(months / 12)}年前`;
  }

  // ========== HTML 转义 ==========
  function escapeHtml(text) {
    if (!text) return "";
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
  }

  // ========== ESC 打开/关闭抽屉 ==========
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape") {
      toggleDrawer();
    }
  });

  // ========== 启动 ==========
  createFloatButton();
  initCoreValueEffect();
  initBroadcastChannel();

  // 恢复上次的抽屉状态(先检查其他标签页是否已经打开了抽屉)
  (async () => {
    if (GM_getValue("timeline_drawer_open", false)) {
      const otherHas = await checkOtherTabHasDrawer();
      if (!otherHas) {
        openDrawer();
      }
    }
  })();

  console.log("[时间线] v1.24 已加载 - 悬浮按钮模式");
})();