Discourse Topic Quick Switcher

Enhance Discourse forums with instant topic switching, current topic highlighting, and quick navigation to previous/next topics

// ==UserScript==
// @name                 Discourse Topic Quick Switcher
// @name:zh-CN           Discourse 话题快捷切换器
// @namespace            https://github.com/utags
// @homepageURL          https://github.com/utags/userscripts#readme
// @supportURL           https://github.com/utags/userscripts/issues
// @version              0.5.0
// @description          Enhance Discourse forums with instant topic switching, current topic highlighting, and quick navigation to previous/next topics
// @description:zh-CN    增强 Discourse 论坛体验,提供即时话题切换、当前话题高亮和上一个/下一个话题的快速导航功能
// @author               Pipecraft
// @license              MIT
// @icon                 https://www.google.com/s2/favicons?sz=64&domain=meta.discourse.org
// @match                https://meta.discourse.org/*
// @match                https://linux.do/*
// @match                https://idcflare.com/*
// @match                https://meta.appinn.net/*
// @match                https://community.openai.com/*
// @match                https://community.cloudflare.com/*
// @match                https://community.wanikani.com/*
// @match                https://forum.cursor.com/*
// @match                https://forum.obsidian.md/*
// @match                https://forum-zh.obsidian.md/*
// @noframes
// @grant                GM.addStyle
// @grant                GM.setValue
// @grant                GM.getValue
// ==/UserScript==

