Torn Revive Blacklist

8/25/2025, 7:08:36 PM - A userscript to allow users to mark players within a revive ban list similar to that of the Torn enemy and target lists.

// ==UserScript==
// @name        Torn Revive Blacklist
// @namespace   Violentmonkey Scripts
// @match       https://www.torn.com/*
// @grant       none
// @version     1.0
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @author      Bilbosaggings[2323763] (BillyBourbon)
// @description 8/25/2025, 7:08:36 PM - A userscript to allow users to mark players within a revive ban list similar to that of the Torn enemy and target lists.
// @license     MIT
// ==/UserScript==

// =======================================================================================================================================

const LOCAL_STORAGE_KEY = "TornReviveBlacklistScript";
const COLOUR_LIST = [
  'Red',
  'Orange',
  'Gold',
  'Yellow',
  'Green',
  'Teal',
  'Cyan',
  'Blue',
  'Navy',
  'Purple',
  'Magenta',
  'Pink',
  'Brown',
  'Olive',
  'Gray',
  'Black',
];
const BAN_TYPE_LIST = ['disable', 'prompt'];

// =======================================================================================================================================

function waitForElement(selector){
  return new Promise((resolve) => {
    if(document.querySelector(selector)){
      resolve(document.querySelector(selector));
    }

    const observer = new MutationObserver((mutations) => {
      if(document.querySelector(selector)){
        resolve(document.querySelector(selector));
        observer.disconnect();
      }
    });

    observer.observe(document.body, {
      subtree:true,
      childList:true,
    });
  });
}

const getCurrentTornUser = () => {
  // { playername, id, avatar, role }
  return JSON.parse(document.getElementById("torn-user").value);
};

const defaultFamilyTargetSettings = { targets: {}, updateTime: 0, url:null };
const defaultSettings = {
  catergories:[
    {
      name: 'Non Payer',
      colour: 'red',
      banType: 'disable'
    },
    {
      name: 'General Douchebag',
      colour: 'purple',
      banType: 'prompt'
    },
    {
      name: 'War Target',
      colour: 'black',
      banType: 'prompt'
    }
  ]
};

async function getScriptData(){
  const data = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '{}');

  if(!data.targets || typeof data.targets !== 'object') data.targets = {};

  if(!data.settings || typeof data.settings !== 'object') data.settings = defaultSettings;

  if(!data.familyTargets || typeof data.familyTargets !== 'object') data.familyTargets = defaultFamilyTargetSettings;

  window.userscriptDataObject = data;

  window.listOfDummies = await getReviveBlacklist();

  return data;
}

async function saveScriptData(data){
  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data));
  window.userscriptDataObject = data;

  await getReviveBlacklist(window.userscriptDataObject);
}

async function getReviveBlacklist() {
  const data = window.userscriptDataObject;
  let publicDumDums;

  if(data.familyTargets.url && data.familyTargets.url.length > 0 && new Date().getTime()/1000 >= (data.familyTargets.updateTime + (15 * 60))){
    try{
      publicDumDums = await fetchPublicDumDums(data.familyTargets.url);

      data.familyTargets.targets = publicDumDums;
      data.familyTargets.updateTime = Math.floor(new Date().getTime() / 1000);

      saveScriptData(data);
    } catch(err){
      console.error('Error Fetching family ban list... ',err);
    }
  } else{
    publicDumDums = data.familyTargets.targets;
  }

  Object.values(publicDumDums).forEach(o => o.banLevel = 'family');

  const personalDumDums = window.userscriptDataObject.targets;

  Object.values(personalDumDums).forEach(o => o.banLevel = 'personal');

  const obj = { ...publicDumDums, ...personalDumDums };

  window.listOfDummies = obj;

  return obj;
};

async function fetchPublicDumDums(baseUrl) {
  const endpoint = `/api/revive-ban-list`;
  const url = `${baseUrl}${endpoint}`;

  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      headers: {
        'Accept': 'application/json'
      },
      onload: function (response) {
        try {
          const json = JSON.parse(response.responseText);
          resolve(json);
        } catch (err) {
          console.error('Failed to parse JSON:', err);
          reject(err);
        }
      },
      onerror: function (err) {
        console.error('Request failed:', err);
        reject(err);
      }
    });
  });
}

