LINUX DO Timeline

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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 已加载 - 悬浮按钮模式");
})();