F95Zone Latest Highlighter

Highlight thread cards on the Latest Updates Page and adds colorful thread tags!

// ==UserScript==
// @name         F95Zone Latest Highlighter
// @icon         https://external-content.duckduckgo.com/iu/?u=https://f95zone.to/data/avatars/l/1963/1963870.jpg?1744969685
// @namespace    https://f95zone.to/threads/f95zone-latest.250836/
// @homepage     https://f95zone.to/threads/f95zone-latest.250836/
// @homepageURL  https://f95zone.to/threads/f95zone-latest.250836/
// @author       X Death on F95zone
// @match        https://f95zone.to/sam/latest_alpha/*
// @match        https://f95zone.to/threads/*
// @grant        GM.setValue
// @grant        GM.getValues
// @run-at       document-idle
// @license      GPL-3.0-or-later
// @version      3.0.2
// @description  Highlight thread cards on the Latest Updates Page and adds colorful thread tags!
// ==/UserScript==
// ------------------------------------------------------------
// Built on 2025-08-23T16:03:31.250Z — AUTO-GENERATED, edit from /src and rebuild
// ------------------------------------------------------------

(() => {
  // src/constants.js
  var debug = false;
  var state = {
    modalInjected: false,
    tagsUpdated: false,
    colorRendered: false,
    overlayRendered: false,
    threadSettingsRendered: false,
    reapplyOverlay: false,
    isThread: false,
    isLatest: false,
  };
  var defaultColors = {
    completed: "#388e3c",
    onhold: "#1976d2",
    abandoned: "#c9a300",
    highVersion: "#2e7d32",
    invalidVersion: "#a38400",
    tileInfo: "#9398a0",
    tileHeader: "#d9d9d9",
    preferred: "#7b1fa2",
    preferredText: "#ffffff",
    excluded: "#b71c1c",
    excludedText: "#ffffff",
    neutral: "#37383a",
    neutralText: "#9398a0",
  };
  var defaultOverlaySettings = {
    completed: true,
    onhold: true,
    abandoned: true,
    highVersion: true,
    invalidVersion: true,
    preferred: true,
    excluded: true,
    overlayText: true,
    tileText: true,
  };
  var defaultThreadSetting = {
    neutral: true,
    preferred: true,
    preferredShadow: true,
    excluded: true,
    excludedShadow: true,
  };
  var defaultLatestSettings = {
    autoRefresh: false,
    webNotif: false,
    scriptNotif: false,
  };
  var config = {
    tags: [],
    preferredTags: [],
    excludedTags: [],
    color: [],
    overlaySettings: [],
    threadSettings: [],
    configVisibility: true,
    minVersion: 0.5,
    latestSettings: [],
  };
  var STATUS = Object.freeze({
    PREFERRED: "preferred",
    EXCLUDED: "excluded",
    NEUTRAL: "neutral",
  });

  // src/cores/latest.js
  function watchAndUpdateTiles() {
    const mutationObserver = new MutationObserver(() => {
      processAllTiles();
    });
    const latestUpdateWrapper = document.getElementById("latest-page_items-wrap");
    if (!latestUpdateWrapper) return;
    const options = {
      childList: true,
    };
    mutationObserver.observe(latestUpdateWrapper, options);
  }
  function processAllTiles(reset = false) {
    const tiles = document.getElementsByClassName("resource-tile");
    if (!tiles.length) {
      return;
    }
    for (let i = 0; i < tiles.length; i++) {
      processTile(tiles[i], reset);
    }
  }
  function processTile(tile, reset = false) {
    if (tile.dataset.modified === "true" && !reset) return;
    if (reset) tile.dataset.modified = "";
    let isOverlayApplied = false;
    let colors = [];
    const body = tile.querySelector(".resource-tile_body");
    const versionText = getVersionText(tile);
    const match = versionText.match(/(\d+\.\d+)/);
    const versionNumber = match ? parseFloat(match[1]) : null;
    const isValidKeyword = ["full", "final"].some((valid) =>
      versionText.toLowerCase().includes(valid)
    );
    debug && console.log(versionText, versionNumber, match, isValidKeyword);
    const labelText = getLabelText(tile);
    const matchedTag = processTag(tile, config.preferredTags);
    const excludedTag = processTag(tile, config.excludedTags);
    debug && console.log(labelText, matchedTag, excludedTag);
    if (excludedTag && config.overlaySettings.excluded) {
      isOverlayApplied = addOverlayLabel(tile, excludedTag, isOverlayApplied);
      colors.push(config.color.excluded);
    }
    if (matchedTag && config.overlaySettings.preferred) {
      isOverlayApplied = addOverlayLabel(tile, matchedTag, isOverlayApplied);
      colors.push(config.color.preferred);
    }
    if (labelText === "completed" && config.overlaySettings.completed) {
      isOverlayApplied = addOverlayLabel(tile, "Completed", isOverlayApplied);
      colors.push(config.color.completed);
    } else if (labelText === "onhold" && config.overlaySettings.onhold) {
      isOverlayApplied = addOverlayLabel(tile, "On Hold", isOverlayApplied);
      colors.push(config.color.onhold);
    } else if (labelText === "abandoned" && config.overlaySettings.abandoned) {
      isOverlayApplied = addOverlayLabel(tile, "Abandoned", isOverlayApplied);
      colors.push(config.color.abandoned);
    }
    if (
      (config.overlaySettings.highVersion &&
        versionNumber !== null &&
        versionNumber >= config.minVersion) ||
      isValidKeyword
    ) {
      isOverlayApplied = addOverlayLabel(tile, "High Version", isOverlayApplied);
      colors.push(config.color.highVersion);
    } else if (
      versionNumber !== null &&
      versionNumber < config.minVersion &&
      config.overlaySettings.invalidVersion
    ) {
      isOverlayApplied = addOverlayLabel(tile, "Invalid Version", isOverlayApplied);
      colors.push(config.color.invalidVersion);
    }
    body.style.background = "";
    if (colors.length > 0) {
      body.style.background = createSegmentedGradient(colors, "45deg");
    }
    tile.dataset.modified = "true";
  }
  function addOverlayLabel(tile, reasonText, isApplied) {
    if (isApplied || !config.overlaySettings.overlayText) {
      if (!config.overlaySettings.overlayText) {
        removeOverlayLabel();
      }
      return true;
    }
    const thumbWrap = tile.querySelector(".resource-tile_thumb-wrap");
    if (!thumbWrap) return false;
    let existingOverlay = thumbWrap.querySelector(".custom-overlay-reason");
    if (!existingOverlay) {
      existingOverlay = document.createElement("div");
      existingOverlay.className = "custom-overlay-reason";
      thumbWrap.prepend(existingOverlay);
    }
    existingOverlay.innerText = reasonText;
    return true;
  }
  function createSegmentedGradient(colors, direction = "to right") {
    if (!Array.isArray(colors) || colors.length === 0) return "";
    if (colors.length === 1) return colors[0];
    const segment = 100 / colors.length;
    return (
      `linear-gradient(${direction}, ` +
      colors
        .map((color, i) => {
          const start = (i * segment).toFixed(2);
          const end = ((i + 1) * segment).toFixed(2);
          return `${color} ${start}% ${end}%`;
        })
        .join(", ") +
      `)`
    );
  }
  function removeOverlayLabel() {
    let existingOverlay = document.querySelector(".custom-overlay-reason");
    if (existingOverlay) {
      existingOverlay.remove();
    }
  }
  function getLabelText(tile) {
    const labelWrap = tile.querySelector(".resource-tile_label-wrap_right");
    const labelEl = labelWrap?.querySelector('[class^="label--"]');
    return labelEl?.innerHTML?.toLowerCase().trim() || "";
  }
  function processTag(tile, tags) {
    const tagIds = (tile.getAttribute("data-tags") || "")
      .split(",")
      .map((id) => parseInt(id.trim(), 10))
      .filter(Number.isFinite);
    debug && console.log(tagIds);
    const matchedId = tagIds.find((id) => tags.some((tag) => tag === id));
    debug && console.log(matchedId);
    if (!matchedId) return false;
    const matchedTag = config.tags.find((tag) => tag.id == matchedId);
    return matchedTag ? matchedTag.name : false;
  }
  function getVersionText(tile) {
    const versionEl = tile.querySelector(".resource-tile_label-version");
    return versionEl?.innerHTML?.toLowerCase().trim() || "";
  }

  // src/cores/thread.js
  function processThreadTags() {
    const tagList = document.querySelector(".js-tagList");
    if (!tagList) {
      return;
    }
    let tags = tagList.getElementsByClassName("tagItem");
    tags = Array.from(tags);
    tags.forEach((tag) => {
      processThreadTag(tag);
    });
  }
  function processThreadTag(tagElement) {
    const tagName = tagElement.innerHTML.trim();
    const preferredId = config.preferredTags.find((id) =>
      config.tags.find((t) => t.id === id && t.name === tagName)
    );
    const excludedId = config.excludedTags.find((id) =>
      config.tags.find((t) => t.id === id && t.name === tagName)
    );
    Object.values(STATUS).forEach((cls) => tagElement.classList.remove(cls));
    if (preferredId && config.threadSettings.preferred) {
      tagElement.classList.add(STATUS.PREFERRED);
    } else if (excludedId && config.threadSettings.excluded) {
      tagElement.classList.add(STATUS.EXCLUDED);
    } else if (config.threadSettings.neutral) {
      tagElement.classList.add(STATUS.NEUTRAL);
    }
  }
  function autoRefreshClick() {
    const autoRefreshBtn = document.getElementById("controls_auto-refresh");
    if (!autoRefreshBtn) return;
    const selected = autoRefreshBtn.classList.contains("selected");
    if (
      (!selected && config.latestSettings.autoRefresh) ||
      (selected && !config.latestSettings.autoRefresh)
    ) {
      autoRefreshBtn.click();
    }
  }
  function webNotifClick() {
    const webNotifBtn = document.getElementById("controls_notify");
    if (!webNotifBtn) return;
    const selected = webNotifBtn.classList.contains("selected");
    if (!selected && config.latestSettings.webNotif) {
      webNotifBtn.click();
    } else if (selected && !config.latestSettings.webNotif) {
      webNotifBtn.click();
    }
  }

  // src/renderer/updateColorStyle.js
  function updateColorStyle() {
    for (const [key, value] of Object.entries(config.color)) {
      const varName = `--${key}-color`;
      document.documentElement.style.setProperty(varName, value);
      debug && console.log(varName, value);
    }
    const preferredShadow = config.threadSettings.preferredShadow ? "0 0 2px 1px white" : "none";
    const excludedShadow = config.threadSettings.excludedShadow ? "0 0 2px 1px white" : "none";
    document.documentElement.style.setProperty("--preferred-shadow", preferredShadow);
    document.documentElement.style.setProperty("--excluded-shadow", excludedShadow);
  }

  // src/storage/save.js
  async function saveConfigKeys(data) {
    const promises = Object.entries(data).map(([key, value]) => GM.setValue(key, value));
    await Promise.all(promises);
    if (debug) console.log("Config saved (keys)", data);
  }
  async function loadData() {
    let parsed = {};
    try {
      parsed = (await GM.getValues(Object.keys(config))) ?? {};
    } catch (e) {
      debug && console.warn("loadData error:", e);
      parsed = {};
    }
    const result = {
      tags: Array.isArray(parsed.tags) ? parsed.tags : [],
      preferredTags: Array.isArray(parsed.preferredTags) ? parsed.preferredTags : [],
      excludedTags: Array.isArray(parsed.excludedTags) ? parsed.excludedTags : [],
      color: parsed.color && typeof parsed.color === "object" ? parsed.color : { ...defaultColors },
      overlaySettings:
        parsed.overlaySettings && typeof parsed.overlaySettings === "object"
          ? parsed.overlaySettings
          : { ...defaultOverlaySettings },
      threadSettings:
        parsed.threadSettings && typeof parsed.threadSettings === "object"
          ? parsed.threadSettings
          : { ...defaultThreadSetting },
      configVisibility: parsed.configVisibility ?? true,
      minVersion: parsed.minVersion ?? 0.5,
      latestSettings:
        parsed.latestSettings && typeof parsed.latestSettings === "object"
          ? parsed.latestSettings
          : { ...defaultLatestSettings },
    };
    debug && console.log("loadData result:", result);
    return result;
  }

  // src/utils/waitFor.js
  function waitFor(conditionFn, interval = 50, timeout = 2e3) {
    return new Promise((resolve, reject) => {
      const start = Date.now();
      const check = () => {
        if (conditionFn()) {
          resolve(true);
        } else if (Date.now() - start > timeout) {
          reject(new Error("Timeout waiting for condition"));
        } else {
          setTimeout(check, interval);
        }
      };
      check();
    });
  }
  function detectPage() {
    const path = location.pathname;
    if (!window.location.hostname === "f95zone.to") return;
    if (path.startsWith("/threads")) {
      state.isThread = true;
    } else if (path.startsWith("/sam/latest_alpha")) {
      state.isLatest = true;
    }
  }

  // src/data/tags.js
  async function updateTags() {
    if (state.tagsUpdated) return;
    const selector = document.querySelector(".selectize-input.items.not-full");
    const dropdown = document.querySelector(".selectize-dropdown.single.filter-tags-select");
    if (!selector || !dropdown) {
      if (debug) console.log("updateTags: failed to find selector/dropdown");
      return;
    }
    selector.click();
    try {
      await waitFor(() => dropdown.querySelectorAll(".option").length > 0, 50, 3e3);
    } catch (err) {
      if (debug) console.log("updateTags: timeout waiting for options", err);
      return;
    }
    const options = [...dropdown.querySelectorAll(".option")];
    const newTags = options.map((opt) => ({
      id: parseInt(opt.getAttribute("data-value")),
      name: opt.querySelector(".tag-name")?.textContent.trim() || "",
    }));
    const arraysAreDifferent = !(
      config.tags.length === newTags.length &&
      config.tags.every(
        (tag, index) => tag.id === newTags[index].id && tag.name === newTags[index].name
      )
    );
    if (arraysAreDifferent) {
      config.tags = newTags;
      saveConfigKeys({ tags: config.tags });
      if (debug) console.log("updateTags: tags updated", newTags);
    }
    state.tagsUpdated = true;
    if (debug) console.log("updateTags: finished");
  }

  // src/renderer/searchTags.js
  function renderList(filteredTags) {
    const results = document.getElementById("search-results");
    const input = document.getElementById("tags-search");
    if (!results || !input) return;
    results.innerHTML = "";
    const visibleTags = filteredTags.filter(
      (tag) => !config.preferredTags.includes(tag.id) && !config.excludedTags.includes(tag.id)
    );
    if (visibleTags.length === 0) {
      results.style.display = "none";
      return;
    }
    visibleTags.forEach((tag) => {
      const li = document.createElement("li");
      li.classList.add("search-result-item");
      li.style.display = "flex";
      li.style.justifyContent = "space-between";
      li.style.alignItems = "center";
      const nameSpan = document.createElement("span");
      nameSpan.textContent = tag.name;
      const buttonsContainer = document.createElement("div");
      buttonsContainer.style.display = "flex";
      buttonsContainer.style.gap = "5px";
      const preferredBtn = document.createElement("button");
      preferredBtn.textContent = "\u2713";
      preferredBtn.title = "Add to preferred";
      preferredBtn.classList.add("tag-btn", "preferred");
      preferredBtn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        config.preferredTags.push(tag.id);
        renderPreferred();
        state.reapplyOverlay = true;
        input.value = "";
        results.style.display = "none";
        showToast(`${tag.name} added to preferred`);
        saveConfigKeys({ preferredTags: config.preferredTags });
      });
      const excludedBtn = document.createElement("button");
      excludedBtn.textContent = "\u2717";
      excludedBtn.title = "Add to excluded";
      excludedBtn.classList.add("tag-btn", "excluded");
      excludedBtn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        config.excludedTags.push(tag.id);
        renderExcluded();
        state.reapplyOverlay = true;
        input.value = "";
        results.style.display = "none";
        showToast(`${tag.name} added to exclusion`);
        saveConfigKeys({ excludedTags: config.excludedTags });
      });
      buttonsContainer.appendChild(preferredBtn);
      buttonsContainer.appendChild(excludedBtn);
      li.appendChild(nameSpan);
      li.appendChild(buttonsContainer);
      results.appendChild(li);
    });
    results.style.display = "block";
  }
  function renderPreferred() {
    const preferredContainer = document.getElementById("preffered-tags-list");
    if (!preferredContainer) return;
    preferredContainer.innerHTML = "";
    config.preferredTags.forEach((id, index) => {
      const tag = config.tags.find((t) => t.id === id);
      if (!tag) return;
      const item = document.createElement("div");
      item.classList.add("preferred-tag-item");
      const text = document.createElement("span");
      text.textContent = tag.name;
      const removeBtn = document.createElement("button");
      removeBtn.textContent = "X";
      removeBtn.classList.add("preferred-tag-remove");
      removeBtn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        state.reapplyOverlay = true;
        config.preferredTags.splice(index, 1);
        renderPreferred();
        showToast(`${tag.name} removed from preffered`);
        saveConfigKeys({ preferredTags: config.preferredTags });
      });
      item.appendChild(text);
      item.appendChild(removeBtn);
      preferredContainer.appendChild(item);
    });
  }
  function renderExcluded() {
    const excludedContainer = document.getElementById("excluded-tags-list");
    if (!excludedContainer) return;
    excludedContainer.innerHTML = "";
    config.excludedTags.forEach((id, index) => {
      const tag = config.tags.find((t) => t.id === id);
      if (!tag) return;
      const item = document.createElement("div");
      item.classList.add("excluded-tag-item");
      const text = document.createElement("span");
      text.textContent = tag.name;
      const removeBtn = document.createElement("button");
      removeBtn.textContent = "X";
      removeBtn.classList.add("excluded-tag-remove");
      removeBtn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        config.excludedTags.splice(index, 1);
        state.reapplyOverlay = true;
        renderExcluded();
        showToast(`${tag.name} removed from exclusion`);
        saveConfigKeys({ excludedTags: config.excludedTags });
      });
      item.appendChild(text);
      item.appendChild(removeBtn);
      excludedContainer.appendChild(item);
    });
  }

  // src/template/ui.html?raw
  var ui_default =
    '<div id="toast"></div>\r\n<div class="modal-content">\r\n  <h2 style="text-align: center">CONFIG</h2>\r\n\r\n  <!-- General -->\r\n  <div class="modal-settings-spacing">\r\n    <details class="config-list-details">\r\n      <summary>General</summary>\r\n      <div class="settings-wrapper">\r\n        <div class="config-row">\r\n          <label for="config-visibility">Config Visibility</label>\r\n          <input type="checkbox" id="config-visibility" />\r\n        </div>\r\n      </div>\r\n    </details>\r\n  </div>\r\n  <hr class="thick-line" />\r\n  <!-- Latest page settings -->\r\n  <div class="modal-settings-spacing">\r\n    <details class="config-list-details">\r\n      <summary>Latest page settings</summary>\r\n      <div class="settings-wrapper">\r\n        <div id="latest-settings-warning"></div>\r\n        <div class="config-row">\r\n          <label for="settings-auto-refresh">Auto Refresh</label\r\n          ><input type="checkbox" id="settings-auto-refresh" />\r\n        </div>\r\n        <div class="config-row">\r\n          <label for="settings-web-notif">web notification</label\r\n          ><input type="checkbox" id="settings-web-notif" />\r\n        </div>\r\n        <div class="config-row">\r\n          <label for="settings-script-notif">Script notification</label\r\n          ><input type="checkbox" id="settings-script-notif" />\r\n        </div>\r\n        <div class="config-row">\r\n          <label for="min-version">Min Version:</label>\r\n          <input id="min-version" type="number" step="0.1" min="0" placeholder="e.g., 0.5" />\r\n        </div>\r\n        <div id="overlay-settings-container"></div>\r\n      </div>\r\n    </details>\r\n  </div>\r\n  <hr class="thick-line" />\r\n  <!-- Thread settings -->\r\n  <div class="modal-settings-spacing">\r\n    <details class="config-list-details">\r\n      <summary>Thread settings</summary>\r\n      <div class="settings-wrapper">\r\n        <div id="thread-settings-container"></div>\r\n      </div>\r\n    </details>\r\n  </div>\r\n  <hr class="thick-line" />\r\n  <!-- TAGS -->\r\n  <div class="modal-settings-spacing">\r\n    <details class="config-list-details">\r\n      <summary>Tags</summary>\r\n\r\n      <div class="settings-wrapper">\r\n        <div id="tag-error-notif"></div>\r\n        <div id="tags-container">\r\n          <div\r\n            id="search-container"\r\n            style="position: relative; display: inline-block; min-height: 250px; width: 100%"\r\n          >\r\n            <input\r\n              type="text"\r\n              id="tags-search"\r\n              placeholder="Search prefixes..."\r\n              autocomplete="off"\r\n            />\r\n            <ul id="search-results"></ul>\r\n            <div id="preffered-tags-list"></div>\r\n            <div id="excluded-tags-list"></div>\r\n          </div>\r\n        </div>\r\n      </div>\r\n    </details>\r\n  </div>\r\n  <hr class="thick-line" />\r\n  <!-- COLORS -->\r\n  <div class="modal-settings-spacing">\r\n    <details class="config-list-details">\r\n      <summary>Color</summary>\r\n      <div class="settings-wrapper">\r\n        <div id="color-container"></div>\r\n      </div>\r\n      <div class="centered-item">\r\n        <button id="rese-color" class="modal-btn">Reset color</button>\r\n      </div>\r\n    </details>\r\n  </div>\r\n  <hr class="thick-line" />\r\n\r\n  <!-- Close -->\r\n  <div class="centered-item">\r\n    <button id="close-modal" class="modal-btn">\u{1F5D9} Close</button>\r\n  </div>\r\n</div>\r\n';

  // src/template/css.css?raw
  var css_default =
    ':root {\r\n  --completed-color: #388e3c;\r\n  --onhold-color: #1976d2;\r\n  --abandoned-color: #c9a300;\r\n  --highVersion-color: #2e7d32;\r\n  --invalidVersion-color: #a38400;\r\n  --tileInfo-color: #9398a0;\r\n  --tileHeader-color: #d9d9d9;\r\n  --preferred-color: #7b1fa2;\r\n  --preferred-text-color: #ffffff;\r\n  --excluded-color: #b71c1c;\r\n  --excluded-text-color: #ffffff;\r\n  --neutral-color: #37383a;\r\n  --neutral-text-color: #9398a0;\r\n\r\n  /* optional shadow toggles */\r\n  --preferred-shadow: 0 0 2px 1px white;\r\n  --excluded-shadow: 0 0 2px 1px white;\r\n}\r\n#tag-error-notif {\r\n  display: none; /* hidden by default */\r\n  background-color: #ffe5e5; /* soft red/pink */\r\n  color: #b00020; /* dark red text */\r\n  border: 1px solid #b00020;\r\n  padding: 12px 16px;\r\n  border-radius: 6px;\r\n  margin-bottom: 12px;\r\n  font-size: 14px;\r\n  font-weight: 500;\r\n}\r\n.preferred {\r\n  background-color: var(--preferred-color);\r\n  font-weight: bold;\r\n  color: var(--preferred-text-color);\r\n  box-shadow: var(--preferred-shadow);\r\n}\r\n\r\n.excluded {\r\n  background-color: var(--excluded-color);\r\n  font-weight: bold;\r\n  color: var(--excluded-text-color);\r\n  box-shadow: var(--excluded-shadow);\r\n}\r\n\r\n.neutral {\r\n  background-color: var(--neutral-color);\r\n  font-weight: bold;\r\n  color: var(--neutral-text-color);\r\n}\r\n.custom-overlay-reason {\r\n  position: absolute;\r\n  top: 4px;\r\n  left: 4px;\r\n  background: rgba(0, 0, 0, 0.7);\r\n  color: white;\r\n  padding: 2px 6px;\r\n  font-size: 12px;\r\n  border-radius: 4px;\r\n  z-index: 2;\r\n  pointer-events: none;\r\n}\r\n.centered-item {\r\n  display: flex;\r\n  justify-content: center;\r\n  align-items: center;\r\n  padding: 10px;\r\n}\r\n.settings-wrapper {\r\n  padding: 10px;\r\n  color: #ccc;\r\n  font-size: 14px;\r\n  line-height: 1.6;\r\n}\r\ndiv#latest-page_items-wrap_inner\r\n  div.resource-tile\r\n  a.resource-tile_link\r\n  div.resource-tile_info\r\n  div.resource-tile_info-meta {\r\n  color: var(--tileInfo-color);\r\n  font-weight: 600;\r\n}\r\n\r\ndiv#latest-page_items-wrap_inner div.resource-tile a.resource-tile_link {\r\n  color: var(--tileHeader-color);\r\n}\r\n.tag-btn {\r\n  border: none;\r\n  padding: 5px;\r\n  margin: 0 2px;\r\n  cursor: pointer;\r\n  font-size: 14px;\r\n  color: white;\r\n  font-weight: bold;\r\n  transition: background-color 0.2s ease;\r\n}\r\n\r\n.tag-btn.excluded {\r\n  background-color: var(--excluded-color);\r\n  color: var(--excludedText-color);\r\n}\r\n\r\n.tag-btn.preferred {\r\n  background-color: var(--preferred-color);\r\n  color: var(--preferredText-color);\r\n}\r\n\r\n.tag-btn:hover {\r\n  filter: brightness(1.1);\r\n}\r\n#toast {\r\n  position: fixed;\r\n  top: 20px;\r\n  left: 50%;\r\n  transform: translateX(-50%);\r\n  padding: 10px;\r\n  background-color: #333;\r\n  color: #fff;\r\n  border-radius: 8px;\r\n  z-index: 10000; /* above modal */\r\n  opacity: 0;\r\n  transition:\r\n    opacity 0.3s ease,\r\n    top 0.3s ease;\r\n  pointer-events: none; /* doesn\u2019t block clicks */\r\n}\r\n#toast.show {\r\n  opacity: 1;\r\n}\r\n#tag-config-modal {\r\n  display: none;\r\n  position: fixed;\r\n  z-index: 9999;\r\n  top: 0;\r\n  left: 0;\r\n  width: 100%;\r\n  height: 100%;\r\n  background-color: rgba(0, 0, 0, 0.5);\r\n}\r\n/* Preferred tags container */\r\n#preffered-tags-list {\r\n  display: flex;\r\n  flex-wrap: wrap;\r\n  gap: 6px;\r\n  margin-top: 8px;\r\n}\r\n\r\n/* Preferred tag item */\r\n.preferred-tag-item {\r\n  display: inline-flex;\r\n  align-items: center;\r\n  background-color: var(--preferred-color);\r\n  color: var(--preferredText-color);\r\n  border-radius: 4px;\r\n  font-size: 14px;\r\n  font-weight: bold;\r\n}\r\n\r\n.preferred-tag-item span {\r\n  margin-right: 6px;\r\n  margin-left: 6px;\r\n}\r\n\r\n.preferred-tag-remove {\r\n  background-color: #c15858;\r\n  color: #fff;\r\n  border: none;\r\n  border-top-right-radius: 4px;\r\n  border-bottom-right-radius: 4px;\r\n\r\n  padding: 10px;\r\n  cursor: pointer;\r\n  font-weight: bold;\r\n  font-size: 12px;\r\n}\r\n\r\n/* Excluded tags container */\r\n#excluded-tags-list {\r\n  display: flex;\r\n  flex-wrap: wrap;\r\n  gap: 6px;\r\n  margin-top: 8px;\r\n}\r\n\r\n/* Excluded tag item */\r\n.excluded-tag-item {\r\n  display: inline-flex;\r\n  align-items: center;\r\n  background-color: var(--excluded-color);\r\n  color: var(--excludedText-color);\r\n  border-radius: 4px;\r\n  font-size: 14px;\r\n  font-weight: bold;\r\n}\r\n\r\n.excluded-tag-item span {\r\n  margin-right: 6px;\r\n}\r\n\r\n.excluded-tag-remove {\r\n  background-color: #c15858;\r\n  color: #fff;\r\n  border: none;\r\n  padding: 10px;\r\n  cursor: pointer;\r\n  border-top-right-radius: 4px;\r\n  border-bottom-right-radius: 4px;\r\n  font-size: 12px;\r\n  font-weight: bold;\r\n}\r\n\r\n.ignored-tag-remove:hover {\r\n  background-color: #a34040;\r\n}\r\n\r\n/* Individual list items */\r\n#search-results li {\r\n  padding: 6px 8px;\r\n  cursor: pointer;\r\n  color: #fff;\r\n  background-color: #222;\r\n}\r\n\r\n#search-results li:hover {\r\n  background-color: #333; /* slightly lighter on hover */\r\n}\r\n#tags-search {\r\n  background-color: #222;\r\n  color: #fff;\r\n  border: 1px solid #555;\r\n  border-radius: 4px;\r\n  padding: 6px 8px;\r\n  width: 100%;\r\n}\r\n\r\n#tags-search:focus {\r\n  outline: none;\r\n  border: 1px solid #c15858;\r\n}\r\n#search-results {\r\n  position: absolute;\r\n  left: 0;\r\n  right: 0;\r\n  max-height: 200px;\r\n  overflow-y: auto;\r\n  background-color: #222; /* same as inputs */\r\n  border: 1px solid #555; /* same border as input */\r\n  border-radius: 4px;\r\n  margin: 2px 0 0 0; /* small gap below input */\r\n  padding: 0;\r\n  list-style: none;\r\n  display: none;\r\n  z-index: 1000;\r\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); /* subtle shadow */\r\n}\r\n/* All text inputs, textareas, selects */\r\n#tag-config-modal input,\r\n#tag-config-modal textarea,\r\n#tag-config-modal select {\r\n  background-color: #222;\r\n  color: #fff;\r\n  border: 1px solid #555;\r\n  border-radius: 4px;\r\n}\r\n#tag-config-modal input:focus,\r\n#tag-config-modal textarea:focus,\r\n#tag-config-modal select:focus {\r\n  outline: none;\r\n  border: 1px solid #c15858;\r\n}\r\n\r\n/* Checkboxes and radios */\r\n#tag-config-modal input[type="checkbox"],\r\n#tag-config-modal input[type="radio"] {\r\n  accent-color: #c15858;\r\n  background-color: #222;\r\n  border: 1px solid #555;\r\n}\r\n#tag-config-modal .config-color-input {\r\n  border: 2px solid #3f4043;\r\n  border-radius: 5px;\r\n  padding: 2px;\r\n  width: 40px;\r\n  height: 28px;\r\n  cursor: pointer;\r\n  background-color: #181a1d;\r\n}\r\n\r\n#tag-config-modal .config-color-input::-webkit-color-swatch-wrapper {\r\n  padding: 0;\r\n}\r\n\r\n#tag-config-modal .config-color-input::-webkit-color-swatch {\r\n  border-radius: 4px;\r\n  border: none;\r\n}\r\n\r\n.modal-btn {\r\n  background-color: #893839;\r\n  color: white;\r\n  border: 2px solid #893839;\r\n  border-radius: 6px;\r\n  padding: 8px 16px;\r\n  font-weight: 600;\r\n  font-size: 14px;\r\n  cursor: pointer;\r\n  transition:\r\n    background-color 0.3s ease,\r\n    border-color 0.3s ease;\r\n  box-shadow: 0 4px 8px rgba(137, 56, 56, 0.5);\r\n}\r\n\r\n.modal-btn:hover {\r\n  background-color: #b94f4f;\r\n  border-color: #b94f4f;\r\n}\r\n\r\n.modal-btn:active {\r\n  background-color: #6e2b2b;\r\n  border-color: #6e2b2b;\r\n  box-shadow: none;\r\n}\r\n.config-row {\r\n  display: flex;\r\n  gap: 10px;\r\n  margin-bottom: 8px;\r\n  margin-top: 10px;\r\n}\r\n\r\n.config-row label {\r\n  flex-shrink: 0;\r\n  width: 140px; /* fixed width for all labels */\r\n  text-align: left;\r\n  user-select: none;\r\n}\r\n\r\n.config-row input[type="checkbox"],\r\n.config-row input[type="color"],\r\n.config-row input[type="number"] {\r\n  flex-grow: 1;\r\n  width: 10px;\r\n}\r\n\r\n#tag-config-button {\r\n  position: fixed;\r\n  bottom: 20px;\r\n  right: 20px;\r\n  left: 20px;\r\n  padding: 8px 12px;\r\n  font-size: 20px;\r\n  z-index: 7;\r\n  cursor: pointer;\r\n  border: 2px inset #461616;\r\n  background: #cc3131;\r\n  color: white;\r\n  border-radius: 8px;\r\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\r\n  max-width: 70px;\r\n  width: auto;\r\n  opacity: 0.75;\r\n  transition:\r\n    opacity 0.2s ease,\r\n    transform 0.2s ease;\r\n  @media (width < 480px) {\r\n    bottom: 60px;\r\n  }\r\n}\r\n\r\n/* Hover effect */\r\n#tag-config-button:hover {\r\n  opacity: 1;\r\n}\r\n#tag-config-button:active {\r\n  transform: scale(0.9);\r\n}\r\n#tag-config-button.hidden {\r\n  opacity: 0;\r\n  pointer-events: auto;\r\n  transition: opacity 0.3s ease;\r\n}\r\n\r\n#tag-config-button.hidden:hover {\r\n  opacity: 0.75;\r\n}\r\n\r\n#tag-config-modal .modal-content {\r\n  background: black;\r\n  border-radius: 10px;\r\n  min-width: 300px;\r\n  max-height: 80vh;\r\n  overflow-y: scroll; /* always show vertical scrollbar */\r\n  background: #191b1e;\r\n  max-width: 400px;\r\n  margin: 100px auto;\r\n}\r\n\r\n#tag-config-modal.show {\r\n  display: flex;\r\n}\r\n\r\n.config-list-details {\r\n  overflow: hidden;\r\n  transition:\r\n    border-width 1s,\r\n    max-height 1s ease;\r\n  max-height: 40px;\r\n}\r\n\r\n.config-list-details[open] {\r\n  border-width: 2px;\r\n  max-height: 1300px;\r\n}\r\n.thick-line {\r\n  border: none;\r\n  height: 1px;\r\n  background-color: #3f4043;\r\n}\r\n.config-list-details summary {\r\n  text-align: center;\r\n  background: #353535;\r\n  border-radius: 8px;\r\n  padding-top: 5px;\r\n  padding-bottom: 5px;\r\n  cursor: pointer;\r\n}\r\n\r\n.config-tag-item {\r\n  margin-left: 5px;\r\n  cursor: pointer;\r\n}\r\n\r\n.modal-settings-spacing {\r\n  padding: 10px;\r\n}\r\n';

  // src/ui/listeners.js
  function injectListener() {
    setEventById("tag-config-button", openModal);
    setEventById("close-modal", closeModal);
    setEventById("tags-search", updateSearch, "input");
    setEventById("tags-search", showAllTags, "focus");
    setEventById("config-visibility", updateConfigVisibility);
    setEventById("rese-color", resetColor);
    setEventById("min-version", updateMinVersion, "change");
    setEventById("settings-auto-refresh", updateAutoRefresh);
    setEventById("settings-web-notif", updateWebNotif);
    setEventById("settings-script-notif", updateScriptNotif());
    document.addEventListener("click", (e) => {
      const input = document.getElementById("tags-search");
      const results = document.getElementById("search-results");
      if (!input || !results) return;
      if (!input.contains(e.target) && !results.contains(e.target)) {
        results.style.display = "none";
      }
    });
  }
  function setEventById(idSelector, callback, eventType = "click") {
    const el = document.getElementById(idSelector);
    if (el) {
      el.addEventListener(eventType, callback);
    } else {
      console.warn(`setEventById: element with id "${idSelector}" not found.`);
    }
  }
  function updateSearch(event) {
    const query = event.target.value.trim().toLowerCase();
    const results = document.getElementById("search-results");
    if (!query || !results) {
      if (results) results.style.display = "none";
      return;
    }
    const filteredTags = config.tags.filter((tag) => tag.name.toLowerCase().includes(query));
    renderList(filteredTags);
  }
  function showAllTags() {
    const results = document.getElementById("search-results");
    if (!results) return;
    renderList(config.tags);
    results.style.display = "block";
  }
  function updateColor(event, key) {
    const newValue = event.target.value;
    showToast("color saved successfully!");
    config.color[key] = newValue;
    updateColorStyle();
    saveConfigKeys({ color: config.color });
    state.reapplyOverlay = true;
  }
  function updateConfigVisibility(event) {
    config.configVisibility = event.target.checked;
    saveConfigKeys({ configVisibility: config.configVisibility });
    showToast("config visibility saved!");
    updateButtonVisibility();
  }
  function updateMinVersion(event) {
    const valueStr = event.target?.value ?? event.value;
    const value = parseFloat(valueStr);
    if (isNaN(value)) {
      showToast("Invalid version: must be a number");
      return;
    }
    config.minVersion = value;
    saveConfigKeys({ minVersion: config.minVersion });
    showToast(`Min version changed to ${config.minVersion}`);
    state.reapplyOverlay = true;
  }
  function resetColor() {
    if (confirm("Are you sure you want to reset all colors to default?")) {
      config.color = { ...defaultColors };
      updateColorStyle();
      renderColorConfig();
      saveConfigKeys({ color: config.color });
      showToast("Colors have been reset to default");
      state.reapplyOverlay = true;
    }
  }
  function updateAutoRefresh(event) {
    config.latestSettings.autoRefresh = event.target.checked;
    if (!event.target.checked) {
      config.latestSettings.webNotif = false;
      const notif = document.getElementById("settings-web-notif");
      if (notif) notif.checked = false;
    }
    saveConfigKeys({ latestSettings: config.latestSettings });
    const message = event.target.checked ? "Auto refresh enabled" : "Auto refresh disabled";
    showToast(message);
    autoRefreshClick();
  }
  function updateWebNotif(event) {
    const autoRefresh = document.getElementById("settings-auto-refresh");
    if (!autoRefresh.checked) {
      showToast("auto refresh is disabled");
      event.target.checked = false;
      return;
    }
    config.latestSettings.webNotif = event.target.checked;
    saveConfigKeys({ latestSettings: config.latestSettings });
    const message = event.target.checked
      ? "Browser notifications enabled"
      : "Browser notifications disabled";
    showToast(message);
    webNotifClick();
  }
  function updateScriptNotif() {}

  // src/renderer/color.js
  function renderColorConfig() {
    const container = document.getElementById("color-container");
    if (!container) return;
    container.innerHTML = "";
    Object.entries(config.color).forEach(([key, value]) => {
      if (key === "preferred") {
        const hr = document.createElement("hr");
        hr.className = "thick-line";
        container.appendChild(hr);
      }
      const row = document.createElement("div");
      row.className = "config-row";
      const label = document.createElement("label");
      label.setAttribute("for", `color-${key}`);
      label.textContent = key.charAt(0).toUpperCase() + key.slice(1) + ":";
      const input = document.createElement("input");
      input.type = "color";
      input.id = `color-${key}`;
      input.className = "config-color-input";
      input.value = value;
      row.appendChild(label);
      row.appendChild(input);
      container.appendChild(row);
      setEventById(`color-${key}`, (event) => updateColor(event, key), "change");
    });
  }

  // src/renderer/overlay.js
  function renderOverlaySettings() {
    const container = document.getElementById("overlay-settings-container");
    container.innerHTML = "";
    Object.keys(config.overlaySettings).forEach((key) => {
      const row = document.createElement("div");
      row.className = "config-row";
      const label = document.createElement("label");
      label.setAttribute("for", `tag-settings-${key}`);
      label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
      const input = document.createElement("input");
      input.type = "checkbox";
      input.id = `tag-settings-${key}`;
      input.checked = config.overlaySettings[key];
      input.addEventListener("change", (e) => {
        config.overlaySettings[key] = e.target.checked;
        const label2 = key.charAt(0).toUpperCase() + key.slice(1);
        const st = e.target.checked ? "enabled" : "disabled";
        saveConfigKeys({ overlaySettings: config.overlaySettings });
        state.reapplyOverlay = true;
        showToast(`${label2} ${st}`);
      });
      row.appendChild(label);
      row.appendChild(input);
      container.appendChild(row);
    });
  }

  // src/renderer/threadSettings.js
  function renderThreadSettings() {
    const container = document.getElementById("thread-settings-container");
    container.innerHTML = "";
    Object.entries(config.threadSettings).forEach(([key, value]) => {
      const row = document.createElement("div");
      row.className = "config-row";
      const label = document.createElement("label");
      label.setAttribute("for", `thread-settings-${key}`);
      label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
      const checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.id = `thread-settings-${key}`;
      checkbox.checked = value;
      checkbox.addEventListener("change", (e) => {
        config.threadSettings[key] = e.target.checked;
        saveConfigKeys({ threadSettings: config.threadSettings });
        showToast(`${key} ${e.target.checked ? "enabled" : "disabled"}`);
        state.reapplyOverlay = true;
        updateColorStyle();
      });
      row.appendChild(label);
      row.appendChild(checkbox);
      container.appendChild(row);
    });
  }

  // src/renderer/latestSettings.js
  function renderLatest() {
    const elAuto = document.getElementById("settings-auto-refresh");
    if (elAuto) elAuto.checked = !!config.latestSettings.autoRefresh;
    const elNotif = document.getElementById("settings-web-notif");
    if (elNotif) elNotif.checked = !!config.latestSettings.webNotif;
  }

  // src/cores/safety.js
  function checkTags() {
    const el = document.getElementById("tag-error-notif");
    if (!el) return;
    if (config.tags.length === 0) {
      el.textContent = "No tag detected, go to f95zone latest page and open this menu again.";
      el.style.display = "block";
    } else {
      el.style.display = "none";
    }
  }

  // src/ui/modal.js
  function injectButton() {
    const button = document.createElement("button");
    button.textContent = "\u2699\uFE0F";
    button.id = "tag-config-button";
    button.addEventListener("click", () => openModal());
    document.body.appendChild(button);
  }
  function showToast(message, duration = 2e3) {
    let toast = document.getElementById("toast");
    if (!toast) {
      toast = document.createElement("div");
      toast.id = "toast";
      document.body.appendChild(toast);
    }
    toast.textContent = message;
    toast.classList.add("show");
    setTimeout(() => {
      toast.classList.remove("show");
    }, duration);
  }
  function openModal() {
    if (!state.modalInjected) {
      state.modalInjected = true;
      injectModal();
      injectListener();
    }
    if (!state.colorRendered) {
      state.colorRendered = true;
      renderColorConfig();
    }
    if (!state.overlayRendered) {
      state.overlayRendered = true;
      renderOverlaySettings();
    }
    if (!state.threadSettingsRendered) {
      state.threadSettingsRendered = true;
      renderThreadSettings();
      renderLatest();
    }
    document.getElementById("tag-config-modal").style.display = "block";
    renderPreferred();
    renderExcluded();
    updateTags();
    checkTags();
  }
  function closeModal() {
    document.getElementById("tag-config-modal").style.display = "none";
    if (state.reapplyOverlay) {
      if (state.isThread) {
        processThreadTags();
      } else if (state.isLatest) {
        processAllTiles(true);
      }
    }
  }
  function injectModal() {
    const modal = document.createElement("div");
    modal.id = "tag-config-modal";
    modal.innerHTML = `${ui_default}`;
    document.body.appendChild(modal);
    const visibility = document.getElementById("config-visibility");
    if (visibility) visibility.checked = config.configVisibility;
    const minVer = document.getElementById("min-version");
    if (minVer) minVer.value = config.minVersion;
    const modalContent = modal.querySelector(".modal-content");
    modal.addEventListener("click", (e) => {
      if (!modalContent.contains(e.target)) {
        closeModal();
      }
    });
  }
  function injectCSS() {
    const hasStyle = document.head.lastElementChild.textContent.includes("#tag-config-button");
    const customCSS = hasStyle ? document.head.lastElementChild : document.createElement("style");
    customCSS.textContent = `${css_default}`;
    document.head.appendChild(customCSS);
  }
  function updateButtonVisibility() {
    const button = document.getElementById("tag-config-button");
    if (!button) return;
    if (config.configVisibility === false) {
      let blinkCount = 0;
      const maxBlinks = 3;
      const blinkInterval = 400;
      if (button.blinkIntervalId) {
        clearInterval(button.blinkIntervalId);
      }
      button.classList.add("hidden");
      button.blinkIntervalId = setInterval(() => {
        button.classList.toggle("hidden");
        blinkCount++;
        if (blinkCount >= maxBlinks * 2) {
          clearInterval(button.blinkIntervalId);
          button.classList.add("hidden");
          button.blinkIntervalId = void 0;
        }
      }, blinkInterval);
    } else {
      if (button.blinkIntervalId) {
        clearInterval(button.blinkIntervalId);
        button.blinkIntervalId = void 0;
      }
      button.classList.remove("hidden");
    }
  }

  // src/main.js
  function waitForBody(callback) {
    if (document.body) {
      callback();
    } else {
      requestAnimationFrame(() => waitForBody(callback));
    }
  }
  waitForBody(async () => {
    Object.assign(config, await loadData());
    detectPage();
    injectCSS();
    injectButton();
    updateColorStyle();
    updateButtonVisibility();
    if (state.isLatest) {
      waitFor(() => document.getElementById("latest-page_items-wrap"))
        .then(() => {
          watchAndUpdateTiles();
        })
        .catch(() => {
          console.warn("Observer container not found on this page");
        });
      autoRefreshClick();
      webNotifClick();
    }
    if (state.isThread) {
      processThreadTags();
    }
  });
})();