async function postPublicDumDum(target){
  const endpoint = `/api/revive-ban-list`;
  const url = `${window.userscriptDataObject.familyTargets.url}${endpoint}`;

  const payload = JSON.stringify({target, sender: getCurrentTornUser().id});

  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method:'POST',
      url: url,
      headers: {
        'Content-Type': 'application/json'
      },
      data: payload,
      onload: (res) => {
        try{
          const json = JSON.parse(res.responseText);
          console.log({json});
          resolve(json);
        } catch(err){
          console.error('Failed to parse JSON:', err);
          reject(err);
        }
      },
      onerror: function (err) {
        console.error('Request failed:', err);
        reject(err);
      }
    });
  });
}

// =======================================================================================================================================

// Setup observer to handle mini profiles
function createMiniProfileObserver(){
  console.log('Creating Observer to handle miniProfiles');
  const observer = new MutationObserver((mutationsList) => {
    for (const mutation of mutationsList) {
      if (mutation.target.id === "profile-mini-root") {
        const miniProfile = mutation.target.children[0];

        if (miniProfile === null || miniProfile === undefined) return;

        handleMiniProfile(miniProfile);
      }
    }
  });

  observer.observe(document.querySelector('body'), {
    childList: true,
    subtree: true,
  });

  console.log('Observer started to handle miniProfiles');
}

async function handleMiniProfile(profile) {
  // console.log('Handling Profile ', {profile});
  const wrapper = await waitForElement(".profile-mini-_userProfileWrapper___iIXVW");

  const profileLink = wrapper.querySelector(".profile-mini-_honorWrap___BHau4 > .profile-mini-_linkWrap___ZS6r9")?.href;
  const playerId = profileLink.match(/XID=(\d+)/)[1];
  const playerName = [...wrapper.querySelectorAll('.honor-text')].filter(x => x.classList.length === 1)[0].textContent ?? '';
  // console.log({playerId, playerName});

  const buttonsWrapper = wrapper.querySelector(".buttons-wrap");
  const buttonsList = buttonsWrapper.querySelector(".buttons-list");

  const reviveButton = buttonsList.querySelector(".profile-button-revive");

  insertAddTargetButton(buttonsWrapper, playerId, playerName);
};