;(async function () {
  'use strict'

  // Configuration
  const CONFIG = {
    // Settings storage key
    SETTINGS_KEY: 'discourse_topic_switcher_settings',
    // Cache key base name
    CACHE_KEY_BASE: 'discourse_topic_list_cache',
    // Cache expiry time (milliseconds) - 1 hour
    CACHE_EXPIRY: 60 * 60 * 1000,
    // Whether to show floating button on topic pages
    SHOW_FLOATING_BUTTON: true,
    // Route check interval (milliseconds)
    ROUTE_CHECK_INTERVAL: 500,
    // Default language (en or zh-CN)
    DEFAULT_LANGUAGE: 'en',
  }

  // User settings with defaults
  let userSettings = {
    language: CONFIG.DEFAULT_LANGUAGE,
    showNavigationButtons: true,
    darkMode: 'auto', // auto, light, dark
    // Custom hotkey settings
    hotkeys: {
      showTopicList: 'Alt+KeyQ',
      nextTopic: 'Alt+KeyW',
      prevTopic: 'Alt+KeyE',
    },
  }

  // Pre-initialized site-specific keys (calculated once at script load)
  const SITE_CACHE_KEY = `${CONFIG.CACHE_KEY_BASE}_${window.location.hostname}`
  const SITE_SETTINGS_KEY = `${CONFIG.SETTINGS_KEY}_${window.location.hostname}`

  // Internationalization support
  const I18N = {
    en: {
      viewTopicList: 'View topic list (press Alt + Q)',
      topicList: 'Topic List',
      cacheExpired: 'Cache expired',
      cachedAgo: 'Cached {time} ago',
      searchPlaceholder: 'Search topics...',
      noResults: 'No matching topics found',
      backToList: 'Back to list',
      topicsCount: '{count} topics',
      currentTopic: 'Current topic',
      sourceFrom: 'Source',
      close: 'Close',
      loading: 'Loading...',
      refresh: 'Refresh',
      replies: 'Replies',
      views: 'Views',
      activity: 'Activity',
      language: 'Language',
      noCachedList:
        'No cached topic list available. Please visit a topic list page first.',
      prevTopic: 'Previous Topic',
      nextTopic: 'Next Topic',
      noPrevTopic: 'No previous topic',
      noNextTopic: 'No next topic',
      settings: 'Settings',
      save: 'Save',
      cancel: 'Cancel',
      showNavigationButtons: 'Show navigation buttons',
      darkMode: 'Dark Mode',
      darkModeAuto: 'Auto',
      darkModeLight: 'Light',
      darkModeDark: 'Dark',
      // Hotkey settings
      hotkeys: 'Hotkeys',
      hotkeyShowTopicList: 'Show topic list',
      hotkeyNextTopic: 'Next topic',
      hotkeyPrevTopic: 'Previous topic',
      hotkeyInputPlaceholder: 'e.g., Alt+KeyQ, Ctrl+KeyK, KeyG',
      hotkeyInvalidFormat: 'Invalid hotkey format',
    },
    'zh-CN': {
      viewTopicList: '查看话题列表(按 Alt + Q 键)',
      topicList: '话题列表',
      cacheExpired: '缓存已过期',
      cachedAgo: '{time}前缓存',
      searchPlaceholder: '搜索话题...',
      noResults: '未找到匹配的话题',
      backToList: '返回列表',
      topicsCount: '{count}个话题',
      currentTopic: '当前话题',
      sourceFrom: '来源',
      close: '关闭',
      loading: '加载中...',
      refresh: '刷新',
      replies: '回复',
      views: '浏览',
      activity: '活动',
      language: '语言',
      noCachedList: '没有可用的话题列表缓存。请先访问一个话题列表页面。',
      prevTopic: '上一个话题',
      nextTopic: '下一个话题',
      noPrevTopic: '没有上一个话题',
      noNextTopic: '没有下一个话题',
      settings: '设置',
      save: '保存',
      cancel: '取消',
      showNavigationButtons: '显示导航按钮',
      darkMode: '深色模式',
      darkModeAuto: '自动',
      darkModeLight: '浅色',
      darkModeDark: '深色',
      // Hotkey settings
      hotkeys: '快捷键',
      hotkeyShowTopicList: '显示话题列表',
      hotkeyNextTopic: '下一个话题',
      hotkeyPrevTopic: '上一个话题',
      hotkeyInputPlaceholder: '例如:Alt+KeyQ, Ctrl+KeyK, KeyG',
      hotkeyInvalidFormat: '快捷键格式无效',
    },
  }

  /**
   * Load user settings from storage
   */
  async function loadUserSettings() {
    const savedSettings = await GM.getValue(SITE_SETTINGS_KEY)
    if (savedSettings) {
      try {
        const parsedSettings = JSON.parse(savedSettings)
        userSettings = { ...userSettings, ...parsedSettings }
      } catch (e) {
        console.error('[DTQS] Error parsing saved settings:', e)
      }
    }
    return userSettings
  }

  /**
   * Save user settings to storage
   */
  async function saveUserSettings() {
    await GM.setValue(SITE_SETTINGS_KEY, JSON.stringify(userSettings))
  }

  // Get user language
  function getUserLanguage() {
    // Use language from settings
    if (
      userSettings.language &&
      (userSettings.language === 'en' || userSettings.language === 'zh-CN')
    ) {
      return userSettings.language
    }

    // Try to get language from browser
    const browserLang = navigator.language || navigator.userLanguage

    // Check if we support this language
    if (browserLang.startsWith('zh')) {
      return 'zh-CN'
    }

    // Default to English
    return CONFIG.DEFAULT_LANGUAGE
  }

  // Current language
  let currentLanguage = getUserLanguage()

  /**
   * Create and show settings dialog
   */
  async function showSettingsDialog() {
    // If dialog already exists, don't create another one
    if (document.getElementById('dtqs-settings-overlay')) {
      return
    }

    // Create overlay
    const overlay = document.createElement('div')
    overlay.id = 'dtqs-settings-overlay'

    // Create dialog
    const dialog = document.createElement('div')
    dialog.id = 'dtqs-settings-dialog'

    // Create dialog content
    dialog.innerHTML = `
      <h2>${t('settings')}</h2>

      <div class="dtqs-setting-item">
        <label for="dtqs-language-select">${t('language')}</label>
        <select id="dtqs-language-select">
          <option value="en" ${userSettings.language === 'en' ? 'selected' : ''}>English</option>
          <option value="zh-CN" ${userSettings.language === 'zh-CN' ? 'selected' : ''}>中文</option>
        </select>
      </div>

      <div class="dtqs-setting-item">
        <label for="dtqs-dark-mode-select">${t('darkMode')}</label>
        <select id="dtqs-dark-mode-select">
          <option value="auto" ${userSettings.darkMode === 'auto' ? 'selected' : ''}>${t('darkModeAuto')}</option>
          <option value="light" ${userSettings.darkMode === 'light' ? 'selected' : ''}>${t('darkModeLight')}</option>
          <option value="dark" ${userSettings.darkMode === 'dark' ? 'selected' : ''}>${t('darkModeDark')}</option>
        </select>
      </div>

      <div class="dtqs-setting-item checkbox-item">
        <label for="dtqs-show-nav-buttons">
          <input type="checkbox" id="dtqs-show-nav-buttons" ${userSettings.showNavigationButtons ? 'checked' : ''}>
          <span>${t('showNavigationButtons')}</span>
        </label>
      </div>

      <div class="dtqs-setting-section">
        <h3>${t('hotkeys')}</h3>

        <div class="dtqs-setting-item">
          <label for="dtqs-hotkey-show-list">${t('hotkeyShowTopicList')}</label>
          <input type="text" id="dtqs-hotkey-show-list" value="${userSettings.hotkeys.showTopicList}" placeholder="${t('hotkeyInputPlaceholder')}">
        </div>

        <div class="dtqs-setting-item">
          <label for="dtqs-hotkey-next-topic">${t('hotkeyNextTopic')}</label>
          <input type="text" id="dtqs-hotkey-next-topic" value="${userSettings.hotkeys.nextTopic}" placeholder="${t('hotkeyInputPlaceholder')}">
        </div>

        <div class="dtqs-setting-item">
          <label for="dtqs-hotkey-prev-topic">${t('hotkeyPrevTopic')}</label>
          <input type="text" id="dtqs-hotkey-prev-topic" value="${userSettings.hotkeys.prevTopic}" placeholder="${t('hotkeyInputPlaceholder')}">
        </div>
      </div>

      <div class="dtqs-buttons">
        <button id="dtqs-settings-save">${t('save')}</button>
        <button id="dtqs-settings-cancel">${t('cancel')}</button>
      </div>
    `

    // Add dialog to overlay
    overlay.appendChild(dialog)

    // Add overlay to page
    document.body.appendChild(overlay)

    // Add event listeners
    const saveButton = document.getElementById('dtqs-settings-save')
    const cancelButton = document.getElementById('dtqs-settings-cancel')

    addTouchSupport(saveButton, async () => {
      // Save language setting
      const languageSelect = document.getElementById('dtqs-language-select')
      userSettings.language = languageSelect.value

      // Save dark mode setting
      const darkModeSelect = document.getElementById('dtqs-dark-mode-select')
      userSettings.darkMode = darkModeSelect.value

      // Save navigation buttons setting
      const showNavButtons = document.getElementById('dtqs-show-nav-buttons')
      userSettings.showNavigationButtons = showNavButtons.checked

      // Save hotkey settings with validation
      const hotkeyShowList = document.getElementById('dtqs-hotkey-show-list')
      const hotkeyNextTopic = document.getElementById('dtqs-hotkey-next-topic')
      const hotkeyPrevTopic = document.getElementById('dtqs-hotkey-prev-topic')

      // Validate hotkey format
      const hotkeyPattern =
        /^(Ctrl\+|Alt\+|Shift\+|Meta\+)*(Key[A-Z]|Digit[0-9]|Space|Enter|Escape|Backspace|Tab|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|F[1-9]|F1[0-2])$/

      const hotkeys = {
        showTopicList: hotkeyShowList.value.trim(),
        nextTopic: hotkeyNextTopic.value.trim(),
        prevTopic: hotkeyPrevTopic.value.trim(),
      }

      // Validate each hotkey
      for (const [key, value] of Object.entries(hotkeys)) {
        if (value && !hotkeyPattern.test(value)) {
          alert(`${t('hotkeyInvalidFormat')}: ${value}`)
          return
        }
      }

      // Check for duplicate hotkeys
      const hotkeyValues = Object.values(hotkeys).filter((v) => v)
      const uniqueHotkeys = new Set(hotkeyValues)
      if (hotkeyValues.length !== uniqueHotkeys.size) {
        alert('Duplicate hotkeys are not allowed')
        return
      }

      userSettings.hotkeys = hotkeys

      // Save settings
      await saveUserSettings()

      // Update language
      currentLanguage = userSettings.language

      // Update dark mode
      detectDarkMode()

      // Close dialog
      closeSettingsDialog()

      // Remove and recreate floating button to apply new settings
      if (floatingButton) {
        hideFloatingButton()
        addFloatingButton()
      }

      // If topic list is open, reopen it to apply new settings
      if (topicListContainer) {
        hideTopicList()
        topicListContainer.remove()
        topicListContainer = null
        setTimeout(() => {
          showTopicList()
        }, 350)
      }
    })

    addTouchSupport(cancelButton, closeSettingsDialog)

    // Close when clicking on overlay (outside dialog)
    overlay.addEventListener('click', (e) => {
      if (e.target === overlay) {
        closeSettingsDialog()
      }
    })
  }

  /**
   * Close settings dialog
   */
  function closeSettingsDialog() {
    const overlay = document.getElementById('dtqs-settings-overlay')
    if (overlay) {
      overlay.remove()
    }
  }

  // Translate function
  function t(key, params = {}) {
    // Get the translation
    let text = I18N[currentLanguage][key] || I18N['en'][key] || key

    // Replace parameters
    for (const param in params) {
      text = text.replace(`{${param}}`, params[param])
    }

    return text
  }

  // Status variables
  let isListVisible = false
  let cachedTopicList = null
  let cachedTopicListTimestamp = 0
  let cachedTopicListUrl = ''
  let cachedTopicListTitle = ''
  let floatingButton = null
  let topicListContainer = null
  let lastUrl = window.location.href
  let urlCheckTimer = null
  let isDarkMode = false
  let isButtonClickable = true // Flag to prevent consecutive clicks
  let prevTopic = null // Previous topic data
  let nextTopic = null // Next topic data
  let isMobileDevice = false // Mobile device detection

  /**
   * Detect if the current device is a mobile device
   */
  function detectMobileDevice() {
    // Check user agent for mobile devices
    const userAgent = navigator.userAgent || navigator.vendor || window.opera
    const mobileRegex =
      /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i

    // Check screen width
    const isSmallScreen = window.innerWidth <= 768

    // Check for touch support
    const hasTouchSupport =
      'ontouchstart' in window || navigator.maxTouchPoints > 0

    // Combine all checks
    isMobileDevice =
      mobileRegex.test(userAgent) || (isSmallScreen && hasTouchSupport)

    console.log(`[DTQS] Mobile device detection: ${isMobileDevice}`)

    // Add mobile class to body for CSS targeting
    if (isMobileDevice) {
      document.body.classList.add('dtqs-mobile-device')
    } else {
      document.body.classList.remove('dtqs-mobile-device')
    }

    return isMobileDevice
  }

  /**
   * Detect dark mode based on user settings
   */
  function detectDarkMode() {
    let shouldUseDarkMode = false

    // Check user's dark mode preference
    switch (userSettings.darkMode) {
      case 'dark':
        // Force dark mode
        shouldUseDarkMode = true
        console.log('[DTQS] Dark mode: Force enabled by user setting')
        break

      case 'light':
        // Force light mode
        shouldUseDarkMode = false
        console.log('[DTQS] Dark mode: Force disabled by user setting')
        break

      case 'auto':
      default:
        // Auto mode - check system and site preferences
        if (window.matchMedia) {
          // Check system preference
          const systemDarkMode = window.matchMedia(
            '(prefers-color-scheme: dark)'
          ).matches

          // Check if the Discourse site is in dark mode
          const discourseBodyClass =
            document.body.classList.contains('dark-scheme') ||
            document.documentElement.classList.contains('dark-scheme') ||
            document.body.dataset.colorScheme === 'dark' ||
            document.documentElement.dataset.colorScheme === 'dark' ||
            document.documentElement.dataset.themeType === 'dark' ||
            // linux.do
            document.querySelector('header picture > source')?.media === 'all'

          // Enable dark mode if the system or site uses it
          shouldUseDarkMode = systemDarkMode || discourseBodyClass

          console.log(
            `[DTQS] Dark mode (auto): System: ${systemDarkMode}, Site: ${discourseBodyClass}, Final: ${shouldUseDarkMode}`
          )
        }
        break
    }

    // Update global dark mode state
    isDarkMode = shouldUseDarkMode

    // Add or remove dark mode class
    if (isDarkMode) {
      document.body.classList.add('topic-list-viewer-dark-mode')
    } else {
      document.body.classList.remove('topic-list-viewer-dark-mode')
    }
  }

  /**
   * Set up dark mode listener
   */
  function setupDarkModeListener() {
    if (window.matchMedia) {
      // Listen for system dark mode changes
      const darkModeMediaQuery = window.matchMedia(
        '(prefers-color-scheme: dark)'
      )

      // Add change listener (only trigger if user is in auto mode)
      const handleSystemChange = (e) => {
        if (userSettings.darkMode === 'auto') {
          detectDarkMode()
        }
      }

      if (darkModeMediaQuery.addEventListener) {
        darkModeMediaQuery.addEventListener('change', handleSystemChange)
      } else if (darkModeMediaQuery.addListener) {
        // Fallback for older browsers
        darkModeMediaQuery.addListener(handleSystemChange)
      }

      // Listen for Discourse theme changes (only trigger if user is in auto mode)
      const observer = new MutationObserver((mutations) => {
        if (userSettings.darkMode === 'auto') {
          mutations.forEach((mutation) => {
            if (
              mutation.attributeName === 'class' ||
              mutation.attributeName === 'data-color-scheme'
            ) {
              detectDarkMode()
            }
          })
        }
      })

      // Observe class changes on body and html elements
      observer.observe(document.body, { attributes: true })
      observer.observe(document.documentElement, { attributes: true })
    }
  }

  /**
   * Initialize the script
   */
  async function init() {
    // Load user settings
    await loadUserSettings()

    // Load cached topic list from storage
    await loadCachedTopicList()

    // Detect mobile device
    detectMobileDevice()

    // Detect dark mode
    detectDarkMode()

    // Set up dark mode listener
    // setupDarkModeListener()

    // Set up mobile device detection on window resize
    window.addEventListener('resize', () => {
      detectMobileDevice()
    })

    // Initial handling of the current page
    handleCurrentPage()

    // Set up URL change detection
    setupUrlChangeDetection()

    // Add global hotkey listener
    addHotkeyListener()
  }

  /**
   * Set up URL change detection
   * Use multiple methods to reliably detect URL changes
   */
  function setupUrlChangeDetection() {
    // Record initial URL
    lastUrl = window.location.href

    // Method 1: Listen for popstate events (handles browser back/forward buttons)
    window.addEventListener('popstate', () => {
      console.log('[DTQS] Detected popstate event')
      handleCurrentPage()
    })

    // Method 2: Use MutationObserver to listen for DOM changes that might indicate a URL change
    const pageObserver = new MutationObserver(() => {
      checkUrlChange('MutationObserver')
    })

    // Start observing DOM changes
    pageObserver.observe(document.body, {
      childList: true,
      subtree: true,
    })

    // Method 3: Set up a regular check as a fallback
    if (urlCheckTimer) {
      clearInterval(urlCheckTimer)
    }

    urlCheckTimer = setInterval(() => {
      checkUrlChange('Interval check')
    }, CONFIG.ROUTE_CHECK_INTERVAL)
  }

  /**
   * Check if the URL has changed
   * @param {string} source The source that triggered the check
   */
  function checkUrlChange(source) {
    const currentUrl = window.location.href
    if (currentUrl !== lastUrl) {
      console.log(`[DTQS] URL change detected (Source: ${source})`, currentUrl)
      lastUrl = currentUrl
      handleCurrentPage()
    }
  }

  /**
   * Handle the current page
   */
  function handleCurrentPage() {
    // If the list is visible, hide it
    if (isListVisible) {
      hideTopicList()
    }

    // Perform different actions based on the current page type
    if (isTopicPage()) {
      // On a topic page, add the floating button
      console.log('[DTQS] On a topic page, show button')
      if (CONFIG.SHOW_FLOATING_BUTTON) {
        addFloatingButton()

        // Update navigation buttons if we're on a topic page
        updateNavigationButtons()
      }

      // On a topic page, pre-render the list (if cached)
      if (cachedTopicList && !topicListContainer) {
        // Use setTimeout to ensure the DOM is fully loaded
        setTimeout(() => {
          prerenderTopicList()
        }, 100)
      }
    } else if (isTopicListPage()) {
      // On a topic list page, cache the current list
      console.log('[DTQS] On a list page, update cache')
      cacheCurrentTopicList()

      // Hide the button on the list page
      hideFloatingButton()
    } else {
      // On other pages, hide the button
      hideFloatingButton()

      // Observe the topic list element
      observeTopicListElement()
    }
  }

  /**
   * Check if the current page is a topic list page
   * @returns {boolean} Whether it is a topic list page
   */
  function isTopicListPage() {
    return (
      document.querySelector(
        '.contents table.topic-list tbody.topic-list-body'
      ) !== null
    )
  }

  /**
   * Observe the appearance of the topic list element
   * Solves the problem that the list element may not be rendered when the page loads
   */
  function observeTopicListElement() {
    // Create an observer instance
    const observer = new MutationObserver((mutations, obs) => {
      // Check if the list element has appeared
      if (
        document.querySelector(
          '.contents table.topic-list tbody.topic-list-body'
        )
      ) {
        console.log('[DTQS] Detected that the list element has been rendered')
        // If the list element appears, re-handle the current page
        handleCurrentPage()
        // The list element has been found, stop observing
        obs.disconnect()
      }
    })

    // Configure observer options
    const config = {
      childList: true, // Observe changes to the target's child nodes
      subtree: true, // Observe all descendant nodes
    }

    // Start observing the document body
    observer.observe(document.body, config)

    // Set a timeout to avoid indefinite observation
    setTimeout(() => {
      observer.disconnect()
    }, 10000) // Stop observing after 10 seconds
  }

  /**
   * Check if the current page is a topic page
   * @returns {boolean} Whether it is a topic page
   */
  function isTopicPage() {
    return window.location.pathname.includes('/t/')
  }

  /**
   * Check for URL changes
   */
  function checkForUrlChanges() {
    const currentUrl = window.location.href
    if (currentUrl !== lastUrl) {
      lastUrl = currentUrl

      // If we're on a topic page, update the button
      if (isTopicPage()) {
        addFloatingButton()
        // Update navigation buttons with new adjacent topics
        updateNavigationButtons()
      } else {
        // Remove the button if not on a topic page
        if (floatingButton) {
          floatingButton.remove()
          floatingButton = null
        }
      }

      // Hide the topic list if it's visible
      if (isListVisible) {
        hideTopicList()
      }
    }
  }

  /**
   * Get the current topic ID
   * @returns {number|null} The current topic ID or null
   */
  function getCurrentTopicId() {
    // Extract topic ID from the URL
    const match = window.location.pathname.match(/\/t\/[^\/]+\/(\d+)/)
    return match ? parseInt(match[1]) : null
  }

  /**
   * Check if a topic row is visible (not hidden)
   * @param {Element} row - The topic row element
   * @returns {boolean} - Whether the topic is visible
   */
  function isTopicVisible(row) {
    // Use more reliable method to detect element visibility
    if (typeof row.checkVisibility === 'function') {
      return row.checkVisibility()
    }

    // If checkVisibility is not available, use offsetParent for detection
    return row.offsetParent !== null
  }

  /**
   * Find adjacent topics (previous and next) from the cached topic list
   * @returns {Object} Object containing previous and next topics
   */
  function findAdjacentTopics() {
    // If no cached topic list, return empty result
    if (!cachedTopicList) {
      return { prev: null, next: null }
    }

    // Get current topic ID
    const currentId = getCurrentTopicId()
    if (!currentId) {
      return { prev: null, next: null }
    }

    // Create a temporary container to parse the cached HTML
    const tempContainer = document.createElement('div')
    tempContainer.style.position = 'absolute'
    tempContainer.style.visibility = 'hidden'
    tempContainer.innerHTML = `<table>${cachedTopicList}</table>`

    // Add to document.body to ensure offsetParent works correctly
    document.body.appendChild(tempContainer)

    // Get all topic rows
    const topicRows = tempContainer.querySelectorAll('tr')
    if (!topicRows.length) {
      // Remove the temporary container from document.body
      tempContainer.remove()
      return { prev: null, next: null }
    }

    // Find the current topic index
    let currentIndex = -1
    for (let i = 0; i < topicRows.length; i++) {
      const row = topicRows[i]
      const topicLink = row.querySelector('a.title')
      if (!topicLink) continue

      // Extract topic ID from the link
      const match = topicLink.href.match(/\/t\/[^\/]+\/(\d+)/)
      if (match && parseInt(match[1]) === currentId) {
        currentIndex = i
        break
      }
    }

    // If current topic not found in the list
    if (currentIndex === -1) {
      // Remove the temporary container from document.body
      tempContainer.remove()
      return { prev: null, next: null }
    }

    // Get previous visible topic
    let prevTopic = null
    for (let i = currentIndex - 1; i >= 0; i--) {
      const prevRow = topicRows[i]
      if (!isTopicVisible(prevRow)) continue

      const prevLink = prevRow.querySelector('a.title')
      if (prevLink) {
        prevTopic = {
          id: extractTopicId(prevLink.href),
          title: prevLink.textContent.trim(),
          url: prevLink.href,
        }
        break
      }
    }

    // Get next visible topic
    let nextTopic = null
    for (let i = currentIndex + 1; i < topicRows.length; i++) {
      const nextRow = topicRows[i]
      if (!isTopicVisible(nextRow)) continue

      const nextLink = nextRow.querySelector('a.title')
      if (nextLink) {
        nextTopic = {
          id: extractTopicId(nextLink.href),
          title: nextLink.textContent.trim(),
          url: nextLink.href,
        }
        break
      }
    }

    // Remove the temporary container from document.body
    tempContainer.remove()

    return { prev: prevTopic, next: nextTopic }
  }

  /**
   * Update navigation buttons with adjacent topics
   */
  function updateNavigationButtons() {
    // Find adjacent topics
    const { prev, next } = findAdjacentTopics()
    console.log('[DTQS] Adjacent topics:', prev, next)

    // Store for global access
    prevTopic = prev
    nextTopic = next

    // Update previous topic button
    const prevButton = document.querySelector('.topic-nav-button.prev-topic')
    if (prevButton) {
      const titleSpan = prevButton.querySelector('.topic-nav-title')
      if (prev) {
        titleSpan.textContent = prev.title
        prevButton.title = prev.title
        prevButton.style.opacity = '1'
        prevButton.style.pointerEvents = 'auto'
      } else {
        titleSpan.textContent = ''
        prevButton.title = t('noPrevTopic')
        prevButton.style.opacity = '0.5'
        prevButton.style.pointerEvents = 'none'
      }
    }

    // Update next topic button
    const nextButton = document.querySelector('.topic-nav-button.next-topic')
    if (nextButton) {
      const titleSpan = nextButton.querySelector('.topic-nav-title')
      if (next) {
        titleSpan.textContent = next.title
        nextButton.title = next.title
        nextButton.style.opacity = '1'
        nextButton.style.pointerEvents = 'auto'
      } else {
        titleSpan.textContent = ''
        nextButton.title = t('noNextTopic')
        nextButton.style.opacity = '0.5'
        nextButton.style.pointerEvents = 'none'
      }
    }
  }

  /**
   * Navigate to previous topic
   */
  function navigateToPrevTopic() {
    if (prevTopic && prevTopic.url) {
      navigateWithSPA(prevTopic.url)
    }
  }

  /**
   * Navigate to next topic
   */
  function navigateToNextTopic() {
    if (nextTopic && nextTopic.url) {
      navigateWithSPA(nextTopic.url)
    }
  }

  /**
   * Extract topic ID from a topic URL
   * @param {string} url The topic URL
   * @returns {number|null} The topic ID or null
   */
  function extractTopicId(url) {
    const match = url.match(/\/t\/[^\/]+\/(\d+)/)
    return match ? parseInt(match[1]) : null
  }

  /**
   * Cache the current topic list
   */
  function cacheCurrentTopicList() {
    // Check if the list element exists
    const topicListBody = document.querySelector('tbody.topic-list-body')
    if (topicListBody) {
      // If the list element exists, process it directly
      updateTopicListCache(topicListBody)

      // Listen for list content changes (when scrolling to load more)
      observeTopicListChanges(topicListBody)
    } else {
      // If the list element does not exist, listen for its appearance
      console.log('[DTQS] Waiting for the topic list element to appear')
      observeTopicListAppearance()
    }
  }

  /**
   * Observe the appearance of the topic list element
   */
  function observeTopicListAppearance() {
    // Create an observer instance
    const observer = new MutationObserver((mutations, obs) => {
      // Check if the list element has appeared
      const topicListBody = document.querySelector('tbody.topic-list-body')
      if (topicListBody) {
        console.log('[DTQS] Detected that the list element has been rendered')
        // Process the list content
        processTopicList(topicListBody)
        // Listen for list content changes
        observeTopicListChanges(topicListBody)
        // The list element has been found, stop observing
        obs.disconnect()
      }
    })

    // Configure observer options
    const config = {
      childList: true, // Observe changes to the target's child nodes
      subtree: true, // Observe all descendant nodes
    }

    // Start observing the document body
    observer.observe(document.body, config)
  }

  /**
   * Observe topic list content changes (when scrolling to load more)
   * @param {Element} topicListBody The topic list element
   */
  function observeTopicListChanges(topicListBody) {
    // Record the current number of rows
    let previousRowCount = topicListBody.querySelectorAll('tr').length

    // Create an observer instance
    const observer = new MutationObserver((mutations) => {
      // Get the current number of rows
      const currentRowCount = topicListBody.querySelectorAll('tr').length

      // If the number of rows increases, it means more topics have been loaded
      if (currentRowCount > previousRowCount) {
        console.log(
          `[DTQS] Detected list update, rows increased from ${previousRowCount} to ${currentRowCount}`
        )
        // Update the cache
        updateTopicListCache(topicListBody)
        // Update the row count record
        previousRowCount = currentRowCount
      }
    })

    // Configure observer options
    const config = {
      childList: true, // Observe changes to the target's child nodes
      subtree: true, // Observe all descendant nodes
    }

    // Start observing the list element
    observer.observe(topicListBody, config)
  }

  /**
   * Update the topic list cache
   * @param {Element} topicListBody The topic list element
   */
  async function updateTopicListCache(topicListBody) {
    // Ensure the list has content
    const topicRows = topicListBody.querySelectorAll('tr')
    if (topicRows.length === 0) {
      console.log('[DTQS] Topic list is empty, not caching')
      return
    }

    console.log('[DTQS] Updating topic list cache')

    // Clone the node to save the complete topic list
    const clonedTopicList = topicListBody.cloneNode(true)

    // Save the current URL to show the source when the list is popped up
    const currentUrl = window.location.href

    // Get the list title
    let listTitle = t('topicList')
    // const titleElement = document.querySelector(
    //   '.category-name, .page-title h1, .topic-list-heading h2'
    // )
    // if (titleElement) {
    //   listTitle = titleElement.textContent.trim()
    // }
    const title = document.title.replace(/ - .*/, '').trim()
    if (title) {
      listTitle = title
    }

    // Get current category information (if any)
    let categoryInfo = ''
    const categoryBadge = document.querySelector(
      '.category-name .badge-category'
    )
    if (categoryBadge) {
      categoryInfo = categoryBadge.textContent.trim()
    }

    console.log(
      `[DTQS] Caching topic list "${listTitle}", containing ${topicRows.length} topics`
    )

    // Save to cache
    cachedTopicList = clonedTopicList.outerHTML
    cachedTopicListTimestamp = Date.now()
    cachedTopicListUrl = currentUrl
    cachedTopicListTitle = listTitle

    // Save to GM storage with site-specific key
    await GM.setValue(SITE_CACHE_KEY, {
      html: cachedTopicList,
      timestamp: cachedTopicListTimestamp,
      url: cachedTopicListUrl,
      title: cachedTopicListTitle,
      category: categoryInfo,
      topicCount: topicRows.length,
    })

    // Remove the list container, it needs to be re-rendered
    if (topicListContainer) {
      topicListContainer.remove()
      topicListContainer = null
    }
  }

  /**
   * Load the cached topic list from storage
   */
  async function loadCachedTopicList() {
    const cache = await GM.getValue(SITE_CACHE_KEY)
    if (cache) {
      cachedTopicList = cache.html
      cachedTopicListTimestamp = cache.timestamp
      cachedTopicListUrl = cache.url
      cachedTopicListTitle = cache.title
    }
  }

  /**
   * Add a floating button
   */
  function addFloatingButton() {
    // If the button already exists, do not add it again
    if (document.getElementById('topic-list-viewer-button')) return

    // Create the button container
    floatingButton = document.createElement('div')
    floatingButton.id = 'topic-list-viewer-button'

    // Create navigation container
    const navContainer = document.createElement('div')
    navContainer.className = 'topic-nav-container'

    // Control navigation buttons visibility based on user settings
    if (!userSettings.showNavigationButtons) {
      navContainer.classList.add('hide-nav-buttons')
    }

    // Create previous topic button
    const prevButton = document.createElement('div')
    prevButton.className = 'topic-nav-button prev-topic'
    prevButton.innerHTML = `
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="15 18 9 12 15 6"></polyline>
      </svg>
      <span class="topic-nav-title"></span>
    `
    prevButton.title = t('prevTopic')

    addTouchSupport(prevButton, navigateToPrevTopic)

    // Create center button
    const centerButton = document.createElement('div')
    centerButton.className = 'topic-nav-button center-button'
    centerButton.innerHTML = `
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
    `
    centerButton.title = t('viewTopicList')
    addTouchSupport(centerButton, toggleTopicList)

    // Create next topic button
    const nextButton = document.createElement('div')
    nextButton.className = 'topic-nav-button next-topic'
    nextButton.innerHTML = `
      <span class="topic-nav-title"></span>
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="9 18 15 12 9 6"></polyline>
      </svg>
    `
    nextButton.title = t('nextTopic')
    addTouchSupport(nextButton, navigateToNextTopic)

    // Add all elements to the container
    navContainer.appendChild(prevButton)
    navContainer.appendChild(centerButton)
    navContainer.appendChild(nextButton)
    floatingButton.appendChild(navContainer)

    // Add to page
    document.body.appendChild(floatingButton)

    // Update navigation buttons
    updateNavigationButtons()
  }

  /**
   * Check if any unwanted modifier keys are pressed
   * @param {KeyboardEvent} event - The keyboard event
   * @returns {boolean} True if any unwanted modifier key is pressed
   */
  function hasUnwantedModifierKeys(event) {
    return event.shiftKey || event.ctrlKey || event.metaKey
  }

  /**
   * Check if the focus is on an input element
   * @returns {boolean} True if focus is on an input element
   */
  function isFocusOnInput() {
    const activeElement = document.activeElement
    if (!activeElement) return false

    const tagName = activeElement.tagName.toLowerCase()
    const inputTypes = ['input', 'textarea', 'select']

    // Check if it's an input element
    if (inputTypes.includes(tagName)) {
      return true
    }

    // Check if it's a contenteditable element
    if (activeElement.contentEditable === 'true') {
      return true
    }

    // Check if it's inside a contenteditable element
    let parent = activeElement.parentElement
    while (parent) {
      if (parent.contentEditable === 'true') {
        return true
      }
      parent = parent.parentElement
    }

    return false
  }

  /**
   * Parse hotkey string into components
   * @param {string} hotkeyStr - Hotkey string like "Alt+KeyQ" or "Ctrl+Shift+KeyA"
   * @returns {Object} - Object with modifier flags and key code
   */
  function parseHotkey(hotkeyStr) {
    if (!hotkeyStr || typeof hotkeyStr !== 'string') {
      return null
    }

    const parts = hotkeyStr.split('+')
    const result = {
      ctrl: false,
      alt: false,
      shift: false,
      meta: false,
      code: null,
    }

    for (const part of parts) {
      const trimmed = part.trim()
      switch (trimmed) {
        case 'Ctrl':
          result.ctrl = true
          break
        case 'Alt':
          result.alt = true
          break
        case 'Shift':
          result.shift = true
          break
        case 'Meta':
          result.meta = true
          break
        default:
          result.code = trimmed
          break
      }
    }

    return result.code ? result : null
  }

  /**
   * Check if event matches the parsed hotkey
   * @param {KeyboardEvent} event - Keyboard event
   * @param {Object} parsedHotkey - Parsed hotkey object
   * @returns {boolean} - True if event matches hotkey
   */
  function matchesHotkey(event, parsedHotkey) {
    if (!parsedHotkey) {
      return false
    }

    return (
      event.ctrlKey === parsedHotkey.ctrl &&
      event.altKey === parsedHotkey.alt &&
      event.shiftKey === parsedHotkey.shift &&
      event.metaKey === parsedHotkey.meta &&
      event.code === parsedHotkey.code
    )
  }

  /**
   * Add a hotkey listener
   */
  function addHotkeyListener() {
    document.addEventListener(
      'keydown',
      function (event) {
        // Skip if unwanted modifier keys are pressed (but allow Alt)
        // if (hasUnwantedModifierKeys(event)) {
        //   return
        // }

        // Skip if focus is on an input element
        if (isFocusOnInput()) {
          return
        }

        // Check for hotkeys only on topic pages
        if (!isTopicPage()) {
          return
        }

        console.log(
          `[DTQS] keydown event: key=${event.key}, code=${event.code}, modifiers: Ctrl=${event.ctrlKey}, Alt=${event.altKey}, Shift=${event.shiftKey}, Meta=${event.metaKey}`
        )

        // Parse configured hotkeys
        const showListHotkey = parseHotkey(userSettings.hotkeys.showTopicList)
        const nextTopicHotkey = parseHotkey(userSettings.hotkeys.nextTopic)
        const prevTopicHotkey = parseHotkey(userSettings.hotkeys.prevTopic)

        // Check for show topic list hotkey
        if (showListHotkey && matchesHotkey(event, showListHotkey)) {
          event.preventDefault()
          event.stopPropagation()
          toggleTopicList()
          return
        }

        // Check for next topic hotkey
        if (nextTopicHotkey && matchesHotkey(event, nextTopicHotkey)) {
          event.preventDefault()
          event.stopPropagation()
          navigateToNextTopic()
          return
        }

        // Check for previous topic hotkey
        if (prevTopicHotkey && matchesHotkey(event, prevTopicHotkey)) {
          event.preventDefault()
          event.stopPropagation()
          navigateToPrevTopic()
          return
        }

        // ESC key to close topic list (hardcoded for usability)
        if (
          !event.ctrlKey &&
          !event.altKey &&
          !event.shiftKey &&
          !event.metaKey &&
          event.key === 'Escape' &&
          isListVisible
        ) {
          event.preventDefault()
          event.stopPropagation()
          hideTopicList()
          return
        }
      },
      true
    )
  }

  /**
   * Hide the floating button
   */
  function hideFloatingButton() {
    if (floatingButton && floatingButton.parentNode) {
      floatingButton.parentNode.removeChild(floatingButton)
      floatingButton = null
    }
  }

  /**
   * Toggle the display state of the topic list
   * Includes debounce logic to prevent rapid consecutive clicks
   */
  function toggleTopicList() {
    // If button is not clickable, return immediately
    if (!isButtonClickable) {
      return
    }

    // Set button to non-clickable state
    isButtonClickable = false

    // Execute the original toggle logic
    if (isListVisible) {
      hideTopicList()
    } else {
      showTopicList()
    }

    // Set a timeout to restore button clickable state after 800ms
    setTimeout(() => {
      isButtonClickable = true
    }, 800)
  }

  /**
   * Navigate to the specified URL using SPA routing
   * @param {string} url The target URL
   */
  function navigateWithSPA(url) {
    // Hide the topic list
    hideTopicList()

    // Try to use pushState for SPA navigation
    try {
      console.log(`[DTQS] Navigating to ${url} using SPA routing`)

      // Use history API for navigation
      const urlObj = new URL(url)
      const pathname = urlObj.pathname

      // Update history
      history.pushState({}, '', pathname)

      // Trigger popstate event so Discourse can handle the route change
      window.dispatchEvent(new Event('popstate'))

      // Handle the current page
      setTimeout(handleCurrentPage, 100)
    } catch (error) {
      // If SPA navigation fails, fall back to normal navigation
      console.log(
        `[DTQS] SPA navigation failed, falling back to normal navigation to ${url}`,
        error
      )
      window.location.href = url
    }
  }

  /**
   * Pre-render the topic list
   */
  function prerenderTopicList() {
    // Record start time
    const startTime = performance.now()

    // If there is no cached topic list, do not pre-render
    if (!cachedTopicList) {
      console.log('[DTQS] No cached topic list available, cannot pre-render')
      return
    }

    // If the container already exists, do not create it again
    if (topicListContainer) {
      return
    }

    console.log('[DTQS] Pre-rendering topic list')

    // Check if the cache is expired
    const now = Date.now()
    const cacheAge = now - cachedTopicListTimestamp
    let cacheStatus = ''

    if (cacheAge > CONFIG.CACHE_EXPIRY) {
      cacheStatus = `<div class="cache-status expired">${t('cacheExpired')} (${formatTimeAgo(cacheAge)})</div>`
    } else {
      cacheStatus = `<div class="cache-status">${t('cachedAgo', { time: formatTimeAgo(cacheAge) })}</div>`
    }

    // Create the main container
    topicListContainer = document.createElement('div')
    topicListContainer.id = 'topic-list-viewer-container'

    // Create the overlay
    const overlay = document.createElement('div')
    overlay.className = 'topic-list-viewer-overlay'

    // Add an event listener to close the list when clicking the overlay
    overlay.addEventListener('click', (event) => {
      // If button is not clickable, return immediately
      if (!isButtonClickable) {
        return
      }
      // Make sure the click is on the overlay itself, not its children
      if (event.target === overlay) {
        hideTopicList()
      }
    })

    // Create the content container
    const contentContainer = document.createElement('div')
    contentContainer.className = 'topic-list-viewer-wrapper'

    // Add the content container to the main container
    topicListContainer.appendChild(overlay)
    topicListContainer.appendChild(contentContainer)

    // Add to body
    document.body.appendChild(topicListContainer)

    // Try to get the position and width of the #main-outlet element
    const mainOutlet = document.getElementById('main-outlet')
    if (mainOutlet) {
      console.log(
        '[DTQS] Adjusting list container position and width to match #main-outlet'
      )

      // Adjust position and width when the container is displayed
      const adjustContainerPosition = () => {
        if (topicListContainer && topicListContainer.style.display === 'flex') {
          const mainOutletRect = mainOutlet.getBoundingClientRect()

          // Set the position and width of the content container to match #main-outlet
          contentContainer.style.width = `${mainOutletRect.width}px`
          contentContainer.style.maxWidth = `${mainOutletRect.width}px`
          contentContainer.style.marginLeft = 'auto'
          contentContainer.style.marginRight = 'auto'
        }
      }

      // Add a listener to adjust the position
      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (
            mutation.attributeName === 'style' &&
            topicListContainer &&
            topicListContainer.style.display === 'flex'
          ) {
            adjustContainerPosition()
          }
        })
      })

      observer.observe(topicListContainer, { attributes: true })

      // Readjust on window resize
      window.addEventListener('resize', adjustContainerPosition)
    } else {
      console.log('[DTQS] #main-outlet does not exist, using default styles')
    }

    // Get the cached title
    const listTitle = cachedTopicListTitle || 'Topic List'

    // Fill the content container
    contentContainer.innerHTML = `
          <div class="topic-list-viewer-header">
              <h3>${listTitle}</h3>
              <div class="topic-list-viewer-controls">
                  <a href="${cachedTopicListUrl}" class="source-link" title="${t('sourceFrom')}">${t('sourceFrom')}</a>
                  <button id="topic-list-viewer-settings" title="${t('settings')}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                      <circle cx="12" cy="12" r="3"></circle>
                      <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
                    </svg>
                  </button>
                  <button id="topic-list-viewer-close">×</button>
              </div>
          </div>
          ${cacheStatus}
          <div class="topic-list-viewer-content">
              <table class="topic-list">
                  <thead>
                      <tr>
                          <th class="topic-list-data default">${t('topicList')}</th>
                          <th class="topic-list-data posters"></th>
                          <th class="topic-list-data posts num">${t('replies')}</th>
                          <th class="topic-list-data views num">${t('views')}</th>
                          <th class="topic-list-data activity num">${t('activity')}</th>
                      </tr>
                  </thead>
                  ${cachedTopicList}
              </table>
          </div>
      `

    // Add close button event
    contentContainer
      .querySelector('#topic-list-viewer-close')
      .addEventListener('click', hideTopicList)

    // Add settings button event
    contentContainer
      .querySelector('#topic-list-viewer-settings')
      .addEventListener('click', showSettingsDialog)

    // Add SPA routing events to all links in the topic list
    const topicLinks = contentContainer.querySelectorAll('.topic-list-item a')
    topicLinks.forEach((link) => {
      link.addEventListener(
        'click',
        function (event) {
          console.log(`[DTQS] Link clicked ${link.href}`)
          event.preventDefault()
          event.stopPropagation()
          navigateWithSPA(link.href, null)
          return false
        },
        true
      )
    })

    // Add swipe support for mobile devices
    // addSwipeSupport(contentContainer)

    // Initially hidden
    topicListContainer.style.display = 'none'
    topicListContainer.classList.remove('visible')

    // Calculate and print execution time
    const endTime = performance.now()
    console.log(
      `[DTQS] Pre-rendering topic list completed in ${(endTime - startTime).toFixed(2)}ms`
    )
  }

  // Add touch support for mobile devices
  const addTouchSupport = (button, clickHandler) => {
    button.addEventListener('click', clickHandler)
    if (isMobileDevice) {
      button.addEventListener('touchstart', (e) => {
        e.preventDefault()
        button.style.transform = 'scale(0.95)'
        button.style.opacity = '0.8'
      })

      button.addEventListener('touchend', (e) => {
        e.preventDefault()
        button.style.transform = ''
        button.style.opacity = ''
        clickHandler()
      })

      button.addEventListener('touchcancel', (e) => {
        button.style.transform = ''
        button.style.opacity = ''
      })
    }
  }

  /**
   * Add swipe gesture support for mobile devices
   * @param {Element} element - The element to add swipe support to
   */
  function addSwipeSupport(element) {
    if (!isMobileDevice) return

    let startY = 0
    let currentY = 0
    let isDragging = false
    let startTime = 0

    element.addEventListener(
      'touchstart',
      (e) => {
        startY = e.touches[0].clientY
        currentY = startY
        startTime = Date.now()
        isDragging = true
      },
      { passive: true }
    )

    element.addEventListener(
      'touchmove',
      (e) => {
        if (!isDragging) return

        currentY = e.touches[0].clientY
        const deltaY = currentY - startY

        // Only allow downward swipe to close
        if (deltaY > 0 && deltaY < 100) {
          const opacity = Math.max(0.3, 1 - deltaY / 200)
          element.style.opacity = opacity
          element.style.transform = `translateY(${deltaY}px)`
        }
      },
      { passive: true }
    )

    element.addEventListener(
      'touchend',
      (e) => {
        if (!isDragging) return

        const deltaY = currentY - startY
        const deltaTime = Date.now() - startTime
        const velocity = deltaY / deltaTime

        // Close if swipe down is significant or fast
        if (deltaY > 50 || (velocity > 0.3 && deltaY > 20)) {
          hideTopicList()
        } else {
          // Reset position
          element.style.opacity = ''
          element.style.transform = ''
        }

        isDragging = false
      },
      { passive: true }
    )

    element.addEventListener(
      'touchcancel',
      (e) => {
        if (isDragging) {
          element.style.opacity = ''
          element.style.transform = ''
          isDragging = false
        }
      },
      { passive: true }
    )
  }

  /**
   * Show the topic list
   */
  function showTopicList() {
    // Record start time
    const startTime = performance.now()

    // If there is no cached topic list, do not show
    if (!cachedTopicList) {
      alert(t('noCachedList'))
      return
    }

    // If the container does not exist, pre-render it first
    if (!topicListContainer) {
      prerenderTopicList()
    }

    // Hide body and html scrollbars
    document.body.style.overflow = 'hidden'
    document.documentElement.style.overflow = 'hidden'

    // Record the current scroll position for restoration
    window._savedScrollPosition =
      window.scrollY || document.documentElement.scrollTop

    // Show the container and add the visible class immediately
    topicListContainer.style.display = 'flex'
    // Force reflow
    // void topicListContainer.offsetWidth
    topicListContainer.classList.add('visible')
    isListVisible = true

    // Highlight the current topic
    const currentTopicId = getCurrentTopicId()

    // First, remove any existing highlights
    const previousHighlightedRows = topicListContainer.querySelectorAll(
      '.topic-list-item.current-topic'
    )
    previousHighlightedRows.forEach((row) => {
      row.classList.remove('current-topic')
    })

    if (currentTopicId) {
      // Find all topic rows
      const topicRows = topicListContainer.querySelectorAll('.topic-list-item')
      topicRows.forEach((row) => {
        // Get the topic link
        const topicLink = row.querySelector('a.title')
        if (topicLink) {
          // Extract the topic ID from the link
          const match = topicLink.href.match(/\/t\/[^\/]+\/(\d+)/)
          if (match && parseInt(match[1]) === currentTopicId) {
            // Add highlight class
            row.classList.add('current-topic')
            // Scroll to the current topic
            setTimeout(() => {
              row.scrollIntoView({ behavior: 'smooth', block: 'center' })
            }, 300)
          }
        }
      })
    }

    // Calculate and print execution time
    const endTime = performance.now()
    console.log(
      `[DTQS] Showing topic list completed in ${(endTime - startTime).toFixed(2)}ms`
    )
  }

  /**
   * Hide the topic list
   */
  function hideTopicList() {
    if (!topicListContainer) return

    // Restore body and html scrollbars
    document.body.style.overflow = ''
    document.documentElement.style.overflow = ''

    // Restore scroll position
    if (window._savedScrollPosition !== undefined) {
      window.scrollTo(0, window._savedScrollPosition)
      window._savedScrollPosition = undefined
    }

    // Remove the visible class to trigger the fade-out animation
    topicListContainer.classList.remove('visible')

    // Hide after the animation is complete
    setTimeout(() => {
      if (topicListContainer) {
        topicListContainer.style.display = 'none'
      }
      isListVisible = false
    }, 300)
  }

  /**
   * Format time difference
   * @param {number} ms - Milliseconds
   * @returns {string} - Formatted time difference
   */
  function formatTimeAgo(ms) {
    const seconds = Math.floor(ms / 1000)
    const minutes = Math.floor(seconds / 60)
    const hours = Math.floor(minutes / 60)
    const days = Math.floor(hours / 24)

    if (days > 0) return `${days}d`
    if (hours > 0) return `${hours}h`
    if (minutes > 0) return `${minutes}m`
    return `${seconds}s`
  }

  // Add styles
  GM.addStyle(`
        #topic-list-viewer-button {
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            border-radius: 20px;
            background-color: #0078d7;
            color: white;
            border: none;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            cursor: pointer;
            z-index: 99999;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.3s ease;
            padding: 5px 10px;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
            touch-action: manipulation;
        }

        #dtqs-settings-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            z-index: 999999;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        #dtqs-settings-dialog {
            background: white;
            border-radius: 8px;
            padding: 20px;
            width: 400px;
            max-width: 90%;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
        }

        #dtqs-settings-dialog h2 {
            margin-top: 0;
            margin-bottom: 15px;
            font-size: 18px;
            border-bottom: 1px solid #eee;
            padding-bottom: 10px;
        }

        .dtqs-setting-item {
            display: flex;
            align-items: center;
            margin-bottom: 15px;
        }

        .dtqs-setting-item label {
            margin-right: 10px;
            flex-grow: 1;
            display: flex;
            align-items: center;
        }

        .dtqs-setting-item input[type="checkbox"] {
            margin-right: 5px;
            vertical-align: middle;
        }

        .dtqs-setting-item select {
            padding: 5px;
            border-radius: 4px;
            border: 1px solid #ddd;
        }

        .dtqs-setting-item input[type="text"] {
            padding: 5px 8px;
            border-radius: 4px;
            border: 1px solid #ddd;
            font-family: monospace;
            font-size: 12px;
            min-width: 120px;
        }

        .dtqs-setting-section {
            margin-top: 20px;
            padding-top: 15px;
            border-top: 1px solid #eee;
        }

        .dtqs-setting-section h3 {
            margin: 0 0 15px 0;
            font-size: 16px;
            color: #333;
            font-weight: 600;
        }

        .dtqs-buttons {
            display: flex;
            justify-content: flex-end;
            margin-top: 20px;
            gap: 10px;
        }

        .dtqs-buttons button {
            padding: 6px 12px;
            border-radius: 4px;
            border: 1px solid #ccc;
            background: #f5f5f5;
            cursor: pointer;
            font-weight: 500;
            transition: all 0.2s ease;
        }

        .dtqs-buttons button:hover {
            background: #e5e5e5;
        }

        .dtqs-buttons #dtqs-settings-save {
            background: #007bff;
            border-color: #007bff;
            color: white;
            font-weight: 600;
            box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
        }

        .dtqs-buttons #dtqs-settings-save:hover {
            background: #0056b3;
            border-color: #0056b3;
            box-shadow: 0 3px 6px rgba(0, 123, 255, 0.3);
            transform: translateY(-1px);
        }

        .dtqs-buttons #dtqs-settings-save:active {
            background: #004085;
            border-color: #004085;
            transform: translateY(0);
            box-shadow: 0 1px 2px rgba(0, 123, 255, 0.2);
        }

        .topic-list-viewer-dark-mode #dtqs-settings-overlay {
            background: rgba(0, 0, 0, 0.7);
        }

        .topic-list-viewer-dark-mode #dtqs-settings-dialog {
            background: #2d2d2d;
            color: #e0e0e0;
            border: 1px solid #444;
        }

        .topic-list-viewer-dark-mode #dtqs-settings-dialog h2 {
            color: #e0e0e0;
            border-bottom: 1px solid #444;
        }

        .topic-list-viewer-dark-mode .dtqs-setting-item label {
            color: #e0e0e0;
        }

        .topic-list-viewer-dark-mode .dtqs-setting-item select {
            background: #3a3a3a;
            color: #e0e0e0;
            border: 1px solid #555;
        }

        .topic-list-viewer-dark-mode .dtqs-setting-item select:focus {
            border-color: #64b5f6;
            outline: none;
            box-shadow: 0 0 0 2px rgba(100, 181, 246, 0.2);
        }

        .topic-list-viewer-dark-mode .dtqs-setting-item input[type="checkbox"] {
            accent-color: #64b5f6;
        }

        .topic-list-viewer-dark-mode .dtqs-setting-item input[type="text"] {
            background: #3a3a3a;
            color: #e0e0e0;
            border: 1px solid #555;
        }

        .topic-list-viewer-dark-mode .dtqs-setting-item input[type="text"]:focus {
            border-color: #64b5f6;
            outline: none;
            box-shadow: 0 0 0 2px rgba(100, 181, 246, 0.2);
        }

        .topic-list-viewer-dark-mode .dtqs-setting-section {
            border-top: 1px solid #444;
        }

        .topic-list-viewer-dark-mode .dtqs-setting-section h3 {
            color: #e0e0e0;
        }

        .topic-list-viewer-dark-mode .dtqs-buttons button {
            background: #3a3a3a;
            color: #e0e0e0;
            border: 1px solid #555;
        }

        .topic-list-viewer-dark-mode .dtqs-buttons button:hover {
            background: #4a4a4a;
            border-color: #666;
        }

        .topic-list-viewer-dark-mode .dtqs-buttons button:active {
            background: #2a2a2a;
        }

        .topic-list-viewer-dark-mode .dtqs-buttons #dtqs-settings-save {
            background: #1976d2;
            border-color: #1976d2;
            color: white;
            font-weight: 600;
            box-shadow: 0 2px 4px rgba(25, 118, 210, 0.3);
        }

        .topic-list-viewer-dark-mode .dtqs-buttons #dtqs-settings-save:hover {
            background: #1565c0;
            border-color: #1565c0;
            box-shadow: 0 3px 6px rgba(25, 118, 210, 0.4);
            transform: translateY(-1px);
        }

        .topic-list-viewer-dark-mode .dtqs-buttons #dtqs-settings-save:active {
            background: #0d47a1;
            border-color: #0d47a1;
            transform: translateY(0);
            box-shadow: 0 1px 2px rgba(25, 118, 210, 0.3);
        }

        .topic-nav-container {
             display: grid;
             grid-template-columns: 1fr auto 1fr;
             align-items: center;
             position: relative;
             width: 100%;
         }

         .hide-nav-buttons .prev-topic,
         .hide-nav-buttons .next-topic {
             display: none;
         }

         .topic-nav-button {
             display: flex;
             align-items: center;
             padding: 5px 8px;
             cursor: pointer;
             border-radius: 4px;
             transition: all 0.2s ease;
         }

         .topic-nav-button:hover {
             background-color: rgba(255,255,255,0.2);
         }

         .topic-nav-title {
             max-width: 180px;
             white-space: nowrap;
             overflow: hidden;
             text-overflow: ellipsis;
             font-size: 13px;
             margin: 0 6px;
             font-weight: 500;
         }

         .center-button {
             grid-column: 2;
             z-index: 1;
             margin: 0 15px;
         }

         .prev-topic {
             grid-column: 1;
             justify-self: end;
         }

         .next-topic {
             grid-column: 3;
             justify-self: start;
         }

        #topic-list-viewer-button:hover {
             background-color: #0063b1;
             transform: translateX(-50%) scale(1.05);
         }

        #topic-list-viewer-container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 9999;
            display: none;
            flex-direction: column;
            opacity: 0;
            transition: opacity 0.1s ease;
        }

        .topic-list-viewer-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.7);
            z-index: 1;
        }

        .topic-list-viewer-wrapper {
            position: relative;
            z-index: 2;
            background-color: white;
            width: 100%;
            max-width: 1200px;
            height: 100%;
            overflow-y: auto;
            margin: 0 auto;
            display: flex;
            flex-direction: column;
        }

        #topic-list-viewer-container.visible {
            opacity: 1;
        }

        .topic-list-viewer-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 15px 20px;
            background-color: #f8f8f8;
            border-bottom: 1px solid #ddd;
        }

        .topic-list-viewer-header h3 {
            margin: 0;
            font-size: 18px;
            color: #333;
            flex: 1;
            min-width: 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            margin-right: 15px;
        }

        .topic-list-viewer-controls {
            display: flex;
            align-items: center;
            gap: 15px;
            height: 32px;
            flex-shrink: 0;
        }

        .source-link {
            color: #0078d7;
            text-decoration: none;
            font-size: 14px;
            height: 32px;
            display: flex;
            align-items: center;
            transition: all 0.2s ease;
        }

        .source-link:hover {
            text-decoration: underline;
        }

        #topic-list-viewer-settings {
            background: none;
            border: none;
            padding: 8px;
            cursor: pointer;
            color: #666;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 4px;
            transition: all 0.2s ease;
        }

        #topic-list-viewer-settings:hover {
            background-color: #f0f0f0;
            color: #333;
        }

        #topic-list-viewer-close {
            background: #f0f0f0;
            color: #666;
            border: none;
            font-size: 18px;
            font-weight: normal;
            cursor: pointer;
            padding: 8px 12px;
            border-radius: 4px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s ease;
        }

        #topic-list-viewer-close:hover {
            background-color: #e0e0e0;
            color: #333;
        }

        .cache-status {
            padding: 8px 20px;
            background-color: #f0f7ff;
            color: #0063b1;
            font-size: 12px;
            border-bottom: 1px solid #ddd;
        }

        .cache-status.expired {
            background-color: #fff0f0;
            color: #d70000;
        }

        .topic-list-viewer-content {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
            background-color: white;
        }

        .topic-list-viewer-content table {
            width: 100%;
            border-collapse: collapse;
            position: relative;
        }

        .topic-list-viewer-content th {
            text-align: left;
            padding: 10px;
            border-bottom: 2px solid #ddd;
            color: #555;
            font-weight: bold;
        }

        .topic-list-viewer-content tr:hover {
             background-color: #f5f5f5;
         }

         .topic-list-viewer-content tr.current-topic {
             background-color: #e6f7ff;
             border-left: 3px solid #1890ff;
         }

         .topic-list-viewer-content tr.current-topic:hover {
             background-color: #d4edff;
         }

         .topic-list-viewer-content tr.current-topic td:first-child {
             padding-left: 7px;
         }

        .topic-list-viewer-dark-mode #topic-list-viewer-button {
            background-color: #2196f3;
            box-shadow: 0 2px 5px rgba(0,0,0,0.4);
        }

        .topic-list-viewer-dark-mode #topic-list-viewer-button:hover {
            background-color: #1976d2;
        }

        .topic-list-viewer-dark-mode .topic-list-viewer-overlay {
            background-color: rgba(0,0,0,0.85);
        }

        .topic-list-viewer-dark-mode .topic-list-viewer-wrapper {
            background-color: #222;
            color: #e0e0e0;
        }

        .topic-list-viewer-dark-mode .topic-list-viewer-header {
            background-color: #2c2c2c;
            border-bottom: 1px solid #444;
        }

        .topic-list-viewer-dark-mode .topic-list-viewer-header h3 {
            color: #e0e0e0;
        }

        .topic-list-viewer-dark-mode .source-link {
            color: #64b5f6;
        }

        .topic-list-viewer-dark-mode #topic-list-viewer-settings {
            color: #aaa;
        }

        .topic-list-viewer-dark-mode #topic-list-viewer-settings:hover {
            background-color: #444;
            color: #e0e0e0;
        }

        .topic-list-viewer-dark-mode #topic-list-viewer-close {
            background: #444;
            color: #aaa;
        }

        .topic-list-viewer-dark-mode #topic-list-viewer-close:hover {
            background-color: #555;
            color: #ccc;
        }

        .topic-list-viewer-dark-mode .cache-status {
            background-color: #1a3a5a;
            color: #90caf9;
            border-bottom: 1px solid #444;
        }

        .topic-list-viewer-dark-mode .cache-status.expired {
            background-color: #5a1a1a;
            color: #ef9a9a;
        }

        .topic-list-viewer-dark-mode .topic-list-viewer-content {
            background-color: #333;
            color: #e0e0e0;
        }

        .topic-list-viewer-dark-mode .topic-list-viewer-content th {
            border-bottom: 2px solid #555;
            color: #bbb;
        }

        .topic-list-viewer-dark-mode .topic-list-viewer-content tr:hover {
             background-color: #3a3a3a;
         }

         .topic-list-viewer-dark-mode .topic-list-viewer-content tr.current-topic {
             background-color: #1a365d;
             border-left: 3px solid #1890ff;
         }

         .topic-list-viewer-dark-mode .topic-list-viewer-content tr.current-topic:hover {
             background-color: #234979;
         }

        .topic-list-viewer-dark-mode .topic-list-viewer-content a {
            color: #64b5f6;
        }

        .topic-list-viewer-dark-mode .topic-list-viewer-content a:visited {
            color: #b39ddb;
        }

        .dtqs-mobile-device #topic-list-viewer-container {
            -webkit-overflow-scrolling: touch;
        }

        .dtqs-mobile-device .topic-list-viewer-content {
            -webkit-overflow-scrolling: touch;
            overscroll-behavior: contain;
        }

        .dtqs-mobile-device .topic-list-viewer-content table thead {
            display: none;
        }

        .dtqs-mobile-device .topic-list-viewer-wrapper {
            position: relative;
            overflow: hidden;
        }

        .dtqs-mobile-device .topic-list-viewer-content {
            transition: transform 0.3s ease;
        }

        .dtqs-mobile-device #topic-search-input {
            font-size: 16px;
            padding: 12px 15px;
            border-radius: 8px;
        }

        .dtqs-mobile-device #dtqs-settings-dialog {
            width: 90% !important;
            max-width: 400px !important;
            margin: 20px auto !important;
            padding: 20px !important;
            border-radius: 12px !important;
        }

        .dtqs-mobile-device #dtqs-settings-dialog h2 {
            font-size: 18px !important;
            margin-bottom: 20px !important;
        }

        .dtqs-mobile-device .dtqs-setting-item {
            margin-bottom: 20px !important;
        }

        .dtqs-mobile-device .dtqs-setting-item label {
            font-size: 16px !important;
            line-height: 1.4 !important;
        }

        .dtqs-mobile-device .dtqs-setting-item select {
            padding: 12px !important;
            font-size: 16px !important;
            border-radius: 8px !important;
            min-height: 44px !important;
            width: 100% !important;
            box-sizing: border-box !important;
        }

        .dtqs-mobile-device .dtqs-setting-item input[type="checkbox"] {
            width: 20px !important;
            height: 20px !important;
            margin-right: 12px !important;
        }

        .dtqs-mobile-device .dtqs-buttons {
            display: flex !important;
            gap: 12px !important;
            margin-top: 24px !important;
        }

        .dtqs-mobile-device .dtqs-buttons button {
            flex: 1 !important;
            padding: 14px 20px !important;
            font-size: 16px !important;
            border-radius: 8px !important;
            min-height: 44px !important;
            border: none !important;
            cursor: pointer !important;
            transition: all 0.2s ease !important;
            -webkit-tap-highlight-color: transparent !important;
            touch-action: manipulation !important;
        }

        .dtqs-mobile-device .dtqs-buttons button:active {
            transform: scale(0.98) !important;
        }

        @media (max-width: 768px) {
            #topic-list-viewer-button {
                bottom: 12px;
                padding: 4px 6px;
                border-radius: 18px;
                box-shadow: 0 2px 6px rgba(0,0,0,0.2);
                max-width: calc(100vw - 30px);
                overflow: hidden;
            }

            .topic-nav-container {
                gap: 3px;
            }

            .topic-nav-button {
                padding: 3px 5px;
                min-height: 32px;
                min-width: 32px;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .topic-nav-title {
                max-width: 90px;
                font-size: 10px;
                margin: 0 1px;
                line-height: 1.1;
            }

            .center-button {
                margin: 0 3px;
                background-color: rgba(255,255,255,0.2);
                border-radius: 50%;
                padding: 4px;
            }

            .topic-list-viewer-content {
                padding: 10px;
            }

            .topic-list-viewer-header {
                padding: 12px 15px;
                flex-wrap: wrap;
                gap: 10px;
            }

            .topic-list-viewer-header {
                padding: 12px 15px;
                flex-wrap: nowrap;
            }

            .topic-list-viewer-header h3 {
                font-size: 16px;
                margin-right: 10px;
            }

            .topic-list-viewer-controls {
                gap: 8px;
                flex-shrink: 0;
            }

            #topic-list-viewer-close {
                font-size: 28px;
                padding: 8px;
                min-height: 44px;
                min-width: 44px;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            @media (max-width: 480px) {
                #topic-list-viewer-button {
                    bottom: 10px;
                    padding: 3px 5px;
                    border-radius: 16px;
                }

                .topic-list-viewer-header {
                    padding: 10px 12px;
                }

                .topic-list-viewer-header h3 {
                    font-size: 14px;
                    margin-right: 8px;
                }

                .topic-list-viewer-controls {
                    gap: 6px;
                }

                .topic-nav-container {
                    gap: 2px;
                }

                .topic-nav-button {
                    padding: 2px 3px;
                    min-height: 28px;
                    min-width: 28px;
                }

                .topic-nav-title {
                    max-width: 80px;
                    font-size: 9px;
                    margin: 0;
                    line-height: 1.0;
                }

                .center-button {
                    margin: 0 2px;
                    padding: 3px;
                }

                .topic-nav-title {
                    display: none;
                }

                .prev-topic, .next-topic {
                    padding: 8px;
                }
            }
        }

        @media (max-width: 360px) {
            #topic-list-viewer-button {
                bottom: 8px;
                padding: 3px 5px;
                border-radius: 16px;
            }

            .topic-list-viewer-header {
                padding: 8px 10px;
            }

            .topic-list-viewer-header h3 {
                font-size: 13px;
                margin-right: 6px;
            }

            .topic-list-viewer-controls {
                gap: 4px;
            }

            .topic-nav-container {
                gap: 1px;
            }

            .topic-nav-button {
                padding: 2px 3px;
                min-height: 28px;
                min-width: 28px;
            }

            .center-button {
                margin: 0 1px;
                padding: 3px;
            }

            .topic-list-viewer-header {
                padding: 10px 12px;
            }

            .topic-list-viewer-content {
                padding: 8px;
            }
        }
    `)

  // Initialize after the page has loaded
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init)
  } else {
    await init()
  }
})()