您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Discover similar stories inside works on the Archive Of Our Own.
// ==UserScript== // @name AkinO3 // @namespace http://tampermonkey.net/ // @version 1.11.5 // @description Discover similar stories inside works on the Archive Of Our Own. // @author dxudz // @match https://archiveofourown.org/works/* // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function () { "use strict"; // === SETTINGS with defaults === const defaultSettings = { language: "en", complete: "T", crossover: "", tagBlocklist: "", wordsFrom: "", wordsTo: "" }; const languageOptions = [ { code: "all", label: "All Languages" }, { code: "en", label: "English" }, { code: "zh", label: "Chinese" }, { code: "fr", label: "French" }, { code: "de", label: "German" }, { code: "it", label: "Italian" }, { code: "ja", label: "Japanese" }, { code: "ko", label: "Korean" }, { code: "pl", label: "Polish" }, { code: "pt", label: "Portuguese" }, { code: "ru", label: "Russian" }, { code: "es", label: "Spanish" }, ]; function loadSettings() { return { language: GM_getValue("language", defaultSettings.language), complete: GM_getValue("complete", defaultSettings.complete), crossover: GM_getValue("crossover", defaultSettings.crossover), tagBlocklist: GM_getValue("tagBlocklist", defaultSettings.tagBlocklist), wordsFrom: GM_getValue("wordsFrom", defaultSettings.wordsFrom), wordsTo: GM_getValue("wordsTo", defaultSettings.wordsTo), }; } function saveSettings(settings) { GM_setValue("language", settings.language); GM_setValue("complete", settings.complete); GM_setValue("crossover", settings.crossover); GM_setValue("tagBlocklist", settings.tagBlocklist); GM_setValue("wordsFrom", settings.wordsFrom); GM_setValue("wordsTo", settings.wordsTo); } let modal, overlay; function createSettingsModal() { if (modal) return; overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.top = "0"; overlay.style.left = "0"; overlay.style.width = "100vw"; overlay.style.height = "100vh"; overlay.style.backgroundColor = "rgba(0,0,0,0.5)"; overlay.style.zIndex = "100000"; overlay.style.display = "none"; modal = document.createElement("div"); modal.style.position = "fixed"; modal.style.top = "50%"; modal.style.left = "50%"; modal.style.transform = "translate(-50%, -50%)"; modal.style.backgroundColor = "#ddd"; modal.style.padding = "1em 1.5em"; modal.style.borderRadius = "16px"; modal.style.boxShadow = "0 2px 12px rgba(0,0,0,0.4)"; modal.style.zIndex = "100001"; modal.style.minWidth = "320px"; modal.style.maxWidth = "90vw"; modal.style.display = "none"; modal.style.color = "#000"; modal.style.overflowY = "auto"; modal.style.maxHeight = "80vh"; modal.style.overflowY = "auto"; modal.style.scrollbarWidth = "none"; // Firefox modal.style.msOverflowStyle = "none"; // IE & Edge const style = document.createElement("style"); style.textContent = ` div::-webkit-scrollbar { display: none; } `; document.head.appendChild(style); const langOptionsHtml = languageOptions.map(opt => `<option value="${opt.code}">${opt.label}</option>`).join(""); modal.innerHTML = ` <style> #akinO3-settings-form select, #akinO3-settings-form input, #akinO3-settings-form textarea { width: 100%; padding: 0.5em; margin-bottom: 1.3em; border: 1px solid #ccc; border-radius: 30px; color: #000; font-size: 1em; box-sizing: border-box; box-shadow: inset 1px 1px 4px rgba(0,0,0,0.15); transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.3s ease; font-family: inherit; } #akinO3-settings-form select.akinO3-input { line-height: 1.5em; padding-top: 0.5em; padding-bottom: 0.5em; appearance: none; -webkit-appearance: none; -moz-appearance: none; background-position: right 0.7em center; background-repeat: no-repeat; background-size: 1em; } #akinO3-settings-form input:focus, #akinO3-settings-form textarea:focus, #akinO3-settings-form select:focus { border-color: #900; outline: none; box-shadow: 0 0 5px rgba(136, 0, 0, 0.4); background-color: } #akinO3-settings-form select:hover, #akinO3-settings-form input:hover, #akinO3-settings-form textarea:hover { border-color: #900; box-shadow: 0 0 5px rgba(136, 0, 0, 0.3); transform: scale(0.95); } #akinO3-settings-form select option { font-family: inherit; font-weight: normal; padding: 0.3em 0.5em; } #akinO3-settings-form label { display: block; margin-bottom: 0.4em; font-weight: bold; } #akinO3-settings-form h2 { margin-top: 0; margin-bottom: 0.5em; } #akinO3-settings-form p.header { font-size: 1em; font-weight: normal; margin: 1em 0 1em 0; } #akinO3-settings-form p.word-count-header { font-size: 1em; font-weight: normal; margin: 0.5em 0 1.2em 0; } #akinO3-settings-form .button-group { text-align: right; margin-top: 1em; } #akinO3-settings-form .button-group button { padding: 0.4em 0.9em; border: none; border-radius: 16px; cursor: pointer; font-weight: bold; transition: background 0.2s ease; transform 0.3s ease; } #akinO3-cancel-btn { background: #ccc; color: #000; transition: transform 0.3s ease; margin-right: 0.5em; } #akinO3-cancel-btn:hover { background: #bbb; transform: scale(0.90); } #akinO3-settings-form button[type="submit"] { background: #900; color: #fff; transition: transform 0.3s ease; } #akinO3-settings-form button[type="submit"]:hover { background: #b00; transform: scale(0.90); } #akinO3-settings-form .akinO3-input { width: 100%; padding: 0.5em; font-size: 1em; border: 1px solid #ccc; border-radius: 16px; box-sizing: border-box; margin-bottom: 1.3em; transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.3s ease; height: 2.5em; vertical-align: middle; font-family: inherit; } #akinO3-settings-form .akinO3-input:hover { border-color: #900; box-shadow: 0 0 5px rgba(136, 0, 0, 0.3); transform: scale(0.90); } </style> <h3 style="text-align: center; font-size: 2em; font-weight: bold; margin-bottom: 0;">AkinO3 Parameters</h3> <h5 class="header" style="text-align: center; font-size: 1.1em; margin-bottom: 1.5em;">Select your work preferences!</h5> <form id="akinO3-settings-form"> <label for="akinO3-language">Language:</label> <select id="akinO3-language" name="language" class="akinO3-input"> ${langOptionsHtml} </select> <label for="akinO3-complete">Completion Status:</label> <select id="akinO3-complete" name="complete" class="akinO3-input"> <option value="T">Complete works only</option> <option value="F">Works in progress only</option> <option value="A">All works</option> </select> <label for="akinO3-crossover">Crossovers:</label> <select id="akinO3-crossover" name="crossover" class="akinO3-input"> <option value="">Include crossovers</option> <option value="F">Exclude crossovers</option> <option value="T">Show only crossovers</option> </select> <h5 style="margin-top: 0.5em; margin-bottom: 0.5em; font-weight: normal; font-size: 1.1em;">Word Count:</h5> <label for="akinO3-wordsFrom">From:</label> <input type="number" id="akinO3-wordsFrom" name="wordsFrom" class="akinO3-input" step="1000" /> <label for="akinO3-wordsTo">To:</label> <input type="number" id="akinO3-wordsTo" name="wordsTo" class="akinO3-input" step="1000" /> <label for="akinO3-tagBlocklist">Tag blocklist (comma-separated):</label> <textarea id="akinO3-tagBlocklist" name="tagBlocklist" rows="3" class="akinO3-input" placeholder="e.g. major character death, rape/non-con, gore"></textarea> <div class="button-group"> <button type="button" id="akinO3-cancel-btn" style="transition: transform 0.3s ease;">Close</button> <button type="submit">Save</button> </div> <div id="akinO3-confirmation" style="color: green; margin-top: 10px; display: none; font-weight: bold;"></div> </form> `; document.body.appendChild(overlay); document.body.appendChild(modal); overlay.addEventListener("click", closeModal); document.getElementById("akinO3-cancel-btn").addEventListener("click", closeModal); document.getElementById("akinO3-settings-form").addEventListener("submit", saveModalSettings); } function openSettingsModal() { createSettingsModal(); const settings = loadSettings(); modal.querySelector("#akinO3-language").value = settings.language || "all"; modal.querySelector("#akinO3-complete").value = settings.complete || "T"; modal.querySelector("#akinO3-crossover").value = settings.crossover || ""; modal.querySelector("#akinO3-tagBlocklist").value = settings.tagBlocklist || ""; modal.querySelector("#akinO3-wordsFrom").value = settings.wordsFrom || ""; modal.querySelector("#akinO3-wordsTo").value = settings.wordsTo || ""; modal.querySelector("#akinO3-confirmation").style.display = "none"; modal.querySelector("#akinO3-confirmation").textContent = ""; overlay.style.display = "block"; modal.style.display = "block"; } function closeModal() { if (modal) modal.style.display = "none"; if (overlay) overlay.style.display = "none"; } function saveModalSettings(e) { e.preventDefault(); const form = e.target; const newSettings = { language: form.language.value.toLowerCase(), complete: ["T", "F", "A"].includes(form.complete.value.toUpperCase()) ? form.complete.value.toUpperCase() : "T", crossover: ["", "F", "T"].includes(form.crossover.value.toUpperCase()) ? form.crossover.value.toUpperCase() : "T", tagBlocklist: form.tagBlocklist.value.toLowerCase(), wordsFrom: form.wordsFrom.value.trim(), wordsTo: form.wordsTo.value.trim() }; console.log("Saving settings:", newSettings); saveSettings(newSettings); const confirmation = modal.querySelector("#akinO3-confirmation"); confirmation.innerHTML = "Settings saved!<br>Please run 'Randomize' to apply."; confirmation.style.display = "block"; confirmation.style.color = "#000"; confirmation.style.fontSize = "16px"; confirmation.style.textAlign = "center"; confirmation.style.marginTop = "16px"; } GM_registerMenuCommand("AkinO3 Settings", openSettingsModal); function getTagText(selector) { return Array.from(document.querySelectorAll(selector)).map(el => el.textContent.trim()); } let selectedPairing = null; // Store user's choice for the session function createPairingModal(relationships) { const overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.top = "0"; overlay.style.left = "0"; overlay.style.width = "100%"; overlay.style.height = "100%"; overlay.style.backgroundColor = "rgba(0,0,0,0.5)"; overlay.style.zIndex = "100000"; const modal = document.createElement("div"); modal.style.position = "fixed"; modal.style.top = "50%"; modal.style.left = "50%"; modal.style.transform = "translate(-50%, -50%)"; modal.style.backgroundColor = "#ddd"; modal.style.padding = "1.5em"; modal.style.borderRadius = "16px"; modal.style.boxShadow = "0 2px 12px rgba(0,0,0,0.4)"; modal.style.zIndex = "100001"; modal.style.minWidth = "300px"; modal.style.maxWidth = "90vw"; modal.style.color = "#000"; modal.innerHTML = ` <style> .akinO3-pairing-label { transition: transform 0.2s ease; display: block; margin: 1em 0; } .akinO3-pairing-label:hover { transform: scale(0.95); } </style> <h3 style="text-align: center; margin-top: 0;">What are you looking for today?</h3> <p style="text-align: center; color: #666; font-style: italic; margin: 0 0 1.5em 0; font-size: 0.9em;"> Your choice lasts until you refresh the page. </p> <div class="pairing-section"> <h4 style="margin-bottom: 0.5em; margin-top: 0.5em;">Pairings available:</h4> ${relationships.map(pairing => ` <label class="akinO3-pairing-label" style="display: block; margin: 0.5em 0;"> <input type="radio" name="pairing" value="${pairing}" style="margin-right: 0.5em;"> ${pairing} </label> `).join("")} </div> <div style="display: flex; justify-content: center; gap: 1em; margin-top: 1.5em;"> <button id="selection-cancel-btn" style=" padding: 0.5em 1em; background: #ccc; color: black; border: none; border-radius: 16px; cursor: pointer; font-weight: bold; transition: background-color 0.2s ease, transform 0.3s ease; ">Cancel</button> <button id="selection-start-btn" style=" padding: 0.5em 1em; background: #900; color: white; border: none; border-radius: 16px; cursor: pointer; font-weight: bold; transition: background-color 0.2s ease, transform 0.3s ease; ">Start Search</button> </div> `; return new Promise((resolve) => { document.body.appendChild(overlay); document.body.appendChild(modal); const startBtn = modal.querySelector("#selection-start-btn"); const cancelBtn = modal.querySelector("#selection-cancel-btn"); // Add hover effects startBtn.addEventListener("mouseenter", () => { startBtn.style.backgroundColor = "#b00"; startBtn.style.transform = "scale(0.95)"; }); startBtn.addEventListener("mouseleave", () => { startBtn.style.backgroundColor = "#900"; startBtn.style.transform = "scale(1)"; }); cancelBtn.addEventListener("mouseenter", () => { cancelBtn.style.backgroundColor = "#bbb"; cancelBtn.style.transform = "scale(0.95)"; }); cancelBtn.addEventListener("mouseleave", () => { cancelBtn.style.backgroundColor = "#ccc"; cancelBtn.style.transform = "scale(1)"; }); function cleanup(selection = null) { overlay.remove(); modal.remove(); resolve(selection); } // Click outside to cancel overlay.addEventListener("click", (e) => { if (e.target === overlay) { cleanup(null); } }); // Cancel button cancelBtn.addEventListener("click", (e) => { e.preventDefault(); cleanup(null); }); // Start search button startBtn.addEventListener("click", (e) => { e.preventDefault(); const selected = modal.querySelector('input[name="pairing"]:checked'); cleanup(selected ? selected.value : null); }); // Auto-select first option const firstRadio = modal.querySelector('input[name="pairing"]'); if (firstRadio) firstRadio.checked = true; }); } function buildSearchURL({ fandom, pairing, tags }) { const base = "https://archiveofourown.org/works?"; const params = new URLSearchParams(); const settings = loadSettings(); params.set("work_search[sort_column]", "revised_at"); params.set("work_search[other_tag_names]", tags.join(",")); const excludedTags = settings.tagBlocklist.split(",").map(t => t.trim()).filter(t => t.length); params.set("work_search[excluded_tag_names]", excludedTags.join(",")); if (settings.crossover === "") { params.set("work_search[crossover]", ""); } else if (settings.crossover === "F") { params.set("work_search[crossover]", "F"); } else { params.set("work_search[crossover]", "T"); } if (settings.complete === "T") { params.set("work_search[complete]", "T"); } else if (settings.complete === "F") { params.set("work_search[complete]", "F"); } else { params.set("work_search[complete]", ""); } params.set("work_search[words_from]", settings.wordsFrom || ""); params.set("work_search[words_to]", settings.wordsTo || ""); params.set("work_search[query]", ""); if (settings.language !== "all") { params.set("work_search[language_id]", settings.language); } else { params.delete("work_search[language_id]"); } params.set("commit", "Sort and Filter"); if (pairing) { params.set("tag_id", pairing.replace(/\//g, '*s*')); } else { params.set("tag_id", fandom); } return base + params.toString(); } function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } async function fetchWorks(url) { try { const res = await fetch(url); if (res.status === 429) { // signal to the caller that AO3 is overloaded const err = new Error("AO3_OVERLOADED"); err.code = 429; throw err; } const html = await res.text(); const temp = document.createElement("div"); temp.innerHTML = html; return temp.querySelectorAll("li.work.blurb.group"); } catch (e) { // Only catch network errors, let custom errors propagate if (e.code === 429) throw e; console.error("❌ Fetch error:", e); return []; } } async function fetchRecommendations(forceRefresh = false) { // No longer prompt for pairing here; use selectedPairing from the session const relationships = getTagText("dd.relationship.tags a.tag"); // If no relationships found, exit early if (!relationships.length) { console.log("No relationships found to recommend from."); return; } // Use the selectedPairing for this session if (!selectedPairing) { // Should not happen, but fallback to first relationship if needed selectedPairing = relationships[0]; } const fandoms = getTagText("dd.fandom.tags a.tag"); const allTags = getTagText("dd.freeform.tags a.tag"); if (!fandoms.length) { console.log("Not enough data to recommend."); return; } const fandom = fandoms[0]; const pairing = selectedPairing; // Get settings and filter out blocked tags const settings = loadSettings(); const blockedTags = settings.tagBlocklist .split(",") .map(t => t.trim().toLowerCase()) .filter(t => t.length); // Filter tags before shuffling const filteredTags = allTags.filter(tag => !blockedTags.includes(tag.toLowerCase()) ); const tags = shuffle(filteredTags); const currentWorkIdMatch = window.location.pathname.match(/\/works\/(\d+)/); const currentWorkId = currentWorkIdMatch ? currentWorkIdMatch[1] : null; // Changed here to try 5 tags, then fallback 4, 3, 2, 1 const attempts = [ tags.slice(0, 5), tags.slice(0, 4), tags.slice(0, 3), tags.slice(0, 2), tags.slice(0, 1), ]; let totalDisplayed = 0; const displayedWorkIds = new Set(); const existing = document.getElementById("similar-works-container"); if (existing) existing.remove(); const container = document.createElement("div"); container.id = "similar-works-container"; container.style.display = "block"; container.style.marginTop = "2em"; const titleRow = document.createElement("div"); titleRow.style.display = "flex"; titleRow.style.alignItems = "center"; titleRow.style.justifyContent = "center"; titleRow.style.gap = "1em"; const randomBtn = document.createElement("button"); randomBtn.textContent = "Randomize"; randomBtn.style.cursor = "pointer"; randomBtn.style.padding = "0.25em 0.75em"; randomBtn.style.borderRadius = "0.25em"; randomBtn.style.border = "1px solid #999"; randomBtn.style.color = "#444"; randomBtn.style.backgroundColor = "#eee"; randomBtn.style.fontSize = "100%"; randomBtn.addEventListener("mouseenter", () => { randomBtn.style.color = "#900"; randomBtn.style.boxShadow = "inset 2px 2px 2px #bbb"; }); randomBtn.addEventListener("mouseleave", () => { randomBtn.style.color = "#444"; randomBtn.style.boxShadow = "none"; }); randomBtn.addEventListener("click", () => { const existing = document.getElementById("similar-works-container"); if (existing) existing.remove(); fetchRecommendations(true); }); const title = document.createElement("h3"); title.textContent = "You might also enjoy:"; title.style.textAlign = "center"; titleRow.appendChild(randomBtn); titleRow.appendChild(title); container.appendChild(titleRow); function createMessage(text, tagsOnly) { const msg = document.createElement("div"); msg.style.marginBottom = "5px"; msg.style.marginTop = "5px"; msg.style.fontStyle = "italic"; msg.style.textAlign = "center"; const parts = text.split(" "); const tagIndex = parts.findIndex(part => part.startsWith("matching")); const tagText = parts.slice(tagIndex + 1).join(" "); const preText = parts.slice(0, tagIndex + 1).join(" "); msg.innerHTML = `${preText} <span>${tagText}</span>`; return msg; } let ao3Overloaded = false; for (let i = 0; i < attempts.length && totalDisplayed < 5; i++) { const tagsToUse = attempts[i]; if (!tagsToUse.length) continue; const url = buildSearchURL({ fandom, pairing, tags: tagsToUse }); console.log(`🔗 Attempt #${i + 1}: Tags [${tagsToUse.join(", ")}]`); console.log(`🌐 URL used: ${url}`); let works; try { works = await fetchWorks(url); } catch (e) { if (e.code === 429) { console.warn("🚫 AO3 returned 429 Too Many Requests. Aborting further attempts."); ao3Overloaded = true; // Just set the flag break; } else { console.error("❌ Error while fetching works:", e); continue; } } if (!works.length) { console.log(`⚠️ Found 0 works on attempt #${i + 1}, trying next fallback...`); continue; } const filteredWorks = Array.from(works).filter(w => { let id = null; if (w.hasAttribute("data-work-id")) { id = w.getAttribute("data-work-id"); } else { const link = w.querySelector("h4.heading a[href*='/works/']"); const match = link?.href?.match(/\/works\/(\d+)/); if (match) id = match[1]; } return id !== currentWorkId && !displayedWorkIds.has(id); }); if (!filteredWorks.length) { console.log(`⚠️ Found 0 works after filtering current work on attempt #${i + 1}, trying next fallback...`); continue; } shuffle(filteredWorks); const toShowCount = Math.min(5 - totalDisplayed, filteredWorks.length); const tagLabel = tagsToUse.length === 1 ? "tag" : "tags"; container.appendChild(createMessage(`Showing ${toShowCount} result${toShowCount > 1 ? "s" : ""} matching the ${tagLabel} ${tagsToUse.join(", ")}`)); for (let j = 0; j < toShowCount; j++) { const work = filteredWorks[j]; const clone = work.cloneNode(true); clone.style.border = "1px solid #ccc"; clone.style.padding = "1em"; clone.style.margin = "1em 0"; clone.style.borderRadius = "6px"; clone.style.maxWidth = "950px"; clone.style.marginLeft = "auto"; clone.style.marginRight = "auto"; let workId = null; if (work.hasAttribute("data-work-id")) { workId = work.getAttribute("data-work-id"); } else { const link = work.querySelector("h4.heading a[href*='/works/']"); const match = link?.href?.match(/\/works\/(\d+)/); if (match) workId = match[1]; } if (!workId) continue; displayedWorkIds.add(workId); const markBtn = document.createElement("button"); markBtn.textContent = "Mark for Later"; markBtn.style.padding = "0.25em 0.75em"; markBtn.style.border = "1px solid #999"; markBtn.style.borderRadius = "0.25em"; markBtn.style.color = "#444"; markBtn.style.cursor = "pointer"; markBtn.style.backgroundColor = "#eee"; markBtn.style.fontSize = "100%"; markBtn.style.marginTop = "16px"; markBtn.addEventListener("mouseenter", () => { markBtn.style.color = "#900"; markBtn.style.boxShadow = "inset 2px 2px 2px #bbb"; }); markBtn.addEventListener("mouseleave", () => { markBtn.style.color = "#444"; markBtn.style.boxShadow = "none"; }); markBtn.addEventListener("click", () => { window.open(`https://archiveofourown.org/works/${workId}/mark_for_later/`, "_blank", "noopener,noreferrer"); }); clone.appendChild(markBtn); container.appendChild(clone); } totalDisplayed += toShowCount; } if (ao3Overloaded) { const overloadMsg = document.createElement("div"); overloadMsg.style.textAlign = "center"; overloadMsg.style.fontSize = "18px"; overloadMsg.style.marginTop = "1em"; overloadMsg.style.fontStyle = "italic"; overloadMsg.textContent = "AO3 seems to have gotten too many requests at this time. Please try again in a couple minutes."; container.appendChild(overloadMsg); } else if (totalDisplayed === 0) { const noResultsMsg = document.createElement("div"); noResultsMsg.style.textAlign = "center"; noResultsMsg.style.fontStyle = "italic"; noResultsMsg.style.fontSize = "18px"; noResultsMsg.style.marginTop = "1em"; noResultsMsg.textContent = "Oops, it seems like this search brought up no results at this time. Why don't you try again?"; container.appendChild(noResultsMsg); } let settingsBtn = document.getElementById("akinO3-settings-btn"); if (!settingsBtn) { settingsBtn = document.createElement("button"); settingsBtn.id = "akinO3-settings-btn"; settingsBtn.textContent = "AkinO3 Parameters"; settingsBtn.style.marginTop = "1em"; settingsBtn.style.marginBottom = "1em"; settingsBtn.style.marginLeft = "auto"; settingsBtn.style.marginRight = "auto"; settingsBtn.style.display = "block"; settingsBtn.style.cursor = "pointer"; settingsBtn.style.padding = "0.25em 0.75em"; settingsBtn.style.borderRadius = "0.25em"; settingsBtn.style.border = "1px solid #999"; settingsBtn.style.color = "#444"; settingsBtn.style.backgroundColor = "#eee"; settingsBtn.style.fontSize = "100%"; settingsBtn.addEventListener("mouseenter", () => { settingsBtn.style.color = "#900"; settingsBtn.style.boxShadow = "inset 2px 2px 2px #bbb"; }); settingsBtn.addEventListener("mouseleave", () => { settingsBtn.style.color = "#444"; settingsBtn.style.boxShadow = "none"; }); settingsBtn.addEventListener("click", () => { openSettingsModal(); }); container.appendChild(settingsBtn); } const workskin = document.getElementById("workskin"); if (workskin && workskin.parentNode) { workskin.parentNode.insertBefore(container, workskin.nextSibling); console.log("✅ Inserted recommendations after fic body."); } } // Add this event handler for the "Find Similar Works" link const similarToggle = document.createElement("li"); similarToggle.innerHTML = `<a href="#" id="similar-works-toggle">Find Similar Works</a>`; const navActions = document.querySelector("div.feedback ul.actions"); if (navActions) { navActions.appendChild(similarToggle); } // Only show the pairing modal on the first click per page load // Store the user's choice for the session (until refresh) document.addEventListener("click", async (e) => { if (e.target && e.target.id === "similar-works-toggle") { e.preventDefault(); const container = document.getElementById("similar-works-container"); if (container) { container.style.display = container.style.display === "none" ? "block" : "none"; } else { // Only prompt for pairing if not already selected this session if (!selectedPairing) { const relationships = getTagText("dd.relationship.tags a.tag"); if (relationships.length > 1) { selectedPairing = await createPairingModal(relationships); if (!selectedPairing) { console.log("Selection cancelled by user"); return; } } else if (relationships.length === 1) { selectedPairing = relationships[0]; } else { console.log("No relationships found to recommend from."); return; } } fetchRecommendations(); } } }); })();