function insertAddTargetButton(buttonsWrapper, playerId, playerName){
  const buttonsList = buttonsWrapper.querySelector(".buttons-list");
  const reviveButton = buttonsList.querySelector(".profile-button-revive");

  const addTargetButton = document.createElement('a');
  addTargetButton.classList.add('profile-button');
  addTargetButton.innerHTML = `
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="46" height="46">
    <!-- Person SVG from buttons   -->
    <g transform="translate(-50,0) scale(9)">
      <path d="M11,33.4v-1.1c0-2.2.2-3.4,2.7-4,2.9-.7,5.8-1.3,4.4-3.8-4.1-7.6-1.2-11.8,3.2-11.8s7.3,4.1,3.2,11.8c-1.3,2.5,1.4,3.1,4.4,3.8,2.6.6,2.7,1.9,2.7,4v1.1H11Z"/>
    </g>
    <!-- Revive SVG from buttons   -->
    <g transform="translate(175,25) scale(6)" fill="red">
      <g transform="translate(-860,-180)">
        <path d="M886.6,199.2a1.02,1.02,0,0,0-.8.5l-1.3,2.4-1.6-4.6a.871.871,0,0,0-.8-.6,1.05,1.05,0,0,0-.9.5l-1.1,2.6-1.7-10.2c-.1-.4-.5-.8-.9-.7a.961.961,0,0,0-.9.7l-1.5,11.9-1.2-6.4a.849.849,0,0,0-.8-.7.8.8,0,0,0-.9.5l-2,4.2H867v1.8h3.7a.891.891,0,0,0,.8-.5l1.1-2.2,1.8,9a.96.96,0,0,0,.9.7h0a.948.948,0,0,0,.9-.8l1.5-11.7,1.3,7.7a.849.849,0,0,0,.8.7.937.937,0,0,0,.9-.5l1.4-3.2,1.5,4.4a.871.871,0,0,0,.8.6,1.05,1.05,0,0,0,.9-.5l2-3.8h3.9v-1.8Z"/>
      </g>
    </g>
  </svg>
  `;

  handleReviveButton(reviveButton, playerId);

  buttonsList.appendChild(addTargetButton);

  if(window?.userscriptDataObject?.targets[playerId] && window?.userscriptDataObject?.targets[playerId].targetName !== playerName) {
    const data = window.userscriptDataObject;
    data.targets[playerId].targetName = playerName;
    saveScriptData(data);

    setTimeout(() => {builUIPanelPersonalBanList(); builUIPanelFamilyBanList();}, 0);
  }

  addTargetButton.addEventListener('click', () => {
    const data = window.userscriptDataObject;
    const currentUser = getCurrentTornUser();

    const originalButonListDisplayStyle = buttonsList.style.display;
    buttonsList.style.display = 'none';

    const inputBox = document.createElement('div');
    inputBox.style.cssText = 'display: flex; flex-direction: column; width: 100%; gap: 6px; align-items: stretch; flex: 1 1 auto; box-sizing: border-box;';
    inputBox.innerHTML = `
    <label>
      Reason:
      <input type="text" id="userScriptReasonInput" placeholder="Enter reason..." style="width: 100%; padding: 4px; box-sizing: border-box;" />
    </label>

    <label>
      Category:
      <select id="userScriptCategorySelect" style="width: 100%; padding: 4px; box-sizing: border-box;">
        <option value="General Douchebag" selected>General Douchebag</option>
        <option value="Non Payer">Non Payer</option>
        <option value="War Target">War Target</option>
      </select>
    </label>
    `;

    const inputBoxButtons = document.createElement('div');
    inputBoxButtons.style.cssText = 'display:flex; flex-direction:row; gap:8px;';

    const submitPersonalButton = document.createElement('button');
    submitPersonalButton.textContent = 'Personal';
    submitPersonalButton.classList.add('script-btn');

    const submitFactionButton = document.createElement('button');
    submitFactionButton.textContent = 'Family';
    submitFactionButton.classList.add('script-btn');

    const cancelButton = document.createElement('button');
    cancelButton.textContent = 'Cancel';
    cancelButton.classList.add('script-btn');
    cancelButton.style.background = 'red';

    submitPersonalButton.addEventListener('click', () => {
      data.targets[playerId] = {
        targetId:playerId,
        targetName: playerName,
        catergory:document.getElementById('userScriptCategorySelect')?.value || 'General Douchebag',
        reason:document.getElementById('userScriptReasonInput')?.value || '',
        banLevel:'personal',
        submitterId:currentUser.id,
        submitterName:currentUser.playername
      };

      saveScriptData(data);

      setTimeout(() => {
        handleReviveButton(reviveButton, playerId);
        builUIPanelPersonalBanList();
        buttonsList.style.display = originalButonListDisplayStyle;
        inputBox.remove();
      }, 0);
    });

    submitFactionButton.addEventListener('click', async () => {
      const obj = {
        targetId:playerId,
        targetName: playerName,
        catergory:document.getElementById('userScriptCategorySelect')?.value || 'General Douchebag',
        reason:document.getElementById('userScriptReasonInput')?.value || '',
        submitterId:currentUser.id,
        submitterName:currentUser.playername
      };

      await postPublicDumDum(obj);

      setTimeout(() => {
        buttonsList.style.display = originalButonListDisplayStyle;
        inputBox.remove();
      }, 0);
    });

    cancelButton.addEventListener('click', () => {
      buttonsList.style.display = originalButonListDisplayStyle;
      inputBox.remove();
    });

    inputBoxButtons.appendChild(submitPersonalButton);

    if(data?.familyTargets?.url && data.familyTargets.url.length > 0) inputBoxButtons.appendChild(submitFactionButton);

    inputBoxButtons.appendChild(cancelButton);

    inputBox.appendChild(inputBoxButtons);

    buttonsWrapper.appendChild(inputBox);
  });
}

function handleReviveButton(reviveButton, playerId){
  if(!reviveButton) return;
  if(reviveButton.style.display === 'none') return;

  const listOfDummies = window.listOfDummies;
  if (!listOfDummies[playerId]) return;

  const userDumDumEntry = listOfDummies[playerId];

  const settings = window.userscriptDataObject.settings;
  const catergory = settings.catergories.find(o => o.name.toLowerCase() === userDumDumEntry.catergory.toLowerCase());

  const buttonReason = `\n${userDumDumEntry.catergory}\nSubmitted By: ${userDumDumEntry.submitterId}\nReason: ${userDumDumEntry.reason}`;

  applyColourCross(reviveButton, catergory.colour);

  switch(catergory.banType.toLowerCase()){
  case('disable'):{
    reviveButton = disableButton(reviveButton);
    break;
  }
  case('prompt'):{
    reviveButton = doubleConfirmOnButton(reviveButton, `Are you absolutely sure?\n${buttonReason}`);
    break;
  }
  }

  reviveButton.setAttribute('title', buttonReason);
}

// =======================================================================================================================================

