AO3 Vanity

Add Twitter search links to AO3 works

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         AO3 Vanity
// @namespace    https://github.com/serpentineegg/ao3-vanity
// @version      0.1.2
// @description  Add Twitter search links to AO3 works
// @author       serpentineegg
// @match        *://archiveofourown.org/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // Create Twitter search URL using twiiit.com
  function createTwitterSearchUrl(workUrl) {
    const cleanUrl = workUrl.replace('https://', '');
    const encodedUrl = encodeURIComponent(cleanUrl);
    return `https://twiiit.com/search?f=tweets&q=${encodedUrl}`;
  }

  // Create a Twitter search link element
  function createTwitterLink(twitterUrl, linkText = 'Twitter') {
    const searchLink = document.createElement('a');
    searchLink.href = twitterUrl;
    searchLink.target = '_blank';
    searchLink.rel = 'noopener noreferrer';
    searchLink.className = 'twitter-links';
    searchLink.textContent = linkText;
    searchLink.title = 'Search Twitter for mentions of this work';
    return searchLink;
  }

  // Add Twitter search info to any work stats section
  function addTwitterSearchToStats(workElement, workUrl) {
    // Check if already exists
    if (workElement.querySelector('.twitter-links')) {
      return;
    }

    const twitterUrl = createTwitterSearchUrl(workUrl);

    // Check if this is a work.meta.group first
    if (workElement.matches('dl.work.meta.group')) {
      // For work.meta.group, insert after the stats section
      const statsSection = workElement.querySelector('dd.stats');
      if (statsSection) {
        const dt = document.createElement('dt');
        dt.className = 'twitter-links';
        dt.textContent = 'External Links:';

        const dd = document.createElement('dd');
        dd.className = 'twitter-links';
        dd.appendChild(createTwitterLink(twitterUrl));

        // Insert after the stats section
        const nextSibling = statsSection.nextSibling;
        if (nextSibling) {
          workElement.insertBefore(dt, nextSibling);
          workElement.insertBefore(dd, nextSibling);
        } else {
          workElement.appendChild(dt);
          workElement.appendChild(dd);
        }
      }
      return;
    }

    // Try different stats container locations for other elements
    let statsContainer = workElement.querySelector('dl.stats');

    // If no dl.stats, try the nested stats in work meta
    if (!statsContainer) {
      statsContainer = workElement.querySelector('dd.stats dl.stats');
    }

    if (!statsContainer) {
      return;
    }

    // Create the dd wrapper element
    const dd = document.createElement('dd');
    dd.className = 'twitter-links';
    dd.appendChild(createTwitterLink(twitterUrl, 'Twitter Links'));

    // For regular stats containers, append the wrapped link
    statsContainer.appendChild(dd);
  }

  // Process all work elements on any page
  function processAllWorks() {
    // Find all work elements with different selectors
    const workSelectors = [
      'li.work.blurb',              // Standard work listings
      'li.reading.work.blurb',      // Reading history
      'li.bookmark.blurb',          // Bookmarks
      'div.work.meta',              // Individual work pages
      'dl.work.meta.group',         // Work metadata sections
      'li.work'                     // Generic work items
    ];

    workSelectors.forEach(selector => {
      const workElements = document.querySelectorAll(selector);

      workElements.forEach(workElement => {
        let workUrl = null;

        // Try to find the work URL in different ways
        const titleLink = workElement.querySelector('h4.heading a[href*="/works/"]');
        if (titleLink) {
          workUrl = titleLink.href;
        } else if (selector === 'div.work.meta' || selector === 'dl.work.meta.group') {
          // For individual work pages
          workUrl = window.location.href;
        } else {
          // Try to find work ID in element attributes
          const workId = workElement.id?.match(/work[_-](\d+)/)?.[1] ||
            workElement.className?.match(/work-(\d+)/)?.[1];
          if (workId) {
            workUrl = `https://archiveofourown.org/works/${workId}`;
          }
        }

        if (workUrl) {
          addTwitterSearchToStats(workElement, workUrl);
        }
      });
    });
  }

  // Main initialization function
  function init() {
    // Process all works on any AO3 page
    processAllWorks();

    // Set up observer for dynamically loaded content
    const observer = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {
        if (mutation.addedNodes.length > 0) {
          // Debounce the processing to avoid excessive calls
          clearTimeout(window.twitterSearchTimeout);
          window.twitterSearchTimeout = setTimeout(processAllWorks, 500);
        }
      });
    });

    // Observe the main content area
    const mainContent = document.querySelector('#main') || document.body;
    if (mainContent) {
      observer.observe(mainContent, {
        childList: true,
        subtree: true
      });
    }
  }

  // Wait for page to load and initialize
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})()