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.

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();
})();