Greasy Fork is available in English.

IgnBoards - Thread Analytics

Counts and displays the number of posts made by each user in a specific IgnBoards thread.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name           IgnBoards - Thread Analytics
// @namespace      Violentmonkey Scripts
// @description    Counts and displays the number of posts made by each user in a specific IgnBoards thread.
// @author         Magof
// @version        1.0
// @license        MIT 
// @match          https://www.ignboards.com/threads/*
// @grant          GM_addStyle
// @grant          GM_registerMenuCommand
// ==/UserScript==

const style = `
  .loader {
    width: 100%;
    height: 30px;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9999;
    background-color: #f5f5f5;
    overflow: hidden;
  }
  .loader-bar {
    height: 100%;
    width: 0;
    background-color: #4caf50;
    animation: progress 3s linear infinite;
  }
  @keyframes progress {
    0% { width: 0; }
    100% { width: 100%; }
  }
  .loader-text {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 14px;
    color: #333;
    white-space: nowrap;
  }
  .scrape-button {
    display: inline-block;
    padding: 8px 15px;
    background-color: #6c757d;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    text-align: center;
    text-decoration: none;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    transition: background-color 0.3s ease;
  }
  .scrape-button:hover {
    background-color: #5a6268;
  }
  .scrape-button:disabled {
    background-color: #cccccc;
    cursor: not-allowed;
  }
`;

GM_addStyle(style);

// Variáveis globais
let isProcessing = false;
let totalPages = 0;
let currentPageNum = 0;

function createScrapeButton() {
  const headerInner = document.querySelector('.uix_headerInner--opposite');
  if (!headerInner) return;

  const button = document.createElement('button');
  button.textContent = 'Analyze Thread';
  button.className = 'scrape-button';
  button.id = 'scrape-button';
  button.onclick = startScraping;
  headerInner.appendChild(button);
}

async function fetchPage(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Failed to fetch page: ${response.status} ${response.statusText}`);
    }
    const body = await response.text();
    return new DOMParser().parseFromString(body, 'text/html');
  } catch (error) {
    console.error(`Erro ao buscar página ${url}:`, error);
    throw error;
  }
}

function getTotalPages(doc) {
  const pageNavElement = doc.querySelector('.pageNav-main');
  if (!pageNavElement) return 1;

  const pageLinks = pageNavElement.querySelectorAll('a[data-page]');
  if (pageLinks.length === 0) return 1;

  const pageNumbers = Array.from(pageLinks).map(link => parseInt(link.getAttribute('data-page')));
  return Math.max(...pageNumbers);
}

function updateLoader(currentPage, totalPages, text) {
  const loaderText = document.querySelector('.loader-text');

  if (loaderText) {
    loaderText.textContent = text || `Analyzing page ${currentPage}...`;
  }
}

async function extractPostData(url, currentPage = 1, processedPosts = new Set()) {
  try {
    const doc = await fetchPage(url);

    if (currentPage === 1) {
      totalPages = getTotalPages(doc);
    }

    updateLoader(currentPage, totalPages);

    // Seletor para posts
    const posts = doc.querySelectorAll('article.message div.message-userDetails');
    const postData = {};

    posts.forEach(post => {
      // Verificar se já processamos este post
      const postArticle = post.closest('article.message');
      const postId = postArticle?.getAttribute('data-content') || postArticle?.id;

      if (postId && processedPosts.has(postId)) {
        return; // Pular posts já processados
      }

      if (postId) {
        processedPosts.add(postId);
      }

      // Seletor para username
      const userElement = post.querySelector('a.username') ||
                         post.querySelector('.username span') ||
                         post.querySelector('h4.message-name span') ||
                         post.querySelector('[data-user-id]');

      let username = userElement ? userElement.textContent.trim() : 'Deleted member';

      // Limpar username de caracteres especiais se necessário
      username = username.replace(/\s+/g, ' ').trim();

      postData[username] = (postData[username] || 0) + 1;
    });

    // Verificar se há próxima página
    const nextPageElement = doc.querySelector('.pageNav-jump.pageNav-jump--next');
    const nextPage = nextPageElement ? nextPageElement.getAttribute('href') : null;

    if (nextPage) {
      const nextUrl = `https://www.ignboards.com${nextPage}`;
      currentPage++;

      // Delay de 1 segundo entre requisições
      await new Promise(resolve => setTimeout(resolve, 1000));

      const nextPostData = await extractPostData(nextUrl, currentPage, processedPosts);

      // Combinar dados das páginas
      for (const [user, count] of Object.entries(nextPostData)) {
        postData[user] = (postData[user] || 0) + count;
      }
    }

    return postData;

  } catch (error) {
    console.error(`Erro na página ${currentPage}:`, error);
    updateLoader(currentPage, totalPages, `Error on page ${currentPage}. Continuing...`);

    // Em caso de erro, retornar dados coletados até agora
    return {};
  }
}