function disableButton(button){
  const clone = button.cloneNode(true);
  clone.style.pointerEvents = 'none';
  clone.style.opacity = '0.5';
  clone.setAttribute('aria-disabled', 'true');
  button.parentNode.replaceChild(clone, button);

  return clone;
}


function doubleConfirmOnButton(button, confirmMessage = 'Are you sure you want to revive this dum dum ?_?') {
  const proxyClick = (e) => {
    e.stopImmediatePropagation();
    e.preventDefault();

    if(button.dataset.active === 'true') return;
    button.dataset.active = 'true';

    const confirmationBox = document.createElement('div');
    confirmationBox.style.cssText = "display:flex;flex-direction:column; margin-top:10px; margin-left:10px; gap:10px; align-items:center; max-width:250px;";
    confirmationBox.innerHTML = `
      <span style="color:red;">${confirmMessage}</span>
      <button style="background:green; border-radius:8px;" id="confirmYes">YES</button>
      <button style="background:red; border-radius:8px;" id="confirmNo">NO</button>
    `;

    const parent = button.parentElement;
    const parentsParent = parent.parentElement;
    parentsParent.appendChild(confirmationBox);
    const originalDisplayStyle = parent.style.display;
    parent.style.display = 'none';

    confirmationBox.querySelector('#confirmYes').addEventListener('click', () => {
      button.removeEventListener('click', proxyClick, true);
      button.dataset.active = 'false';
      parent.style.display = originalDisplayStyle;

      const newClick = new MouseEvent('click', { bubbles: true, cancelable: true });
      button.dispatchEvent(newClick);
    });

    confirmationBox.querySelector('#confirmNo').addEventListener('click', () => {
      confirmationBox.remove();
      parent.style.display = originalDisplayStyle;

      button.dataset.active = 'false';
    });
  };

  button.addEventListener('click', proxyClick, true);

  return button;
}

function applyColourCross(element, color, opacity = 0.5, thickness = 10) {
  const encodedSVG = encodeURIComponent(`
    <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>
          <line x1='0' y1='0' x2='100' y2='100' stroke='${color.toLowerCase()}' stroke-width='${thickness}' stroke-opacity='${opacity}'/>
          <line x1='100' y1='0' x2='0' y2='100' stroke='${color.toLowerCase()}' stroke-width='${thickness}' stroke-opacity='${opacity}'/>
    </svg>
  `);

  element.style.setProperty("background-image", `url("data:image/svg+xml;utf8,${encodedSVG}")`);
  element.style.setProperty("box-shadow", `0px 0px 2px 1px ${color.toLowerCase()}`);
}

// =======================================================================================================================================

// Handle special pages
function handlePages(){
  const pathname = window.location.pathname;
  console.log('Current Path: ', pathname);
  switch(pathname.toLowerCase()){
  case('/hospitalview.php'):{
    handleHospitalPage();
    break;
  }
  case('/profiles.php'):{
    handleProfilePage();
    break;
  }
  }
}

async function handleHospitalPage(){
  const wrapper = await waitForElement('.userlist-wrapper');

  // console.log('Hospital Wrapper: ', {wrapper});

  const observer = new MutationObserver((mutationsList) => {
    for (const mutation of mutationsList) {
      mutation.addedNodes.forEach(node => {
        if(node.tagName === 'LI'){
          const user = node.querySelector('a.user.name');

          if(user === null) return;

          const id = user.href.match(/XID=(\d+)/)[1];

          const button = node.querySelector('.revive');
          handleReviveButton(button, id);
        }
      });
    }
  });

  observer.observe(document.querySelector('.user-info-list-wrap'), {
    childList: true,
    subtree: true,
  });

  console.log('Observer started to handle Hospital page user list');
}

