⚡ X Pro Manager

Discovery src
1023 lines by @nichxbt

src/xProManager.js

How to Use

  1. Navigate to x.com and log in
  2. Open DevTools Console (F12 or Cmd+Option+I)
  3. Paste the script below and press Enter

Configuration Options

OptionDefaultDescription
delayBetweenActions2000ms between UI interactions
scrollDelay1500ms between scroll steps
maxRetries3retries for DOM element lookups

Default Configuration

const CONFIG = {
    delayBetweenActions: 2000,   // ms between UI interactions
    scrollDelay: 1500,           // ms between scroll steps
    maxRetries: 3,               // retries for DOM element lookups
    storagePrefix: 'xactions_xpro_mgr',
  };

Full Script

Copy and paste this entire script into your browser DevTools console on x.com.

// src/xProManager.js
// X Pro (TweetDeck) automation for X/Twitter
// by nichxbt
// 1. Go to pro.x.com
// 2. Open Developer Console (F12)
// 3. Paste and run
// Last Updated: 30 March 2026

(() => {
  'use strict';

  const CONFIG = {
    delayBetweenActions: 2000,   // ms between UI interactions
    scrollDelay: 1500,           // ms between scroll steps
    maxRetries: 3,               // retries for DOM element lookups
    storagePrefix: 'xactions_xpro_mgr',
  };

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  /* ─── Selectors ─────────────────────────────────────────── */

  const SEL = {
    // X Pro layout
    column: '[data-testid="column"]',
    columnHeader: '[data-testid="columnHeader"]',
    columnSettings: '[data-testid="columnSettings"]',
    columnContent: '[data-testid="columnContent"]',
    addColumn: '[data-testid="addColumn"]',
    removeColumn: '[data-testid="removeColumn"]',
    deckContainer: '[data-testid="deck"], .deck-container, #deck',
    // General
    searchInput: '[data-testid="SearchBox_Search_Input"]',
    tweet: 'article[data-testid="tweet"]',
    tweetText: '[data-testid="tweetText"]',
    confirmDialog: '[data-testid="confirmationSheetConfirm"]',
    toast: '[data-testid="toast"]',
    userCell: '[data-testid="UserCell"]',
    // Menus & dialogs
    menuItem: '[role="menuitem"]',
    option: '[role="option"]',
    dialog: '[role="dialog"]',
    closeDialog: '[data-testid="app-bar-close"]',
    backButton: '[data-testid="app-bar-back"]',
  };

  /* ─── State ─────────────────────────────────────────────── */

  let aborted = false;

  const stats = {
    columnsCreated: 0,
    columnsRemoved: 0,
    filtersApplied: 0,
    decksSaved: 0,
    collectionsCreated: 0,
    startTime: Date.now(),
  };

  /* ─── Helpers ───────────────────────────────────────────── */

  const isPro = () =>
    window.location.hostname.includes('pro.x.com') ||
    window.location.pathname.includes('/i/tweetdeck');

  const ensurePro = () => {
    if (!isPro()) {
      console.log('❌ This script must be run on pro.x.com');
      console.log('💡 Navigate to https://pro.x.com first, then paste this script.');
      return false;
    }
    return true;
  };

  const waitForElement = async (selector, timeout = 5000) => {
    const start = Date.now();
    while (Date.now() - start < timeout) {
      const el = document.querySelector(selector);
      if (el) return el;
      await sleep(250);
    }
    return null;
  };

  const waitForElements = async (selector, timeout = 5000) => {
    const start = Date.now();
    while (Date.now() - start < timeout) {
      const els = document.querySelectorAll(selector);
      if (els.length > 0) return els;
      await sleep(250);
    }
    return document.querySelectorAll(selector);
  };

  const clickElement = async (el) => {
    if (!el) return false;
    el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    await sleep(300);
    el.click();
    await sleep(CONFIG.delayBetweenActions);
    return true;
  };

  const typeIntoInput = async (input, text) => {
    if (!input) return false;
    input.focus();
    await sleep(300);
    input.value = '';
    document.execCommand('insertText', false, text);
    input.dispatchEvent(new Event('input', { bubbles: true }));
    await sleep(500);
    return true;
  };

  const getStorageKey = (key) => `${CONFIG.storagePrefix}_${key}`;

  const saveToStorage = (key, data) => {
    try {
      sessionStorage.setItem(getStorageKey(key), JSON.stringify(data));
      return true;
    } catch {
      console.log('⚠️ Failed to save to sessionStorage');
      return false;
    }
  };

  const loadFromStorage = (key, fallback = null) => {
    try {
      const raw = sessionStorage.getItem(getStorageKey(key));
      return raw ? JSON.parse(raw) : fallback;
    } catch {
      return fallback;
    }
  };

  /* ─── 1. Multi-Column View Setup ────────────────────────── */

  const getColumns = () => {
    const columns = document.querySelectorAll(
      `${SEL.column}, [class*="column"], section[aria-label]`
    );
    const result = [];
    columns.forEach((col, i) => {
      const header = col.querySelector(`${SEL.columnHeader}, h2, [role="heading"]`);
      const name = header?.textContent?.trim()
        || col.getAttribute('aria-label')
        || `Column ${i + 1}`;
      const tweets = col.querySelectorAll(SEL.tweet);
      result.push({
        index: i,
        name,
        tweetCount: tweets.length,
        element: col,
      });
    });
    return result;
  };

  const scanColumns = () => {
    if (!ensurePro()) return [];

    console.log('🔍 Scanning columns...');
    const columns = getColumns();

    if (columns.length === 0) {
      console.log('ℹ️ No columns detected. The deck may still be loading.');
      return [];
    }

    console.log(`\n📊 Found ${columns.length} column(s):`);
    console.log('─'.repeat(50));
    columns.forEach((col) => {
      console.log(`   ${col.index + 1}. 📌 ${col.name} — ${col.tweetCount} posts visible`);
    });
    console.log('─'.repeat(50));

    saveToStorage('columns', columns.map(({ element, ...rest }) => rest));
    return columns;
  };

  const reorderColumns = async (fromIndex, toIndex) => {
    if (!ensurePro()) return false;

    console.log(`🔄 Moving column ${fromIndex + 1} to position ${toIndex + 1}...`);

    const columns = getColumns();
    if (fromIndex < 0 || fromIndex >= columns.length || toIndex < 0 || toIndex >= columns.length) {
      console.log(`❌ Invalid column indices. You have ${columns.length} columns (0-${columns.length - 1}).`);
      return false;
    }

    const col = columns[fromIndex].element;
    const header = col.querySelector(`${SEL.columnHeader}, h2, [role="heading"]`);
    if (!header) {
      console.log('❌ Could not locate column header for drag.');
      return false;
    }

    // Simulate drag via dataTransfer events
    const rect = header.getBoundingClientRect();
    const startX = rect.x + rect.width / 2;
    const startY = rect.y + rect.height / 2;

    const targetCol = columns[toIndex].element;
    const targetRect = targetCol.getBoundingClientRect();
    const endX = targetRect.x + targetRect.width / 2;
    const endY = targetRect.y + targetRect.height / 2;

    header.dispatchEvent(new MouseEvent('mousedown', { clientX: startX, clientY: startY, bubbles: true }));
    await sleep(200);
    header.dispatchEvent(new MouseEvent('mousemove', { clientX: endX, clientY: endY, bubbles: true }));
    await sleep(200);
    header.dispatchEvent(new MouseEvent('mouseup', { clientX: endX, clientY: endY, bubbles: true }));
    await sleep(CONFIG.delayBetweenActions);

    console.log(`✅ Column move attempted: "${columns[fromIndex].name}" -> position ${toIndex + 1}`);
    console.log('💡 If column did not move, drag it manually — X Pro may require native drag events.');
    return true;
  };

  const scrollColumnToTop = async (columnIndex) => {
    const columns = getColumns();
    if (columnIndex < 0 || columnIndex >= columns.length) {
      console.log(`❌ Invalid column index: ${columnIndex}. You have ${columns.length} columns.`);
      return false;
    }

    const col = columns[columnIndex].element;
    const scrollable = col.querySelector(`${SEL.columnContent}, [style*="overflow"]`)
      || col;
    scrollable.scrollTop = 0;
    console.log(`⬆️ Scrolled column ${columnIndex + 1} ("${columns[columnIndex].name}") to top.`);
    return true;
  };

  /* ─── 2. Custom Column Creation ─────────────────────────── */

  const COLUMN_PRESETS = {
    home:          { label: 'Home',          icon: '🏠' },
    notifications: { label: 'Notifications', icon: '🔔' },
    mentions:      { label: 'Mentions',      icon: '💬' },
    messages:      { label: 'Messages',      icon: '✉️' },
    search:        { label: 'Search',        icon: '🔍' },
    list:          { label: 'List',          icon: '📋' },
    likes:         { label: 'Likes',         icon: '❤️' },
    trending:      { label: 'Trending',      icon: '📈' },
    followers:     { label: 'Followers',     icon: '👥' },
    collection:    { label: 'Collection',    icon: '📂' },
  };

  const openAddColumnMenu = async () => {
    const addBtn = document.querySelector(SEL.addColumn)
      || document.querySelector('button[aria-label*="Add column"]')
      || document.querySelector('button[aria-label*="add column"]');

    if (!addBtn) {
      console.log('❌ Add column button not found. Make sure you are on pro.x.com.');
      return false;
    }

    await clickElement(addBtn);
    return true;
  };

  const selectMenuOption = async (keyword) => {
    const items = document.querySelectorAll(`${SEL.menuItem}, ${SEL.option}, button`);
    const match = [...items].find(el =>
      el.textContent.toLowerCase().includes(keyword.toLowerCase())
    );
    if (match) {
      await clickElement(match);
      return true;
    }
    return false;
  };

  const addColumn = async (type) => {
    if (!ensurePro()) return false;

    const preset = COLUMN_PRESETS[type];
    if (!preset) {
      console.log(`❌ Unknown column type: "${type}"`);
      console.log(`💡 Available types: ${Object.keys(COLUMN_PRESETS).join(', ')}`);
      return false;
    }

    console.log(`${preset.icon} Adding "${preset.label}" column...`);

    if (!(await openAddColumnMenu())) return false;

    const selected = await selectMenuOption(preset.label);
    if (!selected) {
      console.log(`⚠️ Could not find "${preset.label}" in the add-column menu.`);
      console.log('💡 The option may be named differently. Try adding it manually.');
      return false;
    }

    await sleep(CONFIG.delayBetweenActions);
    stats.columnsCreated++;
    console.log(`✅ "${preset.label}" column added.`);
    return true;
  };

  const addSearchColumn = async (query) => {
    if (!ensurePro()) return false;

    console.log(`🔍 Adding search column for: "${query}"...`);

    if (!(await openAddColumnMenu())) return false;

    const selected = await selectMenuOption('search');
    if (!selected) {
      console.log('⚠️ Search option not found in menu.');
      return false;
    }

    await sleep(CONFIG.delayBetweenActions);

    const searchInput = await waitForElement(SEL.searchInput)
      || await waitForElement('input[type="text"]');

    if (!searchInput) {
      console.log('❌ Search input not found.');
      return false;
    }

    await typeIntoInput(searchInput, query);
    await sleep(1000);

    // Submit the search
    const form = searchInput.closest('form');
    const submitBtn = form?.querySelector('button[type="submit"]');
    if (submitBtn) {
      submitBtn.click();
    } else {
      searchInput.dispatchEvent(new KeyboardEvent('keydown', {
        key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true,
      }));
    }

    await sleep(CONFIG.delayBetweenActions);
    stats.columnsCreated++;
    console.log(`✅ Search column created for: "${query}"`);
    return true;
  };

  const addListColumn = async (listName) => {
    if (!ensurePro()) return false;

    console.log(`📋 Adding list column for: "${listName}"...`);

    if (!(await openAddColumnMenu())) return false;

    const selected = await selectMenuOption('list');
    if (!selected) {
      console.log('⚠️ List option not found in menu.');
      return false;
    }

    await sleep(CONFIG.delayBetweenActions);

    // Search for the list within the dialog
    const searchInput = await waitForElement('input[type="text"]');
    if (searchInput) {
      await typeIntoInput(searchInput, listName);
      await sleep(CONFIG.delayBetweenActions);

      // Select matching list
      const listItems = document.querySelectorAll(`${SEL.menuItem}, li, [role="option"]`);
      const match = [...listItems].find(el =>
        el.textContent.toLowerCase().includes(listName.toLowerCase())
      );
      if (match) {
        await clickElement(match);
        stats.columnsCreated++;
        console.log(`✅ List column created for: "${listName}"`);
        return true;
      }
    }

    console.log(`⚠️ Could not find list "${listName}". Make sure the list exists.`);
    return false;
  };

  const removeColumn = async (columnIndex) => {
    if (!ensurePro()) return false;

    const columns = getColumns();
    if (columnIndex < 0 || columnIndex >= columns.length) {
      console.log(`❌ Invalid column index: ${columnIndex}. You have ${columns.length} columns.`);
      return false;
    }

    const col = columns[columnIndex];
    console.log(`🗑️ Removing column ${columnIndex + 1}: "${col.name}"...`);

    // Open column settings
    const settingsBtn = col.element.querySelector(SEL.columnSettings)
      || col.element.querySelector('button[aria-label*="settings"]')
      || col.element.querySelector('button[aria-label*="Settings"]');

    if (settingsBtn) {
      await clickElement(settingsBtn);
    }

    const removeBtn = await waitForElement(SEL.removeColumn)
      || await waitForElement('button[aria-label*="Remove"]');

    if (removeBtn) {
      await clickElement(removeBtn);

      // Handle confirmation
      const confirm = await waitForElement(SEL.confirmDialog, 2000);
      if (confirm) {
        await clickElement(confirm);
      }

      stats.columnsRemoved++;
      console.log(`✅ Column "${col.name}" removed.`);
      return true;
    }

    console.log('⚠️ Remove button not found. Try removing the column manually via its settings.');
    return false;
  };

  /* ─── 3. Column Filtering ───────────────────────────────── */

  const openColumnSettings = async (columnIndex) => {
    const columns = getColumns();
    if (columnIndex < 0 || columnIndex >= columns.length) {
      console.log(`❌ Invalid column index: ${columnIndex}.`);
      return null;
    }

    const col = columns[columnIndex];
    const settingsBtn = col.element.querySelector(SEL.columnSettings)
      || col.element.querySelector('button[aria-label*="settings"]')
      || col.element.querySelector('button[aria-label*="Settings"]')
      || col.element.querySelector('button[aria-label*="filter"]');

    if (!settingsBtn) {
      console.log(`⚠️ Settings button not found for column "${col.name}".`);
      return null;
    }

    await clickElement(settingsBtn);
    return col;
  };

  const addKeywordFilter = async (columnIndex, keyword, exclude = false) => {
    if (!ensurePro()) return false;

    const col = await openColumnSettings(columnIndex);
    if (!col) return false;

    console.log(`🔧 Adding ${exclude ? 'exclusion' : 'inclusion'} filter "${keyword}" to column "${col.name}"...`);

    // Look for filter input inside settings panel
    const filterInput = await waitForElement('input[placeholder*="filter"]')
      || await waitForElement('input[placeholder*="Filter"]')
      || await waitForElement('input[aria-label*="filter"]');

    if (filterInput) {
      const filterText = exclude ? `-${keyword}` : keyword;
      await typeIntoInput(filterInput, filterText);
      await sleep(500);

      // Confirm / apply filter
      const applyBtn = document.querySelector('button[aria-label*="Apply"]')
        || document.querySelector('button[aria-label*="Save"]')
        || [...document.querySelectorAll('button')].find(b =>
          b.textContent.toLowerCase().includes('apply') ||
          b.textContent.toLowerCase().includes('save') ||
          b.textContent.toLowerCase().includes('add')
        );

      if (applyBtn) {
        await clickElement(applyBtn);
        stats.filtersApplied++;
        console.log(`✅ Filter "${filterText}" applied to column "${col.name}".`);
        return true;
      }

      // Try pressing Enter as fallback
      filterInput.dispatchEvent(new KeyboardEvent('keydown', {
        key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true,
      }));
      await sleep(CONFIG.delayBetweenActions);
      stats.filtersApplied++;
      console.log(`✅ Filter "${filterText}" applied to column "${col.name}".`);
      return true;
    }

    console.log('⚠️ Filter input not found. Column filtering may not be available for this column type.');

    // Close settings dialog
    const closeBtn = document.querySelector(SEL.closeDialog) || document.querySelector(SEL.backButton);
    if (closeBtn) await clickElement(closeBtn);

    return false;
  };

  const addUserFilter = async (columnIndex, username, exclude = false) => {
    if (!ensurePro()) return false;

    const cleanUsername = username.replace(/^@/, '');
    const filterKeyword = exclude ? `-from:${cleanUsername}` : `from:${cleanUsername}`;

    console.log(`👤 Adding user filter: ${exclude ? 'excluding' : 'including'} @${cleanUsername}...`);
    return addKeywordFilter(columnIndex, filterKeyword, false);
  };

  const listColumnFilters = async (columnIndex) => {
    if (!ensurePro()) return [];

    const col = await openColumnSettings(columnIndex);
    if (!col) return [];

    console.log(`📋 Filters for column "${col.name}":`);

    // Gather any visible filter tags or list items
    const filterEls = document.querySelectorAll(
      '[class*="filter"], [data-testid*="filter"], [aria-label*="filter"]'
    );

    const filters = [];
    filterEls.forEach(el => {
      const text = el.textContent?.trim();
      if (text) {
        filters.push(text);
        console.log(`   🏷️ ${text}`);
      }
    });

    if (filters.length === 0) {
      console.log('   ℹ️ No filters found for this column.');
    }

    // Close dialog
    const closeBtn = document.querySelector(SEL.closeDialog) || document.querySelector(SEL.backButton);
    if (closeBtn) await clickElement(closeBtn);

    return filters;
  };

  /* ─── 4. Deck Management ────────────────────────────────── */

  const saveDeck = (name) => {
    if (!ensurePro()) return false;

    const columns = getColumns();
    if (columns.length === 0) {
      console.log('⚠️ No columns to save.');
      return false;
    }

    const deckName = name || `Deck ${new Date().toLocaleDateString()}`;
    console.log(`💾 Saving deck configuration: "${deckName}"...`);

    const deckData = {
      name: deckName,
      savedAt: new Date().toISOString(),
      columns: columns.map(({ element, ...rest }) => rest),
    };

    // Load existing saved decks
    const decks = loadFromStorage('decks', []);
    const existingIdx = decks.findIndex(d => d.name === deckName);
    if (existingIdx >= 0) {
      decks[existingIdx] = deckData;
      console.log(`🔄 Overwrote existing deck: "${deckName}"`);
    } else {
      decks.push(deckData);
    }

    saveToStorage('decks', decks);
    stats.decksSaved++;
    console.log(`✅ Deck "${deckName}" saved with ${deckData.columns.length} column(s).`);
    return true;
  };

  const listDecks = () => {
    const decks = loadFromStorage('decks', []);

    if (decks.length === 0) {
      console.log('ℹ️ No saved decks found.');
      console.log('💡 Use saveDeck("name") to save your current layout.');
      return [];
    }

    console.log(`\n📚 Saved Decks (${decks.length}):`);
    console.log('─'.repeat(50));
    decks.forEach((deck, i) => {
      const date = new Date(deck.savedAt).toLocaleString();
      console.log(`   ${i + 1}. 📋 ${deck.name} — ${deck.columns.length} columns — saved ${date}`);
    });
    console.log('─'.repeat(50));
    return decks;
  };

  const loadDeck = async (nameOrIndex) => {
    if (!ensurePro()) return false;

    const decks = loadFromStorage('decks', []);
    let deck;

    if (typeof nameOrIndex === 'number') {
      deck = decks[nameOrIndex];
    } else {
      deck = decks.find(d => d.name === nameOrIndex);
    }

    if (!deck) {
      console.log(`❌ Deck not found: "${nameOrIndex}"`);
      listDecks();
      return false;
    }

    console.log(`📂 Loading deck: "${deck.name}" (${deck.columns.length} columns)...`);
    console.log('⚠️ Note: X Pro does not support programmatic deck restoration.');
    console.log('   Columns from this deck:');
    deck.columns.forEach((col, i) => {
      console.log(`   ${i + 1}. ${col.name}`);
    });
    console.log('\n💡 Manually recreate these columns, or use addColumn()/addSearchColumn() for each.');
    return deck;
  };

  const deleteDeck = (nameOrIndex) => {
    const decks = loadFromStorage('decks', []);
    let removed;

    if (typeof nameOrIndex === 'number') {
      if (nameOrIndex < 0 || nameOrIndex >= decks.length) {
        console.log(`❌ Invalid deck index: ${nameOrIndex}`);
        return false;
      }
      removed = decks.splice(nameOrIndex, 1)[0];
    } else {
      const idx = decks.findIndex(d => d.name === nameOrIndex);
      if (idx < 0) {
        console.log(`❌ Deck not found: "${nameOrIndex}"`);
        return false;
      }
      removed = decks.splice(idx, 1)[0];
    }

    saveToStorage('decks', decks);
    console.log(`🗑️ Deleted deck: "${removed.name}"`);
    return true;
  };

  const exportDeck = (nameOrIndex) => {
    const decks = loadFromStorage('decks', []);
    let deck;

    if (nameOrIndex === undefined) {
      // Export current layout
      const columns = getColumns();
      deck = {
        name: 'Current Layout',
        exportedAt: new Date().toISOString(),
        columns: columns.map(({ element, ...rest }) => rest),
      };
    } else if (typeof nameOrIndex === 'number') {
      deck = decks[nameOrIndex];
    } else {
      deck = decks.find(d => d.name === nameOrIndex);
    }

    if (!deck) {
      console.log(`❌ Deck not found: "${nameOrIndex}"`);
      return false;
    }

    const json = JSON.stringify(deck, null, 2);
    const blob = new Blob([json], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `xactions-deck-${deck.name.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().slice(0, 10)}.json`;
    a.click();
    URL.revokeObjectURL(url);

    console.log(`📥 Deck "${deck.name}" exported as JSON.`);
    return true;
  };

  const importDeck = (jsonString) => {
    try {
      const deck = JSON.parse(jsonString);
      if (!deck.name || !Array.isArray(deck.columns)) {
        console.log('❌ Invalid deck format. Expected { name, columns: [...] }');
        return false;
      }

      const decks = loadFromStorage('decks', []);
      deck.importedAt = new Date().toISOString();
      decks.push(deck);
      saveToStorage('decks', decks);

      console.log(`✅ Deck "${deck.name}" imported with ${deck.columns.length} column(s).`);
      console.log('💡 Use loadDeck() to view column instructions.');
      return true;
    } catch {
      console.log('❌ Failed to parse deck JSON. Check the format.');
      return false;
    }
  };

  /* ─── 5. Collections Management ─────────────────────────── */

  const getCollections = () => loadFromStorage('collections', []);

  const createCollection = (name, description = '') => {
    console.log(`📂 Creating collection: "${name}"...`);

    const collections = getCollections();
    if (collections.find(c => c.name === name)) {
      console.log(`⚠️ Collection "${name}" already exists.`);
      return false;
    }

    collections.push({
      name,
      description,
      createdAt: new Date().toISOString(),
      tweets: [],
    });

    saveToStorage('collections', collections);
    stats.collectionsCreated++;
    console.log(`✅ Collection "${name}" created.`);
    return true;
  };

  const listCollections = () => {
    const collections = getCollections();

    if (collections.length === 0) {
      console.log('ℹ️ No collections found.');
      console.log('💡 Use createCollection("name") to create one.');
      return [];
    }

    console.log(`\n📂 Collections (${collections.length}):`);
    console.log('─'.repeat(50));
    collections.forEach((col, i) => {
      console.log(`   ${i + 1}. 📌 ${col.name} — ${col.tweets.length} tweets${col.description ? ` — ${col.description}` : ''}`);
    });
    console.log('─'.repeat(50));
    return collections;
  };

  const addTweetToCollection = async (collectionName) => {
    if (!ensurePro()) return false;

    const collections = getCollections();
    const collection = collections.find(c => c.name === collectionName);
    if (!collection) {
      console.log(`❌ Collection "${collectionName}" not found. Create it first.`);
      return false;
    }

    console.log(`🔍 Scanning visible tweets to add to "${collectionName}"...`);

    const tweets = document.querySelectorAll(SEL.tweet);
    if (tweets.length === 0) {
      console.log('⚠️ No tweets visible on screen.');
      return false;
    }

    let added = 0;
    for (const tweet of tweets) {
      if (aborted) break;

      const textEl = tweet.querySelector(SEL.tweetText);
      const text = textEl?.textContent?.trim() || '';
      if (!text) continue;

      const authorEl = tweet.querySelector('[data-testid="User-Name"] a');
      const authorLink = authorEl?.getAttribute('href') || '';
      const author = authorLink.replace(/^\//, '').split('/')[0] || 'unknown';

      const timeEl = tweet.querySelector('time');
      const timestamp = timeEl?.getAttribute('datetime') || '';

      const tweetLink = tweet.querySelector('a[href*="/status/"]');
      const tweetUrl = tweetLink ? `https://x.com${tweetLink.getAttribute('href')}` : '';

      // Deduplicate by URL
      if (tweetUrl && collection.tweets.find(t => t.url === tweetUrl)) continue;

      collection.tweets.push({
        text: text.substring(0, 280),
        author,
        timestamp,
        url: tweetUrl,
        addedAt: new Date().toISOString(),
      });
      added++;
    }

    saveToStorage('collections', collections);
    console.log(`✅ Added ${added} tweet(s) to "${collectionName}" (total: ${collection.tweets.length}).`);
    return true;
  };

  const addTweetByUrl = (collectionName, tweetUrl, note = '') => {
    const collections = getCollections();
    const collection = collections.find(c => c.name === collectionName);
    if (!collection) {
      console.log(`❌ Collection "${collectionName}" not found.`);
      return false;
    }

    if (collection.tweets.find(t => t.url === tweetUrl)) {
      console.log('⚠️ Tweet already in this collection.');
      return false;
    }

    collection.tweets.push({
      text: note || '',
      author: '',
      timestamp: '',
      url: tweetUrl,
      addedAt: new Date().toISOString(),
    });

    saveToStorage('collections', collections);
    console.log(`✅ Tweet added to "${collectionName}".`);
    return true;
  };

  const removeTweetFromCollection = (collectionName, tweetIndex) => {
    const collections = getCollections();
    const collection = collections.find(c => c.name === collectionName);
    if (!collection) {
      console.log(`❌ Collection "${collectionName}" not found.`);
      return false;
    }

    if (tweetIndex < 0 || tweetIndex >= collection.tweets.length) {
      console.log(`❌ Invalid tweet index: ${tweetIndex}`);
      return false;
    }

    const removed = collection.tweets.splice(tweetIndex, 1)[0];
    saveToStorage('collections', collections);
    console.log(`🗑️ Removed tweet from "${collectionName}": "${removed.text.substring(0, 50)}..."`);
    return true;
  };

  const viewCollection = (collectionName) => {
    const collections = getCollections();
    const collection = collections.find(c => c.name === collectionName);
    if (!collection) {
      console.log(`❌ Collection "${collectionName}" not found.`);
      return null;
    }

    console.log(`\n📂 Collection: ${collection.name}`);
    if (collection.description) console.log(`   📝 ${collection.description}`);
    console.log(`   📅 Created: ${new Date(collection.createdAt).toLocaleString()}`);
    console.log(`   📊 Tweets: ${collection.tweets.length}`);
    console.log('─'.repeat(50));

    collection.tweets.forEach((tweet, i) => {
      const preview = tweet.text.substring(0, 80) || tweet.url || '(no text)';
      const author = tweet.author ? `@${tweet.author}` : '';
      console.log(`   ${i + 1}. ${author} ${preview}${tweet.text.length > 80 ? '...' : ''}`);
    });
    console.log('─'.repeat(50));

    return collection;
  };

  const deleteCollection = (collectionName) => {
    const collections = getCollections();
    const idx = collections.findIndex(c => c.name === collectionName);
    if (idx < 0) {
      console.log(`❌ Collection "${collectionName}" not found.`);
      return false;
    }

    const removed = collections.splice(idx, 1)[0];
    saveToStorage('collections', collections);
    console.log(`🗑️ Deleted collection "${removed.name}" (${removed.tweets.length} tweets).`);
    return true;
  };

  const exportCollection = (collectionName) => {
    const collections = getCollections();
    const collection = collections.find(c => c.name === collectionName);
    if (!collection) {
      console.log(`❌ Collection "${collectionName}" not found.`);
      return false;
    }

    const json = JSON.stringify(collection, null, 2);
    const blob = new Blob([json], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `xactions-collection-${collection.name.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().slice(0, 10)}.json`;
    a.click();
    URL.revokeObjectURL(url);

    console.log(`📥 Collection "${collection.name}" exported (${collection.tweets.length} tweets).`);
    return true;
  };

  /* ─── Public API ────────────────────────────────────────── */

  window.XActions = window.XActions || {};
  window.XActions.xProManager = {
    // Controls
    abort() { aborted = true; console.log('🛑 Aborting...'); },
    status() {
      const elapsed = ((Date.now() - stats.startTime) / 1000).toFixed(0);
      console.log(`📊 Columns created: ${stats.columnsCreated} | Removed: ${stats.columnsRemoved} | Filters: ${stats.filtersApplied} | Decks saved: ${stats.decksSaved} | Collections: ${stats.collectionsCreated} | ${elapsed}s`);
    },

    // 1. Multi-column view
    scanColumns,
    reorderColumns,
    scrollColumnToTop,

    // 2. Custom column creation
    addColumn,
    addSearchColumn,
    addListColumn,
    removeColumn,

    // 3. Column filtering
    addKeywordFilter,
    addUserFilter,
    listColumnFilters,

    // 4. Deck management
    saveDeck,
    listDecks,
    loadDeck,
    deleteDeck,
    exportDeck,
    importDeck,

    // 5. Collections management
    createCollection,
    listCollections,
    viewCollection,
    addTweetToCollection,
    addTweetByUrl,
    removeTweetFromCollection,
    deleteCollection,
    exportCollection,
  };

  /* ─── Menu ──────────────────────────────────────────────── */

  const showMenu = () => {
    console.log('═══════════════════════════════════════════════════════');
    console.log('📊 XActions — X Pro Manager');
    console.log('   by nichxbt');
    console.log('═══════════════════════════════════════════════════════\n');

    console.log('🎮 CONTROLS:');
    console.log('   window.XActions.xProManager.abort()   — stop running operations');
    console.log('   window.XActions.xProManager.status()  — check stats\n');

    console.log('📐 1. MULTI-COLUMN VIEW:');
    console.log('   .scanColumns()                        — list all current columns');
    console.log('   .reorderColumns(fromIdx, toIdx)       — move a column');
    console.log('   .scrollColumnToTop(idx)               — scroll column to top\n');

    console.log('➕ 2. CUSTOM COLUMN CREATION:');
    console.log('   .addColumn("type")                    — add column (home, notifications, search, list, etc.)');
    console.log('   .addSearchColumn("query")             — add a search column');
    console.log('   .addListColumn("listName")            — add a list column');
    console.log('   .removeColumn(idx)                    — remove a column by index\n');

    console.log('🔧 3. COLUMN FILTERING:');
    console.log('   .addKeywordFilter(colIdx, "word")     — include keyword in column');
    console.log('   .addKeywordFilter(colIdx, "word", true) — exclude keyword');
    console.log('   .addUserFilter(colIdx, "user")        — filter by user');
    console.log('   .listColumnFilters(colIdx)            — show filters for a column\n');

    console.log('💾 4. DECK MANAGEMENT:');
    console.log('   .saveDeck("name")                     — save current column layout');
    console.log('   .listDecks()                          — show saved decks');
    console.log('   .loadDeck("name" | idx)               — view deck details');
    console.log('   .deleteDeck("name" | idx)             — delete a saved deck');
    console.log('   .exportDeck("name" | idx)             — export deck as JSON');
    console.log('   .importDeck(jsonString)               — import a deck\n');

    console.log('📂 5. COLLECTIONS MANAGEMENT:');
    console.log('   .createCollection("name", "desc")     — create a tweet collection');
    console.log('   .listCollections()                    — list all collections');
    console.log('   .viewCollection("name")               — view tweets in a collection');
    console.log('   .addTweetToCollection("name")         — add visible tweets to collection');
    console.log('   .addTweetByUrl("name", "url")         — add a tweet by URL');
    console.log('   .removeTweetFromCollection("name", i) — remove tweet by index');
    console.log('   .deleteCollection("name")             — delete a collection');
    console.log('   .exportCollection("name")             — export collection as JSON\n');

    console.log('💡 All methods: window.XActions.xProManager.<method>()');
    console.log('═══════════════════════════════════════════════════════\n');
  };

  /* ─── Init ──────────────────────────────────────────────── */

  showMenu();

  if (isPro()) {
    console.log('✅ X Pro detected. Scanning columns...\n');
    scanColumns();
  } else {
    console.log('⚠️ You are not on pro.x.com. Navigate there first for full functionality.');
    console.log('💡 Collections and deck saving still work from any page.\n');
  }
})();

⚡ More XActions Scripts

Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.

Browse All Scripts