function displayPostData(postData) {
  const sortedData = Object.entries(postData).sort((a, b) => b[1] - a[1]);
  const totalPosts = sortedData.reduce((sum, [, count]) => sum + count, 0);
  const totalUsers = sortedData.length;

  // Função para converter data to BBCode
  function convertToBBCode(data) {
    return data.map(([user, count]) => `[tr][td]${user}[/td][td]${count}[/td][/tr]`).join('\n');
  }

  const bbcodeContent = `[table]
[tr][th]User[/th][th]Posts[/th][/tr]
${convertToBBCode(sortedData)}
[/table]

[b]Total Posts:[/b] ${totalPosts}
[b]Total Users:[/b] ${totalUsers}`;

  const html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Thread Analysis Report</title>
      <!-- Bootstrap CSS -->
      <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet">
      <style>
        body {
          padding: 20px;
        }
        .container {
          max-width: 800px;
          margin: 0 auto;
        }
        h1 {
          margin-bottom: 20px;
          text-align: center;
        }
        .bbcode-area {
          margin-bottom: 20px;
        }
        .stats {
          background-color: #f8f9fa;
          padding: 15px;
          border-radius: 5px;
          margin-bottom: 20px;
        }
        .copy-success {
          color: #28a745;
          font-weight: bold;
        }
      </style>
    </head>
    <body>
      <div class="container">
        <h1>Thread Analysis Report</h1>

        <div class="stats">
          <div class="row">
            <div class="col-md-6">
              <strong>Total Posts:</strong> ${totalPosts}
            </div>
            <div class="col-md-6">
              <strong>Total Users:</strong> ${totalUsers}
            </div>
          </div>
        </div>

        <div class="bbcode-area">
          <h4>Export Code:</h4>
          <textarea id="bbcode" class="form-control" rows="12" readonly>${bbcodeContent}</textarea>
          <button class="btn btn-primary mt-3" onclick="copyBBCode()">Copy for Forum</button>
          <span id="copy-status" class="ml-2"></span>
        </div>

        <div class="table-responsive">
          <table class="table table-striped">
            <thead class="thead-dark">
              <tr>
                <th>#</th>
                <th>Username</th>
                <th>Posts</th>
                <th>%</th>
              </tr>
            </thead>
            <tbody>
              ${sortedData.map(([user, count], index) => {
                const percentage = ((count / totalPosts) * 100).toFixed(1);
                return `<tr>
                  <td>${index + 1}</td>
                  <td>${user}</td>
                  <td>${count}</td>
                  <td>${percentage}%</td>
                </tr>`;
              }).join('')}
            </tbody>
          </table>
        </div>
      </div>

      <!-- Bootstrap JS and dependencies -->
      <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
      <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
      <script>
        function copyBBCode() {
          const bbcodeTextarea = document.getElementById('bbcode');
          const copyStatus = document.getElementById('copy-status');

          bbcodeTextarea.select();
          bbcodeTextarea.setSelectionRange(0, 99999); // Para mobile

          try {
            document.execCommand('copy');
            copyStatus.textContent = 'Copied!';
            copyStatus.className = 'copy-success';
            setTimeout(() => {
              copyStatus.textContent = '';
              copyStatus.className = '';
            }, 2000);
          } catch (err) {
            console.error('Failed to copy: ', err);
            copyStatus.textContent = 'Failed to copy';
            copyStatus.style.color = '#dc3545';
          }
        }
      </script>
    </body>
    </html>
  `;

  const newTab = window.open();
  newTab.document.open();
  newTab.document.write(html);
  newTab.document.close();
}

async function startScraping() {
  if (isProcessing) return;

  isProcessing = true;
  const button = document.getElementById('scrape-button');

  // Desabilitar botão durante processamento
  if (button) {
    button.disabled = true;
    button.textContent = 'Analyzing...';
  }

  const url = window.location.href;
  const loader = createLoader();

  // Reset variáveis globais
  totalPages = 0;
  currentPageNum = 0;

  try {
    updateLoader(1, 0, 'Initializing...');
    const postData = await extractPostData(url, 1);

    document.body.removeChild(loader);
    displayPostData(postData);

  } catch (error) {
    console.error('Erro durante o scraping:', error);
    document.body.removeChild(loader);
    alert('An error occurred while processing. Check console for details.');
  } finally {
    isProcessing = false;

    // Reabilitar botão
    if (button) {
      button.disabled = false;
      button.textContent = 'Analyze Thread';
    }
  }
}

function createLoader() {
  const loader = document.createElement('div');
  loader.className = 'loader';
  loader.innerHTML = `
    <div class="loader-bar"></div>
    <div class="loader-text">Initializing...</div>
  `;
  document.body.appendChild(loader);
  return loader;
}

// Inicializar botão quando a página carregar
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', createScrapeButton);
} else {
  createScrapeButton();
}