async function handleProfilePage(){
  // console.log('Handling Profile Page. ', {location: window.location});
  const wrapper = await waitForElement(".buttons-wrap");

  const playerId = window.location.search.match(/XID=(\d+)/)[1];
  const playerName = [...document.querySelectorAll('.honor-text')].filter(x => x.classList.length === 1)[0].textContent ?? '';

  insertAddTargetButton(wrapper, playerId, playerName);

  const personalTarget = window.userscriptDataObject.targets[playerId];
  const familyTarget = window.userscriptDataObject.familyTargets.targets[playerId];

  const profileWrapper = await waitForElement('.user-profile');

  const div = document.createElement('div');
  div.classList.add('profile-wrapper');
  div.classList.add('m-top10');

  const banReasonsTitle = document.createElement('div');
  banReasonsTitle.textContent = `Revive Ban List`;
  banReasonsTitle.classList.add('title-black');
  banReasonsTitle.classList.add('top-round');

  const banReasonsDiv = document.createElement('div');
  banReasonsDiv.classList.add('cont');
  banReasonsDiv.classList.add('bottom-round');
  banReasonsDiv.classList.add('profile-container');

  const personalBanReason = document.createElement('p');
  personalBanReason.textContent = 'This user isnt on your revive ban list';
  personalBanReason.classList.add('t-gray-9');
  personalBanReason.classList.add('p10');

  const familyBanReason = document.createElement('p');
  familyBanReason.textContent = 'This user isnt on your family revive ban list';
  familyBanReason.classList.add('t-gray-9');
  familyBanReason.classList.add('p10');

  if(personalTarget){
    personalBanReason.innerHTML = `Personal Revive Ban:<br>${personalTarget.targetName} [${personalTarget.targetId}] Was submitted by ${personalTarget.submitterName} [${personalTarget.submitterId}].<br>Ban Category: ${personalTarget.catergory} | Reason: ${personalTarget.reason}`;
  }
  if(familyTarget){
    familyBanReason.innerHTML = `Family Revive Ban:<br>${familyTarget.targetName} [${familyTarget.targetId}] Was submitted by ${familyTarget.submitterName} [${familyTarget.submitterId}].<br>Ban Category: ${familyTarget.catergory} | Reason: ${familyTarget.reason}`;
  }

  banReasonsDiv.appendChild(familyBanReason);
  banReasonsDiv.appendChild(personalBanReason);

  div.appendChild(banReasonsTitle);
  div.appendChild(banReasonsDiv);

  // profileWrapper.appendChild(div)
  profileWrapper.insertBefore(div, document.querySelector('.medals-wrapper'));
}

function insertUserscriptUI(){
  insertUserscriptUIPanel();

  builUIPanelPersonalBanList();
  builUIPanelFamilyBanList();
  buildUIPanelSettingsTable();

  addListenersToUI();
}

function insertUserscriptUIPanel(){
  const listOfDummies = window.listOfDummies;
  const personalDummies = Object.values(listOfDummies).filter(o => o.banLevel === 'personal');

  const panelHTML = `
      <div id="settings-panel">
        <div class="box">
          <div class="flex-row flex-gap flex-center">
            <h2>Ban Target</h2>
            <button class="script-btn" id="userScriptButtonClosePanel" style="background:red;">Close</button>
          </div>
          <div class="line-under"></div>
          <div class="flex-row flex-gap flex-center">
            <label class="lbl">
              Target ID:
              <input id="targetId" class="input-field" type="number" min="0"/>
            </label>
            <label class="lbl">
              Reason:
              <input id="reason" class="input-field" type="text"/>
            </label>
            <label class="lbl">
              Category:
              <select id="catergory">
                <option value="Non Payer">Non Payer</option>
                <option value="General Douchebag" selected>General Douchebag</option>
                <option value="War Target">War Target</option>
              </select>
            </label>
          </div>
          <div class="line-under"></div>
          <div class="flex-row flex-gap flex-center">
            <button id="addToPersonalList" class="script-btn">
              To Personal List
            </button>
            <button id="addToFamilyList" class="script-btn">
              To Family List
            </button>
            <p id="message-box" class="message-box"></p>
          </div>
          <div class="line-under"></div>
          <div class="flex-row flex-gap flex-center">
            <h2>Remove Target/s</h2>
            <label class="lbl">
              Target ID:
              <select id="removeTargetId">
                <option value="allTargets">All Targets</option>
                ${
  personalDummies.map(o => `<option value="${o.targetId}">${o.targetId}</option>`).join('')
}
              </select>
            </label>
            <button id="removeFromPersonalList" class="script-btn">
              Remove From Personal List
            </button>
          </div>
        </div>

        <div class="box">
          <h2>Personal Ban List</h2>
          <div class="line-under"></div>
          <div class="table-wrapper">
            <table class="table" >
              <thead>
                <tr>
                  <th>Target</th>
                  <th>Reason</th>
                  <th>Category</th>
                  <th>Remove</th>
                </tr>
              </thead>
              <tbody id="personalBanListBody">
              </tbody>
            </table>
          </div>
        </div>

        <div class="box">
          <h2>Family Ban List</h2>
          <div class="line-under"></div>
          <div class="table-wrapper">
            <table class="table">
              <thead>
                <tr>
                  <th>Target</th>
                  <th>Reason</th>
                  <th>Category</th>
                  <th>Submitter</th>
                </tr>
              </thead>
              <tbody id="familyBanListBody">
              </tbody>
            </table>
          </div>
        </div>

        <div class="box">
          <div>
            <h2>Settings</h2>
          <div class="line-under"></div>
            <div>
              <label class="lbl" style="margin: 0;">
                Set Family List Api URL:
                <input type="text" id="familyListApiUrlInput" class="input-field"/>
              </label>
              <button class="script-btn" id="setApiUrlButton">
                Set Api URL
              </button>
              <button class="script-btn" id="deleteApiUrlButton">
                Delete Api URL
              </button>
            </div>
          <div class="line-under"></div>
            <div>
              <label class="lbl" style="margin: 0;">
              New Category Name:
              <input type="text" id="newCategoryNameInput" class="input-field" placeholder="e.g. Melon, Dum Dum etc" />
            </label>
            <button class="script-btn" id="addNewCategoryButton">
              Add Category
            </button>
            </div>
          <div class="line-under"></div>
              <table class="table" id="userScriptSettingsTable">
              <thead>
                <tr>
                  <th>Ban Category</th>
                  <th>Cross Colour</th>
                  <th>Ban Type</th>
                </tr>
              </thead>
              <tbody>
              </tbody>
            </table>
          <div class="line-under"></div>
            <button class="script-btn" id="updateScriptSettings">
              Update Settings
            </button>
            <button class="script-btn" id="clearScriptData">
              Clear ALL Script Data
            </button>
          </div>
        </div>
      </div>
  `;

  const panel = document.createElement('div');
  panel.innerHTML = panelHTML;
  document.getElementById('mainContainer').appendChild(panel);
}

