SG Kick Buffer Column

Auto-loads SG group users, calculates Kick Buffer with flexible settings and highlights negative Value Differences.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         SG Kick Buffer Column
// @namespace    sg-kickbuffer
// @version      1.2
// @description  Auto-loads SG group users, calculates Kick Buffer with flexible settings and highlights negative Value Differences.
// @author       CapnJ
// @license      MIT
// @match        https://www.steamgifts.com/group/*/users*
// @match        https://www.steamgifts.com/group/*/users/search*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  function getSettings() {
    const rawBase = localStorage.getItem('kickBufferBaseLimit');
    const rawPct = localStorage.getItem('kickBufferPercentage');
    const base = rawBase !== null ? parseFloat(rawBase) : 200;
    const pct = rawPct !== null ? parseFloat(rawPct) : 10;
    return {
      baseLimit: isNaN(base) ? 200 : base,
      percentage: isNaN(pct) ? 10 : pct
    };
  }

  function getOffset(username) {
    const raw = localStorage.getItem(`kickBufferOffset_${username}`);
    return raw !== null ? parseInt(raw) || 0 : 0;
  }

  function setOffset(username, value) {
    localStorage.setItem(`kickBufferOffset_${username}`, value);
  }

  function exportData() {
    const settings = {
      baseLimit: localStorage.getItem('kickBufferBaseLimit') || '200',
      percentage: localStorage.getItem('kickBufferPercentage') || '10'
    };
    const offsets = {};
    for (let key in localStorage) {
      if (key.startsWith('kickBufferOffset_')) {
        offsets[key] = localStorage.getItem(key);
      }
    }

    const exportObject = {
      timestamp: new Date().toISOString(),
      settings,
      offsets
    };

    const blob = new Blob([JSON.stringify(exportObject, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const temp = document.createElement('a');
    temp.href = url;
    temp.download = 'kickbuffer_backup.json';
    temp.click();
    URL.revokeObjectURL(url);
  }

  function importData() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'application/json';
    input.style.display = 'none';
    input.addEventListener('change', e => {
      const file = e.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = () => {
        try {
          const imported = JSON.parse(reader.result);
          if (imported.settings) {
            localStorage.setItem('kickBufferBaseLimit', imported.settings.baseLimit);
            localStorage.setItem('kickBufferPercentage', imported.settings.percentage);
          }
          if (imported.offsets) {
            for (let key in imported.offsets) {
              localStorage.setItem(key, imported.offsets[key]);
            }
          }
          rerun();
          alert('✅ Kick Buffer data imported!');
        } catch (err) {
          alert('⚠️ Invalid JSON format.');
        }
      };
      reader.readAsText(file);
    });
    document.body.appendChild(input);
    input.click();
  }

  function injectControls() {
    const nav = document.querySelector('.sidebar__navigation');
    if (!nav || nav.nextElementSibling?.classList.contains('kick-buffer-config-panel')) return;
    const s = getSettings();
    const wrap = document.createElement('div');
    wrap.className = 'kick-buffer-config-panel';
    wrap.style.cssText = 'margin-top:15px;padding:10px;border:1px solid #ccc;background:#f4f4f4;';
    wrap.innerHTML = `
      <strong style="display:block;margin-bottom:8px;">Kick Buffer Settings</strong>
      <label style="margin-right:15px;">
        Base Limit: <input type="number" id="baseLimitInput" value="${s.baseLimit}" step="1" style="width:60px;">
      </label>
      <label>
        Percentage: <input type="number" id="percentageInput" value="${s.percentage}" step="1" style="width:60px;">
      </label>
      <div style="margin-top:10px;">
        <button id="exportKickBuffer">Export Data</button>
        <button id="importKickBuffer">Import Data</button>
      </div>`;
    nav.insertAdjacentElement('afterend', wrap);
    document.getElementById('baseLimitInput').addEventListener('input', () => {
      localStorage.setItem('kickBufferBaseLimit', document.getElementById('baseLimitInput').value);
      rerun();
    });
    document.getElementById('percentageInput').addEventListener('input', () => {
      localStorage.setItem('kickBufferPercentage', document.getElementById('percentageInput').value);
      rerun();
    });
    document.getElementById('exportKickBuffer').addEventListener('click', exportData);
    document.getElementById('importKickBuffer').addEventListener('click', importData);
  }

  function injectHeader() {
    const head = document.querySelector('.table__heading');
    if (!head || head.querySelector('.kick-buffer-header')) return;
    const ref = Array.from(head.children).find(c => c.textContent.trim() === 'Value Difference');
    const buffer = document.createElement('div');
    buffer.className = 'table__column--width-small text-center kick-buffer-header';
    buffer.textContent = 'Kick Buffer';
    const offset = document.createElement('div');
    offset.className = 'table__column--width-small text-center offset-header';
    offset.textContent = 'Offset (-)';

    if (ref) {
      ref.insertAdjacentElement('afterend', buffer);
      buffer.insertAdjacentElement('afterend', offset);
    } else {
      head.appendChild(buffer);
      head.appendChild(offset);
    }
  }

  function injectOffsetCell(row, username) {
    const offset = getOffset(username);
    const cell = document.createElement('div');
    cell.className = 'table__column table__column--width-small text-center offset-cell';

    const input = document.createElement('input');
    input.type = 'number';
    input.value = Math.round(offset);
    input.step = '1';
    input.style.cssText = 'width:60px;text-align:center;font-size:11px;';
    input.addEventListener('change', () => {
      setOffset(username, input.value);
      rerun();
    });

    cell.appendChild(input);
    row.appendChild(cell);
  }

  function processRow(row) {
    if (!row || row.querySelector('.kick-buffer-cell')) return;
    const userLink = row.querySelector('a[href*="/user/"]');
    if (!userLink) return;

    const username = userLink.href.split('/user/')[1];
    const s = getSettings();
    const offset = getOffset(username);

    const cells = row.querySelectorAll('.table__column--width-small.text-center');
    const sentRaw = cells[0]?.textContent.match(/\(([^)]+)\)/)?.[1] || '0';
    const sent = parseFloat(sentRaw.replace(/[^\d.-]/g, '')) || 0;

    const buffer = -(s.baseLimit + sent * (s.percentage / 100) + offset);
    const bCell = document.createElement('div');
    bCell.className = 'table__column table__column--width-small text-center kick-buffer-cell';
    bCell.textContent = buffer.toFixed(2);
    row.appendChild(bCell);

    const valueDiff = parseFloat(cells[3]?.textContent.replace(/[^\d.-]/g, '')) || 0;
    if (cells[3]) {
      cells[3].style.color = valueDiff < buffer ? 'red' : '';
      cells[3].style.fontWeight = valueDiff < buffer ? 'bold' : '';
    }

    injectOffsetCell(row, username);
  }

  function rerun() {
    document.querySelectorAll('.kick-buffer-cell').forEach(e => e.remove());
    document.querySelectorAll('.offset-cell').forEach(e => e.remove());
    document.querySelectorAll('.table__row-inner-wrap').forEach(processRow);
  }

  function autoScrollUntilLoaded(cb) {
    const status = document.createElement('div');
    status.id = 'kick-buffer-status';
    status.style.cssText =
      'position:fixed;bottom:0;left:0;right:0;padding:8px;text-align:center;background:#fffae5;color:#222;z-index:1000;font-weight:bold;box-shadow:0 -2px 4px rgba(0,0,0,0.1);';
    status.textContent = 'Kick Buffer: Loading users…';
    document.body.appendChild(status);

    let prev = 0, idle = 0;
    const interval = setInterval(() => {
      window.scrollTo(0, document.body.scrollHeight);
      const count = document.querySelectorAll('.table__row-inner-wrap').length;
      if (count === prev) {
        idle++;
      } else {
        idle = 0;
        prev = count;
      }
      if (idle > 10) {
        clearInterval(interval);
        status.textContent = 'Kick Buffer: Loaded and calculated.';
        cb();
        setTimeout(() => status.remove(), 5000);
      }
    }, 200);
  }

  setTimeout(() => {
    injectControls();
    injectHeader();
    autoScrollUntilLoaded(() => rerun());
  }, 600);
})();