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