function setMessage(message){
  const p = document.querySelector('#message-box');
  p.textContent = message;
}

function builUIPanelPersonalBanList(){
  // console.log('Building Personal Ban Table Rows');
  const personalDummies = Object.values(window.listOfDummies).filter(o => o.banLevel === 'personal');
  const personalDummiesRowText = personalDummies.map(o => `
  <tr>
    <td><a href="/profiles.php?XID=${o.targetId}">${o.targetName}[${o.targetId}]</a></td>
    <td>${o.reason}</td>
    <td>${o.catergory}</td>
    <td><button class="removeTargetButton" id="removeRowsTarget" data-target-id="${o.targetId}">Remove</button></td>
  </tr>`).join('\n');

  const tbody = document.querySelector('#personalBanListBody');
  tbody.innerHTML = personalDummiesRowText;

  tbody.querySelectorAll('button').forEach(button => {
    button.addEventListener('click', () => {
      const id = button.getAttribute('data-target-id');

      const data = window.userscriptDataObject;
      const name = data.targets[id].targetName;

      delete data.targets[id];
      saveScriptData(data);

      setTimeout(()=>{builUIPanelPersonalBanList();},0);

      setMessage(`Removed Target: ${name} [${id}]`);
    });
  });

}
function builUIPanelFamilyBanList(){
  // console.log('Building family ban list table');
  // console.log({dummies:window.listOfDummies})
  const familyDummies = Object.values(window.listOfDummies).filter(o => o.banLevel === 'family');
  const familyDummiesRowText = familyDummies.map(o => `
  <tr>
    <td><a href="https://www.torn.com/profiles.php?XID=${o.targetId}">${o.targetName}[${o.targetId}]</a></td>
    <td>${o.reason}</td>
    <td>${o.catergory}</td>
    <td>${o.submitterId}</td>
  </tr>`).join('\n');
  const tbody = document.querySelector('#familyBanListBody');
  tbody.innerHTML = familyDummiesRowText;
}

function buildUIPanelSettingsTable(){
  const settings = window.userscriptDataObject.settings;
  const settingsRowText = Object.values(settings.catergories).map(o => `<tr>
    <td>${o.name}</td>
    <td>
      <select>
        ${
  COLOUR_LIST.map(t => `<option value="${t}"${t === o.colour ? ' selected' : ''}>${t}</option>`).join('')
}
      </select>
    </td>
    <td>
      <select>
        ${
  BAN_TYPE_LIST.map(t => `<option value="${t}"${t === o.banType ? ' selected' : ''}>${t}</option>`).join('')
}
      </select>
    </td>
  </tr>`).join('');

  const tbody = document.querySelector('#userScriptSettingsTable > tbody');
  tbody.innerHTML = settingsRowText;
}

