您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } }); })();