BF4 Server Notifier

Notifies when a Battlefield 4 server matches your filters

// ==UserScript==
// @name         BF4 Server Notifier
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Notifies when a Battlefield 4 server matches your filters
// @match        *://battlelog.battlefield.com/bf4/servers/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @author       TorrentOfSouls
// @license      MIT
// ==/UserScript==

window.onload = function () {
  if (Notification.permission !== "granted") {
    Notification.requestPermission();
  }

  const saved = GM_getValue("scriptEnabled");
  const scriptEnabled = saved === "true";

  const maps = {
    Vanilla: [
      "Siege of Shanghai",
      "Paracel Storm",
      "Flood Zone",
      "Zavod 311",
      "Lancang Dam",
      "Hainan Resort",
      "Dawnbreaker",
      "Rogue Transmission",
      "Golmud Railway",
      "Operation Locker",
    ],
    "China Rising": ["Silk Road", "Altai Range", "Guilin Peaks", "Dragon Pass"],
    "Second Assault": [
      "Operation Metro 2014",
      "Caspian Border 2014",
      "Gulf of Oman 2014",
      "Operation Firestorm 2014",
    ],
    "Naval Strike": [
      "Lost Islands",
      "Nansha Strike",
      "Wave Breaker",
      "Operation Mortar",
    ],
    "Dragon's Teeth": [
      "Pearl Market",
      "Propaganda",
      "Sunken Dragon",
      "Lumphini Garden",
    ],
    "Final Stand": [
      "Hangar 21",
      "Operation Whiteout",
      "Giants of Karelia",
      "Hammerhead",
    ],
    "Night Operations": ["Zavod: Graveyard Shift"],
    "Community Operations": ["Operation Outbreak"],
    "Legacy Operations": ["Dragon Valley 2015"],
  };

  const modes = [
    "Conquest",
    "Conquest Large",
    "Conquest Small",
    "Team Deathmatch",
    "Domination",
    "Obliteration",
    "Defuse",
    "Rush",
    "Squad Deathmatch",
    "Chain Link",
    "Carrier Assault",
    "Air Superiority",
    "Gun Master",
    "Capture the Flag",
  ];

  const presets = ["Normal", "Hardcore", "Infantry Only", "Custom"];

  const container = document.createElement("div");
  container.innerHTML = `
            <style>
              #bf4-container {
                position: fixed;
                top: 20px;
                right: 20px;
                background: #000;
                color: #fff;
                border: 2px solid #007bff;
                padding: 0;
                width: 300px;
                font-family: sans-serif;
                font-size: 14px;
                z-index: 10000;
                max-height: 90vh;
                border-radius: 8px;
                box-shadow: 0 0 10px rgba(0,0,0,0.5);
              }
              #bf4-header {
                background-color: #007bff;
                color: white;
                padding: 4px 8px;
                cursor: move;
                display: flex;
                justify-content: space-between;
                align-items: center;
                border-top-left-radius: 6px;
                border-top-right-radius: 6px;
              }
              #bf4-header h3 {
                margin: 0;
                font-size: 16px;
              }
              #toggleBtn {
                background: none;
                border: none;
                color: white;
                font-weight: bold;
                cursor: pointer;
                font-size: 16px;
              }
              #bf4-body {
                padding: 0.5rem;
                background-color: #000;
                overflow-y: auto;
              }
              h3 {
                font-size: 14px;
                margin: 0;
                color: #ddd;
                text-transform: uppercase;
                line-height: 30px;
              }
              .checkbox-group {
                max-height: 100px;
                overflow-y: auto;
                border: 1px solid #444;
                padding: 0.4rem;
                background: #111;
              }
              label {
                display: block;
                margin-bottom: 2px;
                color: #ccc;
                font-size: 13px;
              }
              input[type="number"] {
                width: 96%;
                padding: 4px;
                border-radius: 4px;
                border: 1px solid #555;
                background-color: #222;
                color: #fff;
                font-size: 13px;
              }
              #footer button {
                margin-top: 8px;
                padding: 6px;
                width: 48%;
                font-weight: bold;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 13px;
              }
              #saveBtn { background-color: #007bff; color: white; }
              #saveBtn:hover { background-color: #0056b3; }
              #clearBtn { background-color: #dc3545; color: white; }
              #clearBtn:hover { background-color: #b02a37; }
              .server-row.highlight-match {
                outline: 3px solid #00ffcc !important;
                background-color: rgba(0, 255, 204, 0.1) !important;
                scroll-margin-top: 100px;
              }
              #scriptToggleContainer {
                display: flex;
                align-items: center;
                margin-bottom: 0.7rem;
                gap: 0.5rem;
              }
              .switch {
                position: relative;
                display: inline-block;
                width: 34px;
                height: 18px;
              }
              .switch input {
                opacity: 0;
                width: 0;
                height: 0;
              }
              .slider {
                position: absolute;
                cursor: pointer;
                background-color: #ccc;
                border-radius: 34px;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                transition: 0.3s;
              }
              .slider:before {
                position: absolute;
                content: "";
                height: 12px;
                width: 12px;
                left: 3px;
                bottom: 3px;
                background-color: white;
                border-radius: 50%;
                transition: 0.3s;
              }
              input:checked + .slider {
                background-color: #28a745;
              }
              input:checked + .slider:before {
                transform: translateX(16px);
              }
              .toggle-label {
                color: #fff;
                font-size: 13px;
              }
                #bf4-toast {
                position: absolute;
                bottom: 60px;
                left: 50%;
                transform: translateX(-50%);
                background-color: #28a745;
                color: white;
                padding: 8px 16px;
                border-radius: 6px;
                font-weight: bold;
                font-size: 14px;
                opacity: 0;
                pointer-events: none;
                transition: opacity 0.3s ease;
                z-index: 9999;
                width: 150px;
            }
                #bf4-toast.show {
                opacity: 1;
            }
            #switchInfo {
                display: flex;
                justify-content: space-between;
            }
            </style>
            <div id="bf4-container">
              <div id="bf4-header">
                <h3>BF4 Notifier</h3>
                <button id="toggleBtn">−</button>
              </div>
              <div id="bf4-body">
                <div id='switchInfo'>
                  <div id="scriptToggleContainer"> 
                    <label class="switch">
                        <input type="checkbox" id="toggleScriptSwitch">
                        <span class="slider"></span>
                    </label>
                    <span class="toggle-label">Enable Script</span>
                  </div>

                  <div style="position: relative;">
                    <span id="info-icon" style="cursor: pointer; user-select: none;" title="Click for info">ℹ️</span>
                    <div id="info-box" style="
                        display: none;
                        position: absolute;
                        top: 24px;
                        right: 0;
                        background: #1e1e1e;
                        border: 1px solid #555;
                        border-radius: 8px;
                        padding: 12px;
                        font-size: 12px;
                        color: #ccc;
                        z-index: 9999;
                        width: 260px;
                        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
                    ">
                        <strong style="color:#fff;">How it works:</strong><br><br>
                        This extension checks for Battlefield 4 servers based on your selected filters.<br><br>
                        🔄 It auto-refreshes every 60 seconds.<br>
                        🗺️ You must select at least one map to activate detection.<br>
                        🎮 Modes and presets are optional.<br><br>
                        ⚠️ <strong style="color:#f66;">Important:</strong> You must be on:<br>

                        <a href="https://battlelog.battlefield.com/bf4/servers/" target="_blank" style="color: #ccc; text-decoration: underline;">
                            https://battlelog.battlefield.com/bf4/servers/
                        </a>
                    </div>
                </div>
                </div>
                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
                  <h3 style="margin: 0;">Minimum Players</h3>
                  <div style="font-size: 11px; color: #aaa; text-align: right;" id="countdown-container">
                    ⏱️ Reload servers: <span id="click-timer">30</span>s<br>
                    🔁 Reload page: <span id="reload-timer"> 600</span>s
                  </div>
                </div>
                <input type="number" id="minPlayers" min="0" max="64" />
                <h3>Maps</h3>
                <div id="mapCheckboxes" class="checkbox-group"></div>
                <h3>Modes</h3>
                <div id="modeCheckboxes" class="checkbox-group"></div>
                <h3>Presets</h3>
                <div id="presetCheckboxes" class="checkbox-group"></div>
                <div id="footer" style="display: flex; justify-content: space-between;">
                    <button id="saveBtn">Save</button>
                    <button id="clearBtn">Clear</button>
                </div>
                <div id="bf4-toast">Saved!</div>
                <div style="text-align: center; font-size: 11px; margin-top: 10px; color: #ccc;">
                    made with <span style="color: #e25555;">♥</span> by 
                    <a href="https://www.youtube.com/@TorrentOfSouls" target="_blank" style="color: #ccc; text-decoration: underline;">
                        TorrentOfSouls
                    </a>
                </div>
              </div>
            </div>
          `;

  let clickCountdown = 30;
  let reloadCountdown = 600;

  setInterval(() => {
    const isEnabled = toggleScriptSwitch.checked;
    if (!isEnabled) return;

    if (clickCountdown > 0) clickCountdown--;
    if (reloadCountdown > 0) reloadCountdown--;

    const clickEl = document.getElementById("click-timer");
    const reloadEl = document.getElementById("reload-timer");

    if (clickEl) clickEl.textContent = clickCountdown;
    if (reloadEl) reloadEl.textContent = reloadCountdown;
  }, 1000);

  function resetClickCountdown() {
    clickCountdown = 30;
  }
  function resetReloadCountdown() {
    reloadCountdown = 600;
  }
  document.body.appendChild(container);

  function showToast(message, bgColor = "#28a745") {
    const toast = document.getElementById("bf4-toast");
    toast.textContent = message;
    toast.style.backgroundColor = bgColor;
    toast.classList.add("show");

    setTimeout(() => {
      toast.classList.remove("show");
    }, 2500);
  }

  const infoIcon = document.getElementById("info-icon");
  const infoBox = document.getElementById("info-box");

  let infoBoxTimeout;

  infoIcon.addEventListener("mouseenter", () => {
    clearTimeout(infoBoxTimeout);
    infoBox.style.display = "block";
  });

  infoIcon.addEventListener("mouseleave", () => {
    infoBoxTimeout = setTimeout(() => {
      infoBox.style.display = "none";
    }, 200);
  });

  infoBox.addEventListener("mouseenter", () => {
    clearTimeout(infoBoxTimeout);
    infoBox.style.display = "block";
  });

  infoBox.addEventListener("mouseleave", () => {
    infoBoxTimeout = setTimeout(() => {
      infoBox.style.display = "none";
    }, 200);
  });

  const bf4Container = document.getElementById("bf4-container");
  const bf4Header = document.getElementById("bf4-header");
  let isDragging = false,
    offsetX,
    offsetY;

  bf4Header.addEventListener("mousedown", function (e) {
    isDragging = true;
    offsetX = e.clientX - bf4Container.offsetLeft;
    offsetY = e.clientY - bf4Container.offsetTop;
  });

  document.addEventListener("mouseup", () => (isDragging = false));
  document.addEventListener("mousemove", function (e) {
    if (isDragging) {
      bf4Container.style.left = e.clientX - offsetX + "px";
      bf4Container.style.top = e.clientY - offsetY + "px";
      bf4Container.style.right = "auto";
    }
  });

  const toggleBtn = document.getElementById("toggleBtn");
  const bodyDiv = document.getElementById("bf4-body");
  toggleBtn.onclick = () => {
    const isHidden = bodyDiv.style.display === "none";
    bodyDiv.style.display = isHidden ? "block" : "none";
    toggleBtn.textContent = isHidden ? "−" : "+";
  };

  const mapContainer = document.getElementById("mapCheckboxes");
  const modeContainer = document.getElementById("modeCheckboxes");
  const presetContainer = document.getElementById("presetCheckboxes");
  const minPlayersInput = document.getElementById("minPlayers");
  const saveBtn = document.getElementById("saveBtn");
  const clearBtn = document.getElementById("clearBtn");

  const toggleScriptSwitch = document.getElementById("toggleScriptSwitch");
  toggleScriptSwitch.checked = scriptEnabled;

  const countdownContainer = document.getElementById("countdown-container");
  if (countdownContainer) {
    countdownContainer.style.display = scriptEnabled ? "block" : "none";
  }

  function createCheckbox(name, value, container) {
    const id = `${name}-${value}`;
    const label = document.createElement("label");
    label.htmlFor = id;
    label.innerHTML = `<input type="checkbox" id="${id}" value="${value}" /> ${value}`;
    container.appendChild(label);
  }

  for (const [group, mapList] of Object.entries(maps)) {
    const legend = document.createElement("h6");
    legend.style.margin = "0.3rem 0 0.2rem";
    legend.style.color = "#ccc";
    legend.textContent = group;
    mapContainer.appendChild(legend);
    mapList.forEach((map) => createCheckbox("map", map, mapContainer));
  }

  modes.forEach((mode) => createCheckbox("mode", mode, modeContainer));
  presets.forEach((preset) =>
    createCheckbox("preset", preset, presetContainer)
  );

  function saveFilters() {
    const selectedMaps = [
      ...mapContainer.querySelectorAll("input:checked"),
    ].map((cb) => cb.value);
    const selectedModes = [
      ...modeContainer.querySelectorAll("input:checked"),
    ].map((cb) => cb.value);
    const selectedPresets = [
      ...presetContainer.querySelectorAll("input:checked"),
    ].map((cb) => cb.value);
    const minPlayers = parseInt(minPlayersInput.value || "0");
    const scriptStatus = toggleScriptSwitch.checked;

    GM_setValue("maps", JSON.stringify(selectedMaps));
    GM_setValue("modes", JSON.stringify(selectedModes));
    GM_setValue("presets", JSON.stringify(selectedPresets));
    GM_setValue("minJogadores", minPlayers.toString());
    GM_setValue("scriptEnabled", scriptStatus.toString());

    showToast("Preferences saved!");
    setTimeout(() => location.reload(), 1000);
    resetReloadCountdown();
  }

  function loadFilters() {
    const savedMaps = JSON.parse(GM_getValue("maps") || "[]");
    const savedModes = JSON.parse(GM_getValue("modes") || "[]");
    const savedPresets = JSON.parse(GM_getValue("presets") || "[]");
    const minPlayers = GM_getValue("minJogadores") || "30";

    minPlayersInput.value = minPlayers;
    mapContainer
      .querySelectorAll("input")
      .forEach((cb) => (cb.checked = savedMaps.includes(cb.value)));
    modeContainer
      .querySelectorAll("input")
      .forEach((cb) => (cb.checked = savedModes.includes(cb.value)));
    presetContainer
      .querySelectorAll("input")
      .forEach((cb) => (cb.checked = savedPresets.includes(cb.value)));
  }

  function clearFilters() {
    mapContainer
      .querySelectorAll("input")
      .forEach((cb) => (cb.checked = false));
    modeContainer
      .querySelectorAll("input")
      .forEach((cb) => (cb.checked = false));
    presetContainer
      .querySelectorAll("input")
      .forEach((cb) => (cb.checked = false));
    minPlayersInput.value = "30";
    GM_deleteValue("maps");
    GM_deleteValue("modes");
    GM_deleteValue("presets");
    GM_deleteValue("minJogadores");
    GM_deleteValue("minJogadores");
  }

  function pageLoad() {
    if (!scriptEnabled) return;

    const result = scanServers();
    if (result) return;

    if (!window._bf4ClickInterval) {
      window._bf4ClickInterval = setInterval(() => {
        if (document.visibilityState === "visible") {
          const activeTab = document.querySelector("nav.submenu li.active a");
          if (activeTab) {
            activeTab.click();
            resetClickCountdown();
            console.log("[BF4 Notifier] Clicked active tab (30s refresh)");
          }
        }
      }, 30 * 1000);
    }

    if (!window._bf4ReloadInterval) {
      window._bf4ReloadInterval = setInterval(() => {
        if (document.visibilityState === "visible") {
          location.reload();
          resetReloadCountdown();
          console.log("[BF4 Notifier] Page fully reloaded (10 min)");
        }
      }, 10 * 60 * 1000);
    }
  }

  function scanServers() {
    const serverList = document.getElementsByClassName("server-row");

    const savedMaps = JSON.parse(GM_getValue("maps") || "[]").map((m) =>
      m.trim().toLowerCase()
    );
    const savedModes = JSON.parse(GM_getValue("modes") || "[]").map((m) =>
      m.trim().toLowerCase()
    );
    const savedPresets = JSON.parse(GM_getValue("presets") || "[]").map((p) =>
      p.trim().toLowerCase()
    );
    const minPlayers = parseInt(GM_getValue("minJogadores") || "30", 10);

    let notified = false;
    let foundAny = false;

    for (const serverRow of serverList) {
      const map =
        serverRow
          .getElementsByClassName("map")[1]
          ?.innerHTML.trim()
          .toLowerCase() || "";
      const mode =
        serverRow
          .getElementsByClassName("mode")[0]
          ?.innerHTML.trim()
          .toLowerCase() || "";
      const preset =
        serverRow
          .getElementsByClassName("preset")[0]
          ?.innerHTML.trim()
          .toLowerCase() || "";
      const players = parseInt(
        serverRow.getElementsByClassName("occupied")[0]?.innerHTML || "0",
        10
      );

      const isMapMatch = savedMaps.includes(map);
      const isModeMatch = savedModes.length === 0 || savedModes.includes(mode);
      const isPresetMatch =
        savedPresets.length === 0 || savedPresets.includes(preset);
      const isPlayerCountOk = players >= minPlayers;

      const matched =
        isMapMatch && isModeMatch && isPresetMatch && isPlayerCountOk;

      if (matched) {
        foundAny = true;
        serverRow.classList.add("highlight-match");

        if (!notified && Notification.permission === "granted") {
          notified = true;
          serverRow.scrollIntoView({ behavior: "smooth", block: "center" });
          new Notification("🔥 BF4 Match Found!", {
            body: `Map: ${map}\nMode: ${mode}\nPlayers: ${players}`,
            icon: "https://battlelog.battlefield.com/favicon.ico",
          });
        }
      }
    }

    return foundAny;
  }

  saveBtn.addEventListener("click", saveFilters);
  clearBtn.addEventListener("click", clearFilters);

  loadFilters();
  pageLoad();
};