function addListenersToUI(){
  document.getElementById('addToPersonalList').addEventListener('click', () => addTargetToBanList('personal'));
  document.getElementById('addToFamilyList').addEventListener('click', () => addTargetToBanList('family'));
  document.getElementById('removeFromPersonalList').addEventListener('click', () => clearPersonalBanTarget());

  document.getElementById('updateScriptSettings').addEventListener('click', () => updateScriptSettings());
  document.getElementById('clearScriptData').addEventListener('click', () => localStorage.removeItem(LOCAL_STORAGE_KEY));

  document.getElementById('addNewCategoryButton').addEventListener('click', () => {
    const catergoryName = document.getElementById('newCategoryNameInput').value;
    const data = window.userscriptDataObject;
    const i = data.settings.catergories.findIndex(o => o.name.toLowerCase() === catergoryName.toLowerCase());

    if(i >= 0) return;

    data.settings.catergories.push({
      name: catergoryName,
      colour: 'red',
      banType: 'disable'
    });

    saveScriptData(data);

    setTimeout(() => {buildUIPanelSettingsTable();}, 0);

    setMessage(`Added New Category '${catergoryName}'`);
  });

  document.getElementById('setApiUrlButton').addEventListener('click', async () => {
    const url = document.getElementById('familyListApiUrlInput').value;

    const data = window.userscriptDataObject;
    data.familyTargets.url = url;

    await saveScriptData(data);

    setTimeout(() => {builUIPanelFamilyBanList();}, 0);

    setMessage(`Set Family Revive Ban List Api Url: '${url}'`);
  });

  document.getElementById('deleteApiUrlButton').addEventListener('click', () => {
    const data = window.userscriptDataObject;
    data.familyTargets = defaultFamilyTargetSettings;

    saveScriptData(data);

    setTimeout(() => {builUIPanelFamilyBanList();}, 0);

    setMessage('Removed Family Revive Ban List Api Url');
  });

  const toggleBtn = document.createElement('button');
  toggleBtn.innerHTML = `<span class="link-text" style="color:white;">Open Ban Settings</span>`;
  toggleBtn.id = 'banToggleButton';

  const li = document.createElement('li');
  li.classList.add('link');
  li.appendChild(toggleBtn);

  document.querySelector('.settings-menu').appendChild(li);

  toggleBtn.addEventListener('click', () => {
    const overlay = document.getElementById('settings-panel');

    overlay.style.display = (overlay.style.display === 'block' ? 'none' : 'block');
  });

  document.querySelector('#userScriptButtonClosePanel').addEventListener('click', () => document.getElementById('settings-panel').style.display = 'none');
}

// =======================================================================================================================================

function updateScriptSettings(){
  const settings = window.userscriptDataObject.settings;

  const settingsTable = document.querySelector('#userScriptSettingsTable');

  const [, ...rows] = tableToArray(settingsTable);
  rows.forEach(([cat, colour, type]) => {
    const i = settings.catergories.findIndex(o => o.name === cat);
    if(i >= 0){
      settings.catergories[i].colour = colour;
      settings.catergories[i].banType = type;
    } else{
      settings.catergories.push({name:cat, colour, banType:type});
    }
  });

  saveScriptData(window.userscriptDataObject);

  setMessage(`Updated Settings`);
}
function tableToArray(table){
  const rows = [];
  const tableRows = table.querySelectorAll('tr');

  tableRows.forEach(r => {
    const cells = Array.from(r.querySelectorAll('td, th')).map(c => c.querySelector('select') ? c.querySelector('select').value : c.textContent.trim());
    rows.push(cells);
  });

  return rows;
}
// =======================================================================================================================================

function addTargetToBanList(banLevel){
  const targetId = document.getElementById('targetId').value;
  const reason = document.getElementById('reason').value;
  const catergory = document.getElementById('catergory').value;

  const submitter = getCurrentTornUser();
  const submitterId = submitter.id;
  const submitterName = submitter.playername;

  const obj = { targetId, targetName:'', catergory, reason, banLevel, submitterId, submitterName };

  if(!targetId || targetId <= 0) {
    setMessage(`Error: Invalid Target ID Provided '${targetId}'`);
    return;
  }

  if(banLevel === 'family'){ /* empty */ } else{
    const data = window.userscriptDataObject;

    if(!data.targets[targetId]) data.targets[targetId] = obj;
    else {
      data.targets[targetId].catergory = catergory;
      if(reason.length > 0) data.targets[targetId].reason = reason;
    }

    saveScriptData(data);

    setTimeout(() => {builUIPanelPersonalBanList();}, 0);

    setMessage(`Added Target '${targetId}'`);
  }
}

