LINUX.DO Load More Topics Manually

Load more topics manually with enhanced UI and error handling.

// ==UserScript==
// @name                 LINUX.DO Load More Topics Manually
// @name:zh-CN           LINUX.DO 手动加载更多话题
// @namespace            https://www.pipecraft.net/
// @homepageURL          https://github.com/utags/userscripts#readme
// @supportURL           https://github.com/utags/userscripts/issues
// @version              0.1.0
// @description          Load more topics manually with enhanced UI and error handling.
// @description:zh-CN    手动加载更多话题,具有增强的用户界面和错误处理。
// @author               Pipecraft
// @license              MIT
// @match                https://linux.do/*
// @icon                 https://www.google.com/s2/favicons?sz=64&domain=linux.do
// @grant                none
// ==/UserScript==

;(function () {
  'use strict'

  // Configuration constants
  const CONFIG = {
    BUTTON_ID: 'userscript-load-more-button',
    SENTINEL_SELECTOR: '.load-more-sentinel',
    LOAD_TIMEOUT: 1000,
    OBSERVER_DELAY: 100,
    DEBUG: false,
  }

  let isLoading = false

  /**
   * Log debug messages if debug mode is enabled
   * @param {string} message - The message to log
   * @param {any} data - Optional data to log
   */
  function debugLog(message, data = null) {
    if (CONFIG.DEBUG) {
      console.log(`[Load More Manually] ${message}`, data || '')
    }
  }

  /**
   * Create and style the load more button
   * @returns {HTMLButtonElement} The created button element
   */
  function createLoadMoreButton() {
    const button = document.createElement('button')
    button.id = CONFIG.BUTTON_ID
    button.textContent = 'Load More'
    button.className = 'userscript-load-more-btn'

    // Apply modern button styles
    Object.assign(button.style, {
      width: '100%',
      margin: '16px 0',
      padding: '12px 24px',
      border: 'none',
      borderRadius: '8px',
      backgroundColor: '#007bff',
      color: '#ffffff',
      fontSize: '14px',
      fontWeight: '500',
      cursor: 'pointer',
      transition: 'all 0.2s ease',
      boxShadow: '0 2px 4px rgba(0, 123, 255, 0.2)',
    })

    // Add hover and active states
    button.addEventListener('mouseenter', () => {
      if (!isLoading) {
        button.style.backgroundColor = '#0056b3'
        button.style.transform = 'translateY(-1px)'
        button.style.boxShadow = '0 4px 8px rgba(0, 123, 255, 0.3)'
      }
    })

    button.addEventListener('mouseleave', () => {
      if (!isLoading) {
        button.style.backgroundColor = '#007bff'
        button.style.transform = 'translateY(0)'
        button.style.boxShadow = '0 2px 4px rgba(0, 123, 255, 0.2)'
      }
    })

    return button
  }

  /**
   * Update button state during loading
   * @param {HTMLButtonElement} button - The button element
   * @param {boolean} loading - Whether the button is in loading state
   */
  function updateButtonState(button, loading) {
    isLoading = loading

    if (loading) {
      button.textContent = 'Loading...'
      button.style.backgroundColor = '#6c757d'
      button.style.cursor = 'not-allowed'
      button.style.transform = 'translateY(0)'
      button.disabled = true
    } else {
      button.textContent = 'Load More'
      button.style.backgroundColor = '#007bff'
      button.style.cursor = 'pointer'
      button.disabled = false
    }
  }

  /**
   * Handle the load more button click event
   * @param {HTMLElement} sentinel - The load more sentinel element
   * @param {HTMLButtonElement} button - The button element
   */
  function handleLoadMore(sentinel, button) {
    if (isLoading) {
      debugLog('Already loading, ignoring click')
      return
    }

    debugLog('Load more button clicked')
    updateButtonState(button, true)

    try {
      // Show the sentinel to trigger loading
      sentinel.style.display = 'block'

      // Hide the sentinel after timeout and reset button state
      setTimeout(() => {
        sentinel.style.display = 'none'
        updateButtonState(button, false)
        debugLog('Load more completed')
      }, CONFIG.LOAD_TIMEOUT)
    } catch (error) {
      debugLog('Error during load more:', error)
      updateButtonState(button, false)
    }
  }

  /**
   * Insert the button in the appropriate position relative to the sentinel
   * @param {HTMLElement} sentinel - The load more sentinel element
   * @param {HTMLButtonElement} button - The button element to insert
   */
  function insertButton(sentinel, button) {
    try {
      const parent = sentinel.parentNode
      if (!parent) {
        debugLog('Sentinel has no parent node')
        return false
      }

      if (sentinel.nextSibling) {
        parent.insertBefore(button, sentinel.nextSibling)
      } else {
        parent.appendChild(button)
      }

      debugLog('Button inserted successfully')
      return true
    } catch (error) {
      debugLog('Error inserting button:', error)
      return false
    }
  }

  /**
   * Initialize the load more functionality
   */
  function initLoadMore() {
    try {
      const sentinel = document.querySelector(CONFIG.SENTINEL_SELECTOR)
      if (!sentinel) {
        debugLog('Load more sentinel not found')
        return
      }

      // Hide the sentinel by default
      sentinel.style.display = 'none'

      // Check if button already exists
      const existingButton = document.querySelector(`#${CONFIG.BUTTON_ID}`)
      if (existingButton) {
        debugLog('Button already exists')
        return
      }

      // Create and configure the button
      const button = createLoadMoreButton()
      button.addEventListener('click', () => handleLoadMore(sentinel, button))

      // Insert the button
      if (insertButton(sentinel, button)) {
        debugLog('Load more functionality initialized')
      }
    } catch (error) {
      debugLog('Error initializing load more:', error)
    }
  }

  /**
   * Check if the mutation should trigger an update
   * @param {NodeList} addedNodes - The added nodes from mutation
   * @returns {boolean} Whether an update should be triggered
   */
  function shouldUpdateOnMutation(addedNodes) {
    if (!addedNodes || addedNodes.length === 0) {
      return false
    }

    // Check if any added node contains the sentinel or is the sentinel itself
    for (const node of addedNodes) {
      if (node.nodeType === Node.ELEMENT_NODE) {
        if (node.matches && node.matches(CONFIG.SENTINEL_SELECTOR)) {
          return true
        }
        if (
          node.querySelector &&
          node.querySelector(CONFIG.SENTINEL_SELECTOR)
        ) {
          return true
        }
      }
    }

    return false
  }

  // Initialize mutation observer to watch for DOM changes
  const observer = new MutationObserver((mutationsList) => {
    let shouldUpdate = false

    for (const mutation of mutationsList) {
      if (shouldUpdateOnMutation(mutation.addedNodes)) {
        shouldUpdate = true
        break
      }
    }

    if (shouldUpdate) {
      debugLog('DOM mutation detected, reinitializing')
      setTimeout(initLoadMore, CONFIG.OBSERVER_DELAY)
    }
  })

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

  // Initialize on page load
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initLoadMore)
  } else {
    initLoadMore()
  }

  debugLog('Script initialized')
})()