您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
save include/exclude tags for fast search; hide non-top-tag works
// ==UserScript== // @name AO3 BetterShip // @namespace viasyla // @version 1 // @description save include/exclude tags for fast search; hide non-top-tag works // @author viasyla // @match *://archiveofourown.org/* // @match *://www.archiveofourown.org/* // @icon https://archiveofourown.org/favicon.ico // @license MIT // ==/UserScript== /* START CONFIG */ const relpad = 3; const charpad = 5; /* END CONFIG */ const relationships = []; const characters = []; (function () { const style = document.createElement("style"); style.textContent = ` .workhide { display: flex; justify-content: space-between; align-items: center; font-size: 0.8em; margin: 0.15em 0; width: 100%; } [data-ospp-visibility="false"] > :not(.header), [data-ospp-visibility="false"] > .header > :not(h4) { display: none!important; } [data-ospp-visibility="false"] > div.workhide { display: flex!important; } [data-ospp-visibility="false"] > .header, [data-ospp-visibility="false"] > .header > h4 { margin: 0!important; min-height: auto; font-size: .9em; font-style: italic; } [data-ospp-visibility="false"] { opacity: .6; } .saved-filters-collapser { cursor: pointer; user-select: none; } .saved-filters, .saved-filters > div { margin-bottom: 0.6em; } .saved-filters { border-style: solid; border-width: 1px; padding: 0.6em; } .saved-filters textarea { min-height: 5em; width: 100%; margin-bottom: .3em;} .saved-filters div label { padding-left: 3px; } .prev-search span { color: #000; } .prev-search .temp { background: #ACEA72; } .prev-search .global { background: #93D2ED; } .prev-search .fandom { background: #B9AAED; } .sf-label { display: block; margin-top: .5em; } .ao3-saved-filters-section .sf-row { display: flex; align-items: center; gap: 0em; margin-bottom: 0.2em; width: 100%; box-sizing: border-box; } .ao3-saved-filters-section .checkbox { display: inline-flex; align-items: center; margin: 0; } .ao3-saved-filters-section .indicator { margin-right: 0.18em; padding: 0; } .ao3-saved-filters-section .sf-title { font-weight: normal; font-size: 1em; white-space: nowrap; } .ao3-saved-filters-section .action.js-save-filter { margin-left: auto; flex-shrink: 0; } .ao3-filter-shortcut-btn { width: 100% !important; box-sizing: border-box; margin: 0 0 0.7em 0; min-height: 1.286em; font-size: 100%; text-align: center; } `; document.head.appendChild(style); const fandomLinkElement = document.querySelector("h2.heading a"); if (!fandomLinkElement) return; const fandomLink = fandomLinkElement.href; const tagName = fandomLinkElement.innerText; const isCharacterTag = !(tagName.includes("/") || tagName.includes("&")); if (isCharacterTag) { characters.push(tagName); } else if (!isCharacterTag) { relationships.push(tagName); } fetchFandomData(fandomLink.slice(fandomLink.indexOf("tags")), isCharacterTag) .then(fandomDataAvailable => { if (!fandomDataAvailable) return; insertFilterCheckboxes(); document.getElementById('ospp-relationship-filter').addEventListener('change', applyTagFilter); document.getElementById('ospp-character-filter').addEventListener('change', applyTagFilter); applyTagFilter(); }); const isCharacterSearch = isCharacterTag; const isRelationshipSearch = !isCharacterTag; function insertFilterCheckboxes() { if (document.getElementById('ospp-relationship-filter')) return; const actions = document.querySelector('dd.submit.actions'); if (!actions) return; const parent = actions.parentNode; const dd = document.createElement('dd'); dd.style.marginBottom = '0.6em'; // Relationship const relLabel = document.createElement('label'); relLabel.setAttribute('for', 'ospp-relationship-filter'); const relCheckbox = document.createElement('input'); relCheckbox.type = 'checkbox'; relCheckbox.id = 'ospp-relationship-filter'; relCheckbox.disabled = !isRelationshipSearch; relCheckbox.checked = true && isRelationshipSearch; const relIndicator = document.createElement('span'); relIndicator.className = 'indicator'; relIndicator.setAttribute('aria-hidden', 'true'); const relText = document.createElement('span'); relText.textContent = 'Top Relationship'; relText.style.fontWeight = "normal"; relLabel.appendChild(relCheckbox); relLabel.appendChild(relIndicator); relLabel.appendChild(relText); // Character const charLabel = document.createElement('label'); charLabel.setAttribute('for', 'ospp-character-filter'); const charCheckbox = document.createElement('input'); charCheckbox.type = 'checkbox'; charCheckbox.id = 'ospp-character-filter'; charCheckbox.disabled = !isCharacterSearch; charCheckbox.checked = true && isCharacterSearch; const charIndicator = document.createElement('span'); charIndicator.className = 'indicator'; charIndicator.setAttribute('aria-hidden', 'true'); const charText = document.createElement('span'); charText.textContent = 'Top Character'; charText.style.fontWeight = "normal"; charLabel.appendChild(charCheckbox); charLabel.appendChild(charIndicator); charLabel.appendChild(charText); relLabel.style.marginRight = "1.5em"; relLabel.style.display = 'block'; relLabel.style.marginBottom = '-1em'; dd.appendChild(relLabel); dd.appendChild(document.createElement("br")); dd.appendChild(charLabel); parent.insertBefore(dd, actions); } function applyTagFilter() { const relChecked = document.getElementById('ospp-relationship-filter').checked && isRelationshipSearch; const charChecked = document.getElementById('ospp-character-filter').checked && isCharacterSearch; document.querySelectorAll(".index .blurb").forEach((blurb) => { const tags = blurb.querySelector("ul.tags"); if (!tags) return; const relTags = Array.from(tags.querySelectorAll(".relationships")).slice(0, relpad).map(el => el.innerText); const charTags = Array.from(tags.querySelectorAll(".characters")).slice(0, charpad).map(el => el.innerText); let visible = true; if (relChecked) { visible = relTags.some(tag => relationships.includes(tag)); } if (charChecked) { visible = visible && charTags.some(tag => characters.includes(tag)); } blurb.setAttribute("data-ospp-visibility", visible ? "true" : "false"); if (!visible && !blurb.querySelector('.workhide')) { const buttonDiv = document.createElement("div"); buttonDiv.className = "workhide"; buttonDiv.innerHTML = ` <div class="left">Your preferred tag is not prioritised in this work.</div> <div class="right"><button type="button" class="showwork">Show Work</button></div> `; blurb.insertAdjacentElement("beforeend", buttonDiv); } if (visible && blurb.querySelector('.workhide')) { blurb.querySelector('.workhide').remove(); } }); } document.addEventListener("click", (event) => { if (event.target.matches(".showwork")) { const blurb = event.target.closest(".blurb"); const button = event.target; const isHidden = blurb.getAttribute("data-ospp-visibility") === "false"; blurb.setAttribute("data-ospp-visibility", isHidden ? "true" : "false"); button.textContent = isHidden ? "Hide Work" : "Show Work"; } }); async function fetchFandomData(link, isCharacterTag) { try { const response = await fetch("/" + link + " .parent"); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const pElements = doc.querySelectorAll("p"); if ((pElements[2] && pElements[2].textContent.includes("Additional Tags Category")) || (pElements[3] && pElements[3].textContent.includes("Additional Tags Category"))) { return false; } const synonymSources = doc.querySelectorAll("ul.tags.commas.index.group, ul.tags.tree.index"); synonymSources.forEach((ul, index) => { if (index === 0) return; ul.querySelectorAll(":scope > li").forEach(li => { processListItem(li, isCharacterTag); }); }); return true; } catch (error) { console.error('Error fetching fandom data:', error); return false; } } function processListItem(li, isCharacterTag) { const a = li.querySelector('a'); if (!a) return; const synonym = a.textContent.trim(); if (isCharacterTag) { if (!characters.includes(synonym)) characters.push(synonym); } else { if (!relationships.includes(synonym)) relationships.push(synonym); } const nestedUL = li.querySelector(':scope > ul'); if (nestedUL) { for (const nested of nestedUL.children) { processListItem(nested, isCharacterTag); } } } const TAG_OWNERSHIP_PERCENT = 70; const works = document.querySelector('#main.works-index'); const form = document.querySelector('form#work-filters'); if (!works || !form) return; function getFandomName() { const fandomLabel = document.querySelector('#include_fandom_tags label'); const heading = works.querySelector('.heading'); if (!fandomLabel || !heading) return null; const fandom = fandomLabel.textContent; const fandomCount = parseInt(fandom.substring(fandom.lastIndexOf('(') + 1, fandom.lastIndexOf(')'))); let tagCount = heading.textContent; tagCount = tagCount.substring(0, tagCount.indexOf(' Works')); tagCount = parseInt(tagCount.substring(tagCount.lastIndexOf(' ') + 1)); const fandomName = fandom.substring(0, fandom.lastIndexOf('(')).trim(); if (!fandomName || !fandomCount || !tagCount) return null; return (fandomCount / tagCount * 100 > TAG_OWNERSHIP_PERCENT) ? fandomName : null; } const fandomName = getFandomName(); const tempKey = 'temp-filter'; const tempGlobalKey = 'temp-global-filter'; const tempFandomKey = 'temp-fandom-filter'; const tempExcludeGlobalKey = 'temp-exclude-global-filter'; const tempExcludeFandomKey = 'temp-exclude-fandom-filter'; const globalKey = 'global-filter'; const fandomKey = fandomName ? 'filter-' + fandomName : ''; const excludeGlobalKey = 'exclude-global-filter'; const excludeFandomKey = fandomName ? 'exclude-filter-' + fandomName : ''; const prevGlobal = localStorage[globalKey] || ''; const prevFandom = fandomKey ? localStorage[fandomKey] || '' : ''; const prevExcludeGlobal = localStorage[excludeGlobalKey] || ''; const prevExcludeFandom = excludeFandomKey ? localStorage[excludeFandomKey] || '' : ''; const search = document.querySelector('#work_search_query'); if (!search) return; const dd = search.closest('dd'); const dt = dd && dd.previousElementSibling && dd.previousElementSibling.tagName === 'DT' ? dd.previousElementSibling : null; const realSearch = document.createElement('textarea'); realSearch.style.display = 'none'; realSearch.name = search.name; search.removeAttribute('name'); search.insertAdjacentElement('afterend', realSearch); const container = document.createElement('div'); container.className = 'saved-filters'; function makeFilterSection(opts) { const div = document.createElement('div'); div.className = opts.className + ' ao3-saved-filters-section'; const textarea = document.createElement('textarea'); textarea.value = opts.value; textarea.id = opts.id; textarea.placeholder = opts.placeholder || "tag11;tag12,tag13\ntag21;tag22"; textarea.style.boxSizing = "border-box"; textarea.style.marginTop = ".3em"; textarea.style.width = "100%"; const row = document.createElement('div'); row.className = 'sf-row'; row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.gap = '0em'; row.style.marginBottom = '.2em'; const typeClass = opts.type === 'exclude' ? ' exclude' : ''; const checkboxLabel = document.createElement('label'); checkboxLabel.className = 'checkbox' + typeClass; checkboxLabel.style.position = 'relative'; checkboxLabel.style.margin = '0'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'js-enabled-checkbox'; checkbox.checked = (localStorage[opts.enabledKey] !== 'false'); const indicator = document.createElement('span'); indicator.className = 'indicator'; indicator.setAttribute('aria-hidden', 'true'); checkboxLabel.appendChild(checkbox); checkboxLabel.appendChild(indicator); const titleSpan = document.createElement('span'); titleSpan.className = 'sf-title'; titleSpan.textContent = opts.label; titleSpan.style.fontWeight = 'normal'; const saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'action js-save-filter'; saveBtn.textContent = 'Save'; saveBtn.style.marginLeft = 'auto'; saveBtn.addEventListener('click', function () { localStorage[opts.storeKey] = textarea.value; localStorage[opts.enabledKey] = checkbox.checked + ''; }); row.appendChild(checkboxLabel); row.appendChild(titleSpan); row.appendChild(saveBtn); div.appendChild(row); div.appendChild(textarea); return div; } // global include container.appendChild(makeFilterSection({ label: 'Global', className: 'global-filter', value: prevGlobal, enabledKey: globalKey + '-on', storeKey: globalKey, id: 'sf-global-include', type: 'include' })); // fandom include if (fandomKey) { container.appendChild(makeFilterSection({ label: 'Fandom', className: 'fandom-filter', value: prevFandom, enabledKey: fandomKey + '-on', storeKey: fandomKey, id: 'sf-fandom-include', type: 'include' })); } // global exclude container.appendChild(makeFilterSection({ label: 'Global', className: 'exclude-global-filter', value: prevExcludeGlobal, enabledKey: excludeGlobalKey + '-on', storeKey: excludeGlobalKey, id: 'sf-global-exclude', type: 'exclude' })); // fandom exclude if (excludeFandomKey) { container.appendChild(makeFilterSection({ label: 'Fandom', className: 'exclude-fandom-filter', value: prevExcludeFandom, enabledKey: excludeFandomKey + '-on', storeKey: excludeFandomKey, id: 'sf-fandom-exclude', type: 'exclude' })); } const collapser = document.createElement('dt'); collapser.className = 'saved-filters-collapser filter-toggle collapsed'; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'expander'; btn.setAttribute('aria-expanded', 'false'); btn.setAttribute('aria-controls', 'saved_filters_panel'); btn.textContent = 'Saved Filters'; collapser.appendChild(btn); container.id = 'saved_filters_panel'; container.classList.add('expandable', 'hidden'); if (dt && dt.parentNode) { dt.parentNode.insertBefore(collapser, dt); dt.parentNode.insertBefore(container, dt); } btn.addEventListener('click', function () { const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', expanded ? 'false' : 'true'); collapser.classList.toggle('expanded', !expanded); collapser.classList.toggle('collapsed', expanded); container.classList.toggle('hidden', expanded); }); function insertPrevSearch() { const heading = works.querySelector('.heading'); if (!heading) return; const url = decodeURIComponent(window.location.href); const m = url.match(/work_search_query=([^&]*)/); if (!m) return; let ps = m[1].replace(/\+/g, ' '); let html = 'Your filter was: '; html += ps + '.'; const div = document.createElement('div'); div.className = 'prev-search'; div.innerHTML = html; heading.insertAdjacentElement('afterend', div); } insertPrevSearch(); function parseTags(str) { if (!str) return []; return str.split(/[\n;,]+/).map(s => s.trim()).filter(Boolean); } function quoted(tag) { if (!tag) return ''; tag = tag.trim(); if (/^".*"$/.test(tag)) return tag; return `"${tag.replace(/^"+|"+$/g, '')}"`; } function assembleFilters() { let val = search.value || ''; let includeFilters = []; let excludeFilters = []; container.querySelectorAll('.ao3-saved-filters-section').forEach(section => { const textarea = section.querySelector('textarea'); const enabled = section.querySelector('.js-enabled-checkbox').checked; const className = section.className; if (!enabled || !textarea.value.trim()) return; if (className.includes('exclude-global-filter')) { excludeFilters = excludeFilters.concat(parseTags(textarea.value)); } else if (className.includes('exclude-fandom-filter')) { excludeFilters = excludeFilters.concat(parseTags(textarea.value)); } else if (className.includes('global-filter')) { includeFilters = includeFilters.concat(parseTags(textarea.value)); } else if (className.includes('fandom-filter')) { includeFilters = includeFilters.concat(parseTags(textarea.value)); } }); // include if (includeFilters.length) { includeFilters.forEach(tag => { let qtag = quoted(tag); if (qtag && !val.includes(qtag)) val += ' ' + qtag; }); } // exclude if (excludeFilters.length) { excludeFilters.forEach(tag => { let qtag = quoted(tag); if (qtag && !val.includes('-' + qtag)) val += ' -' + qtag; }); } return val.trim(); } form.addEventListener('submit', function (e) { const savedFiltersPanel = document.getElementById('saved_filters_panel'); const isExpanded = !savedFiltersPanel.classList.contains('hidden'); if (isExpanded) { realSearch.value = assembleFilters(); } else { realSearch.value = search.value; } }); const actionsDd = document.querySelector('dd.submit.actions'); if (actionsDd) { const sortBtn = actionsDd.querySelector('input[type="submit"][value="Sort and Filter"]'); if (sortBtn) { const filterBtn = document.createElement('input'); filterBtn.type = 'button'; filterBtn.value = '🔍 Apply Saved Filters'; filterBtn.className = sortBtn.className; filterBtn.name = ''; filterBtn.classList.add('ao3-filter-shortcut-btn'); actionsDd.insertBefore(filterBtn, sortBtn); filterBtn.addEventListener('click', function() { realSearch.value = assembleFilters(); form.submit(); }); } } document.querySelectorAll('input[type="submit"][value="Sort and Filter"]').forEach(btn => { btn.value = '🔍 Sort and Filter'; }); })();