function clearPersonalBanTarget(){
  const id = document.getElementById('removeTargetId').value;
  const data = window.userscriptDataObject;

  if(id === 'allTargets') {
    data.targets = {};
  } else{
    delete data.targets[id];
  }

  saveScriptData(data);

  setTimeout(() => {builUIPanelPersonalBanList();}, 0);

  setMessage(`Cleared Target '${id}'`);
}








GM_addStyle(`
  #settings-panel {
    display: none;
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    z-index: 99999;
    background-color: rgba(0, 0, 0, 0.6);
    overflow-y: auto;
    padding: 32px 20px;
    box-sizing: border-box;
    border-radius: 8px;
    margin: auto;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
  }

  #settings-panel *,
  #settings-panel *::before,
  #settings-panel *::after {
    box-sizing: border-box;
    font-family: Arial, sans-serif;
    color: #333;
  }

  #settings-panel h2 {
    margin: 0;
    font-size: 20px;
    font-weight: 600;
  }

  .flex-row {
    display: flex;
    flex-direction: row;
  }
  .flex-col {
    display: flex;
    flex-direction: column;
  }
  .flex-center {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .flex-gap {
    gap: 16px;
  }

  .box {
    margin-bottom: 24px;
    padding: 16px;
    border: 1px solid #ddd;
    border-radius: 8px;
    background-color: #fff;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
  }

  .margin10 { 
    margin-top: 10px; 
  }

  .line-under {
    border-bottom: 1px solid #ccc;
    margin: 12px 0;
  }

  .message-box {
    border: 1px solid #444;
    width: 260px;
    max-height: 120px;
    min-height: 28px;
    overflow-y: auto;
    border-radius: 6px;
    color: #222;
    text-align: center;
    padding: 6px;
    background: #fafafa;
  }

  .lbl {
    display: block;
    font-size: 14px;
    font-weight: 500;
  }

  .input-field {
    width: 100%;
    max-width: 300px;
    padding: 8px 10px;
    font-size: 14px;
    border: 1px solid #bbb;
    border-radius: 4px;
    margin-bottom: 12px;
    transition: border-color 0.2s ease;
  }
  .input-field:focus {
    border-color: #00796b;
    outline: none;
    box-shadow: 0 0 0 2px rgba(0, 121, 107, 0.2);
  }
  select.input-field {
    width: auto;
  }

  .script-btn {
    font-size: 14px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    background-color: #00796b;
    color: white;
    padding: 8px 16px;
  }
  .script-btn:hover {
    filter: brightness(85%); /* makes any background 15% darker */
  }

  .script-btn:active {
    transform: scale(0.97);
  }

  #banToggleButtonX {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 999999;
    background-color: #333;
    border-radius: 6px;
    font-size: 14px;
    padding: 8px 12px;
    color: #fff;
  }
  #banToggleButtonX:hover {
    background-color: #000;
  }

  .table-wrapper {
    max-height: 240px;
    overflow-y: auto;
    border: 1px solid #ddd;
    border-radius: 6px;
    background: #fff;
  }

  .table {
    width: 100%;
    border-collapse: collapse;
    position: relative;
  }

  .table thead {
    position: sticky;
    top: 0;
    z-index: 10;
    background: #00796b;
    color: #fff;
  }

  .table th,
  .table td {
    padding: 10px 12px;
    text-align: left;
    font-size: 14px;
  }

  .table th {
    font-weight: 600;
    border-bottom: 2px solid #00796b;
  }

  .table td {
    border-bottom: 1px solid #eee;
  }

  .table tbody tr:nth-child(even) {
    background-color: #f9f9f9;
  }
  .table tbody tr:nth-child(odd) {
    background-color: #fff;
  }
  .table tbody tr:hover {
    background-color: #e0f7f5;
  }

  .removeTargetButton {
    margin: 2px;
    background-color: #d9534f;
    color: white;
    padding: 4px 10px;
    border-radius: 4px;
    font-size: 12px;
    cursor: pointer;
    transition: background-color 0.2s ease;
  }
  .removeTargetButton:hover {
    background-color: #c9302c;
  }
`);



(async () => {
  console.log(`Starting ${LOCAL_STORAGE_KEY}...`);

  await getScriptData();

  createMiniProfileObserver();

  handlePages();

  insertUserscriptUI();
})();