您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allow more flexible muting without resorting to a site skin
// ==UserScript== // @name [AO3] Mute Selectively // @namespace http://tampermonkey.net/ // @version 2.0-beta1 // @description Allow more flexible muting without resorting to a site skin // @license MIT // @match *://*.archiveofourown.org/* // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // ==/UserScript== // Many thanks to a certain other nonnie const standardSize = { titleFont: 30, headerFont: 20, tabFont: 19, refreshFont: 15, actionFont: 14, menuWidth: 600, menuHeight: 600 } const mobileSize = { titleFont: 24, headerFont: 18, tabFont: 14, refreshFont: 13, actionFont: 12, menuWidth: 390, menuHeight: 500 } var fontSizing = {} var commentMute = [] function getMenuRules(darkmode) { var rules = {}; rules['.AO3-MS-toggle'] = { 'color': darkmode? '#f1f1f1' : '#1f1f1f', 'background-color': darkmode? '#1f1f1f' : '#f1f1f1', 'padding': '1px 4px', 'margin': '0px 0px 0px 4px', 'display': 'inline-block', 'border': '1px solid ' + (darkmode? '#f1f1f1' : '#1f1f1f'), }; rules['.AO3-MS-toggle a'] = { 'text-decoration': 'none', 'border': (darkmode? '#f1f1f1' : '#1f1f1f') + '!important', 'color': (darkmode? '#f1f1f1' : '#1f1f1f') + '!important', }; rules['.AO3-MS-usermenu'] = { 'position': 'absolute', 'display': 'inline-block', 'color': darkmode? '#f1f1f1' : '#1f1f1f', 'background-color': darkmode? '#1f1f1f' : '#f1f1f1', 'margin': '0px 0px 0px 4px', 'border-style': 'solid', 'border-color': darkmode? '#f1f1f1 !important' : '#1f1f1f !important', 'z-index': '100', }; rules['.AO3-MS-usermenu-item'] = { 'display': 'block', 'position': 'relative', 'padding': '3px 5px 2px 5px', }; rules['.AO3-MS-usermenu-item-selected'] = { 'background-color': darkmode? '#3f3f3f' : '#d1d1d1', }; rules['.AO3-MS-usermenu-item a'] = { 'color': darkmode? '#f1f1f1' : '#1f1f1f', 'text-decoration': 'none', 'display': 'inline-block', 'width': '100%', 'border': darkmode? '#f1f1f1' : '#1f1f1f', }; rules['.AO3-MS-usermenu-item:hover'] = { 'background-color': darkmode? '#555' : '#ddd', }; rules['.AO3-MS-muted'] = { 'display': 'none !important', }; rules['.AO3-MS-collapseBlurb'] = { 'padding': '20px', } rules['.AO3-MS-mutePhrase'] = { 'margin': '0px 4px', } rules['#AO3-MS-settings'] = { 'color': darkmode? '#f1f1f1' : '#1f1f1f', 'background-color': darkmode? '#1f1f1f' : '#f1f1f1', 'display': 'none', 'flex-flow': 'column', 'position': 'fixed', 'top': '50%', 'left': '50%', 'width': fontSizing.menuWidth + 'px', 'height': fontSizing.menuHeight + 'px', 'margin': '-' + (fontSizing.menuHeight / 2 ) + 'px -' + (fontSizing.menuWidth / 2) + 'px', 'box-shadow': '10px 10px 20px rgba(0, 0, 0, 0.5)', 'z-index': '100', 'border': '1px solid ' + (darkmode? '#f1f1f1' : '#1f1f1f') + '!important', 'font-size': fontSizing.actionFont + 'px', }; rules['#AO3-MS-settings a'] = { 'color': darkmode? '#aaa' : '#666', }; rules['#AO3-MS-settings a:hover'] = { 'color': darkmode? '#ddd' : '#333', }; rules['#AO3-MS-title'] = { 'margin': '10px 0px 0px', 'text-align': 'center', 'font-size': fontSizing.titleFont + 'px', }; rules['#AO3-MS-closeMenu'] = { 'position': 'absolute', 'top': '0px', 'right': '0px', 'margin': '5px', 'width': (fontSizing.actionFont + 10) + 'px', 'height': (fontSizing.actionFont + 10) + 'px', 'font-size': fontSizing.actionFont + 'px', }; rules['#AO3-MS-tablist'] = { 'margin': '10px 0px', 'text-align': 'center', 'border-bottom': '1px solid ' + (darkmode? '#f1f1f1' : '#1f1f1f'), }; rules['.AO3-MS-tab'] = { 'display': 'inline-block', 'padding': '5px', 'height': fontSizing.tabFont + 'px', 'background-color': darkmode? '#2f2f2f' : '#eee', 'border': '1px solid ' + (darkmode? '#e2e2e2' : '#2e2e2e') + '!important', 'box-size': 'border-box', }; rules['#AO3-MS-viewport'] = { 'display': 'flex', 'flex-flow': 'column', 'flex': '1 1 auto', 'bottom': '0px', }; rules['.AO3-MS-view'] = { 'display': 'none', }; rules['.AO3-MS-active.AO3-MS-view'] = { 'display': 'flex', 'flex-flow': 'column', 'height': '100%', 'bottom': '0px', }; rules['.AO3-MS-active.AO3-MS-tab'] = { 'background': darkmode? '#555' : '#ccc', 'color': (darkmode? '#ddd' : '#333') + '!important', }; rules['.AO3-MS-header'] = { 'position': 'inline-block', 'left': '0px', 'right': '0px', 'top': '0px', 'margin': '5px 10px', 'font-size': fontSizing.headerFont + 'px', 'text-align': 'center', }; rules['.AO3-MS-sortbar'] = { 'position': 'inline-block', 'left': '0px', 'right': '0px', 'top': '0px', 'margin': '5px 10px', 'text-align': 'center', }; rules['.AO3-MS-sortbar a'] = { 'margin': '0px 5px', 'text-decoration': 'none', }; rules['.AO3-MS-list'] = { 'flex': '1 1 auto', 'height': '100px', 'overflow-y': 'scroll', }; rules['.AO3-MS-refresh'] = { 'position': 'inline-block', 'font-size': fontSizing.refreshFont + 'px', 'margin': '0px 10px', 'text-align': 'center', }; rules['#AO3-MS-checkboxes'] = { 'flex': '1 1 auto', 'height': '100px', 'padding': '10px', 'overflow-y': 'scroll', }; rules['.AO3-MS-padding'] = { 'display': 'flex', 'flex-flow': 'column', 'flex': '1 1 auto', 'padding': '10px', }; rules['.AO3-MS-textarea, .AO3-MS-textarea:focus'] = { 'position': 'inline-block', 'left': '0px', 'width': '100%', 'display': 'flex', 'flex-flow': 'column', 'padding': '10px', 'height': '100%', 'resize': 'none', 'box-sizing': 'border-box', 'background-color': darkmode? '#3e3e3e' : '#e3e3e3', 'color': darkmode? '#f1f1f1' : '#1f1f1f', }; rules['#AO3-MS-viewport input[type="button"]'] = { 'display': 'inline-block', 'flex': '0 1 auto', 'bottom': '0px', 'margin': '5px 0px 0px 0px', 'font-size': fontSizing.refreshFont + 'px', 'box-shadow': '2px 1px 2px gray', 'background-color': darkmode? '#3e3e3e' : '#e3e3e3', 'color': darkmode? '#f1f1f1' : '#1f1f1f', }; rules['#AO3-MS-viewport input[type="button"]:active'] = { 'box-shadow': '0 0 0 ' + darkmode? '#333' : '#eee', }; rules['#AO3-MS-css-reset,#AO3-MS-css-save'] = { 'width': '100%', }; rules['#AO3-MS-import-button'] = { 'width': '100%', }; rules['.AO3-MS-action'] = { 'font-size': fontSizing.actionFont + 'px', 'margin-left': '5px', 'color': darkmode? '#7f7feb' : '#4d4dff !important', }; rules['.AO3-MS-action:hover'] = { 'font-size': fontSizing.actionFont + 'px', 'margin-left': '5px', 'color': darkmode? '#a5a5fa' : '#27279c !important', }; rules['.AO3-MS-action:before'] = { 'content': '"("', }; rules['.AO3-MS-action:after'] = { 'content': '")"', }; rules['.AO3-MS-item'] = { 'padding': '10px', }; rules['.AO3-MS-item a'] = { 'border': darkmode? '#333' : '#eee', }; rules['.AO3-MS-filter:before'] = { 'content': '\'"\'', }; rules['.AO3-MS-filter:after'] = { 'content': '\'"\'', }; return rules; } function addMuteRules(muteByStyle) { var rules = {}; rules[muteByStyle] = { 'display': 'none !important', }; return rules; } function makeStyleCode(rules) { var contents = ""; for (var descriptor in rules) { if (! rules.hasOwnProperty(descriptor)) continue; contents += descriptor + " {\n"; for (var property in rules[descriptor]) { if (! rules[descriptor].hasOwnProperty(property)) continue; contents += " " + property + ": " + rules[descriptor][property] + ";\n"; } contents += "}\n"; } return contents; } function makeStyleSheet(str,identifier) { if (document.querySelectorAll('#' + identifier).length) { document.querySelector('#' + identifier).remove() } var sheet = document.createElement('style'); sheet.innerHTML = str; sheet.id = identifier document.body.appendChild(sheet); } async function changeReason(identifier,index,reason) { let temp = await GM.getValue(identifier) temp[index][3] = reason await GM.setValue(identifier,temp) let affectedReasons = document.querySelectorAll('.AO3-MS-collapsed-' + temp[index][0] + ' .AO3-MS-mutePhraseReason') for (const r of affectedReasons) { if (!reason) { r.innerText = '' } else { r.innerText = ' (' + reason + ')' } } let refresh = refreshFilterList(identifier) refresh(); } function makeBlurbFilterItem(identifier,index,value,refresh) { let element = document.createElement("div"); element.className = "AO3-MS-item"; let filter = document.createElement("span"); filter.className = "AO3-MS-filter"; let aTitle = document.createElement('a'); aTitle.href = 'https://archiveofourown.org/works/' + value[0].split('work-')[1] aTitle.appendChild(document.createTextNode(value[1])); element.appendChild(aTitle) element.appendChild(document.createTextNode(' by ')) let names = value[2].split(', ') for (let i = 0; i < names.length; i++) { let aName = document.createElement('a') let linkName = names[i].includes(' (')? names[i].split(' (')[1].split(')')[0] : names[i] aName.href = 'https://archiveofourown.org/users/' + linkName aName.appendChild(document.createTextNode(names[i])) element.appendChild(aName) if (i < names.length - 1) { element.appendChild(document.createTextNode(', ')) } } let remove = makeActionLink("unmute", async function() { await delVal(identifier,value[0]) await refresh(); }); element.appendChild(remove) let reason = document.createTextNode(' Reason: ' + value[3]) element.appendChild(reason) let editReason = makeActionLink('edit reason', async function () { let newReason = prompt("Reason for muting " + value[1] + ":", value[3]); if (newReason === null) { return null; } await changeReason(identifier,index,newReason) await refresh(); }); element.appendChild(editReason) return element; } function makeUserFilterItem(identifier, index, value, refresh) { let element = document.createElement("div"); element.className = "AO3-MS-item"; let filter = document.createElement("span"); filter.className = "AO3-MS-filter"; let id = document.createTextNode('(UserID ' + value[0].split('user-')[1] + ') ') let aName = document.createElement('a'); aName.href = 'https://archiveofourown.org/users/' + value[2] aName.appendChild(document.createTextNode(value[2])); let remove = document.createElement('a') remove.className = "AO3-MS-action"; remove.href = "javascript:void(0)"; remove.appendChild(document.createTextNode('edit')); remove.onclick = async function() { await toggleUser(element,remove,value[0],value[2]) }; let reason = document.createTextNode(' Reason: ' + value[3]) let editReason = makeActionLink('edit reason', async function () { let newReason = prompt("Reason for muting " + value[2] + ":", value[3]); if (newReason === null) { return null; } await changeReason(identifier,index,newReason) await refresh(); }); element.appendChild(id) element.appendChild(aName) element.appendChild(remove) element.appendChild(reason) element.appendChild(editReason) return element; } function makeTagFilterItem(identifier,index,value,refresh) { let element = document.createElement("div"); element.className = "AO3-MS-item"; let filter = document.createElement("span"); filter.className = "AO3-MS-filter"; let tagName = document.createElement('a'); tagName.href = value[1] tagName.appendChild(document.createTextNode(value[2])); element.appendChild(tagName) let remove = makeActionLink("unmute", async function() { await delVal(identifier,value[0]) await refresh(); }); element.appendChild(remove) let reason = document.createTextNode(' Reason: ' + value[3]) element.appendChild(reason) let editReason = makeActionLink('edit reason', async function () { let newReason = prompt("Reason for muting " + value[2] + ":", value[3]); if (newReason === null) { return null; } await changeReason(identifier,index,newReason) await refresh(); }); element.appendChild(editReason) return element; } function refreshFilterList(identifier) { let refresh = async function() { // Load the array first to prevent visual glitches. let array = await GM.getValue(identifier,[]), list = document.querySelector('#AO3-MS-list-' + identifier) while (list.firstChild !== null) { list.removeChild(list.firstChild); } if (identifier === 'blurblist') { for (let i = 0; i < array.length; ++i) { list.appendChild(makeBlurbFilterItem(identifier, i, array[i], refresh)); } } else if (identifier === 'userlist') { for (let i = 0; i < array.length; ++i) { list.appendChild(makeUserFilterItem(identifier, i, array[i], refresh)); } } else if (identifier === 'taglist') { for (let i = 0; i < array.length; ++i) { list.appendChild(makeTagFilterItem(identifier, i, array[i], refresh)); } } }; return refresh; } async function sortList(listType, sortBy, reverse) { let list = await GM.getValue(listType,[]); if (!list.length) return; let i = 2; // sortBy === 'Name' if (sortBy === 'Title') i = 1; if (sortBy === 'Date') i = 4; if (sortBy === 'id') { list.sort(function(a, b) { let x = a[0].split('-')[1]*1; let y = b[0].split('-')[1]*1; return x - y; }); } else if (sortBy === 'Date') { list.sort(function(a, b) { let x = (a[i]? a[i]: 0); let y = (b[i]? b[i]: 0); return x - y; }); }else { list.sort(function(a, b) { let x = a[i].toLowerCase(); let y = b[i].toLowerCase(); if (x < y) return -1; if (y < x) return 1; return 0; }); } if (reverse) list.reverse(); await GM.setValue(listType, list); let refresh = refreshFilterList(listType); refresh(); } function makeSortButton(container, listType, title) { let a = document.createElement('a'); a.href = "javascript:void(0)"; a.appendChild(document.createTextNode(title + '↕')); a.onclick = function() { let reverse = a.innerText.includes('↓') sortList(listType, title, reverse) a.innerText = a.innerText.slice(0, -1) + (reverse? '↑' : '↓') } container.appendChild(a) } function makeFilterList(container, identifier, title) { let header = document.createElement("div"); header.className = "AO3-MS-header"; header.appendChild(document.createTextNode(title)); let sortBar = document.createElement("div"); sortBar.className = "AO3-MS-sortbar"; if (identifier === 'blurblist') { makeSortButton(sortBar, identifier, "Title") makeSortButton(sortBar, identifier, "Author") } else { makeSortButton(sortBar, identifier, "Name") } makeSortButton(sortBar, identifier, "Date") let list = document.createElement("div"); list.id = "AO3-MS-list-" + identifier; list.className = "AO3-MS-list"; let refresh = refreshFilterList(identifier) refresh(); container.appendChild(header); container.appendChild(sortBar); container.appendChild(list); return refresh; } function formatListNameForId(val) { val = String(val).charAt(0).toUpperCase() + String(val).slice(1) return val.split('list')[0] + 's' } function makeMutingTab(viewport, listType, header) { // Create the tab showing blocked entries let mutelist = document.createElement('div'); mutelist.id = 'AO3-MS-muted' + formatListNameForId(listType); viewport.appendChild(mutelist); let listRefresh = makeFilterList(mutelist, listType, header); return async function() { await listRefresh(); }; } async function addCheckbox(container, identifier, caption, hover) { var item = document.createElement("div"); var checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = "AO3-MS-settings-" + identifier; checkbox.checked = await GM.getValue(identifier, false); checkbox.onclick = async function() { await GM.setValue(identifier, checkbox.checked); }; var label = document.createElement("label"); label.appendChild(document.createTextNode(caption)); label.setAttribute('for', "AO3-MS-settings-" + identifier); label.setAttribute('title', hover); item.appendChild(checkbox); item.appendChild(label); container.appendChild(item); var refresh = async function() { checkbox.checked = await GM.getValue(identifier, false); }; return refresh; } async function makeFilterTab(viewport) { // Create the tab with the general settings. var general = document.createElement("div"); general.id = "AO3-MS-general"; viewport.appendChild(general); var header = document.createElement("div"); header.className = "AO3-MS-header"; header.appendChild(document.createTextNode("Filter Settings")); general.appendChild(header); var refresh = document.createElement("div"); refresh.className = "AO3-MS-refresh"; refresh.appendChild(document.createTextNode("Refresh the page to make new extension settings take effect.")); general.appendChild(refresh); var checkboxes = document.createElement("div"); checkboxes.id = "AO3-MS-checkboxes"; general.appendChild(checkboxes); var refreshList = []; refreshList.push( await addCheckbox(checkboxes, "mute", "Remove 'Hidden content' messages (aka total mute).", "When unchecked, the content vanishes without trace for works and series.\n"+ "When checked, it will display a message to indicate a work/series has been hidden.\n"+ "Note that this means you'll only be able to unmute items from the menu.\n"+ "Bookmarks and comments by a muted user will not have hidden content indicators.")); refreshList.push( await addCheckbox(checkboxes, "addReason", "Ask me for a reason whenever something/someone is muted.", "When unchecked, it will mute in one click and leave the reason blank.\n" + "When checked, it will prompt for a reason before the mute goes through.")); refreshList.push( await addCheckbox(checkboxes, "showName", "Show name/title in hidden content message for works and series.", "If there is a hidden content message, it defaults to a generic 'Hidden content' message.\n"+ "If you check this box, then it will display the name of the user who posted the content.\n"+ "If the specific work/series was muted, it will also display the title.")); refreshList.push( await addCheckbox(checkboxes, "showReason", "Show reason for muting in hidden content message for works and series.", "If there is a hidden content message, it defaults to a generic 'Hidden content' message.\n"+ "If this box is checked, it will display the reason you entered for muting the content.")); refreshList.push( await addCheckbox(checkboxes, "showTagToggles", "Show tag toggles for all blurbs in works listings.", "If left unchecked, will only show toggles on individual works and page headings. \n"+ "May not be recommended for older/slower mobile devices.\n"+ "By default, tag toggles will show at the header of every tag page and tag work listing, and on individual works.")); refreshList.push( await addCheckbox(checkboxes, "forceMobile", "Display in mobile-friendly view even on larger screens.", "Displays a smaller window with smaller font.")); refreshList.push( await addCheckbox(checkboxes, "darkmode", "Use darkmode theme.", "You can also use the CSS tab to set up a custom style.")); return async function() { for (var i = 0; i < refreshList.length; ++i) { await refreshList[i](); } }; } // Unicode fix from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22 function utf8_to_b64(str) { return window.btoa(unescape(encodeURIComponent(str))); } function b64_to_utf8(str) { return decodeURIComponent(escape(window.atob(str))); } async function getFilterSettings() { var settings = {}; settings.userlist = await GM.getValue('userlist'); settings.blurblist = await GM.getValue('blurblist'); settings.taglist = await GM.getValue('taglist'); var booleanNames = [ 'addReason', 'mute', 'showName', 'showReason', 'forceMobile', 'showTagToggles', 'darkmode', ]; for (var i = 0; i < booleanNames.length; ++i) { var name = booleanNames[i]; settings[name] = await GM.getValue(name, false); } settings['custom-css'] = await loadCustomCSS(); return settings; } async function saveFilterSettings(settings) { await GM.setValue('userlist',settings.userlist) await GM.setValue('blurblist',settings.blurblist) var booleanNames = [ 'addReason', 'mute', 'showName', 'showReason', 'forceMobile', 'showTagToggles', 'darkmode', ]; for (let i = 0; i < booleanNames.length; ++i) { var name = booleanNames[i]; if (settings.hasOwnProperty(name)) { GM.setValue(name, settings[name]); } } if (settings.hasOwnProperty('custom-css')) { GM.setValue('custom-css', settings['custom-css']); } } async function updateExport() { var exportText = document.getElementById("AO3-MS-export"); var settings = await getFilterSettings(); exportText.value = utf8_to_b64(JSON.stringify(settings)); exportText.select(); exportText.focus(); } async function doImport() { var importText = document.getElementById("AO3-MS-import"); try { var settings = JSON.parse(b64_to_utf8(importText.value)); await saveFilterSettings(settings); updateExport(); } catch (e) { alert("Invalid data."); } } function makeImportTab(viewport) { // Make tab to import settings & filters var contMain = document.createElement("div"); contMain.id = "AO3-MS-importMain"; viewport.appendChild(contMain); var header = document.createElement("div"); header.className = "AO3-MS-header"; header.appendChild(document.createTextNode("Import settings & filters")); contMain.appendChild(header); var refresh = document.createElement("div"); refresh.className = "AO3-MS-refresh"; refresh.appendChild(document.createTextNode("Refresh the page to make new extension settings take effect.")); contMain.appendChild(refresh); var padder = document.createElement("div") padder.className = "AO3-MS-padding" contMain.appendChild(padder) var importText = document.createElement("textarea"); importText.id = "AO3-MS-import"; importText.className = "AO3-MS-textarea"; padder.appendChild(importText); var importButton = document.createElement("input"); importButton.id = "AO3-MS-import-button"; importButton.type = "button"; importButton.value = "Import"; importButton.onclick = doImport; padder.appendChild(importButton); } function makeExportTab(viewport) { // Create the tab with save and export boxes. var contMain = document.createElement("div"); contMain.id = "AO3-MS-exportMain"; viewport.appendChild(contMain); var exportHeader = document.createElement("div"); exportHeader.className = "AO3-MS-header"; exportHeader.appendChild(document.createTextNode("Export settings & filters")); contMain.appendChild(exportHeader); var padder = document.createElement("div") padder.className = "AO3-MS-padding" contMain.appendChild(padder) var exportText = document.createElement("textarea"); exportText.id = "AO3-MS-export"; exportText.className = "AO3-MS-textarea"; exportText.readOnly = true; padder.appendChild(exportText); return updateExport; } function getDefaultRules() { var rules = {}; var blockedBackground = 'none'; var blockedColor = 'none'; rules['.AO3-MS-collapseBlurb'] = { 'background-color': blockedBackground, 'color': blockedColor, 'padding': '20px', } return rules; } async function loadCustomCSS() { var result = await GM.getValue('custom-css', null); if (result === null) { result = makeStyleCode(getDefaultRules()); } return result; } async function updateCSS() { var cssText = document.getElementById("AO3-MS-css"); cssText.value = await loadCustomCSS(); } async function saveCSS() { var cssText = document.getElementById("AO3-MS-css"); await GM.setValue('custom-css', cssText.value); } async function resetCSS() { await GM.deleteValue('custom-css'); await updateCSS(); } function makeColorsTab(viewport) { // Create the tab with the custom CSS editor. var colors = document.createElement("div"); colors.id = "AO3-MS-colors"; viewport.appendChild(colors); var cssHeader = document.createElement("div"); cssHeader.className = "AO3-MS-header"; cssHeader.appendChild(document.createTextNode("Custom styling")); colors.appendChild(cssHeader); var refresh = document.createElement("div"); refresh.className = "AO3-MS-refresh"; refresh.appendChild(document.createTextNode("Refresh the page to make new extension settings take effect.")); colors.appendChild(refresh); var padder = document.createElement("div") padder.className = "AO3-MS-padding" colors.appendChild(padder) var css = document.createElement("textarea"); css.id = "AO3-MS-css"; css.className = "AO3-MS-textarea"; padder.appendChild(css); var cssSave = document.createElement("input"); cssSave.id = "AO3-MS-css-save"; cssSave.className = "AO3-MS-css-button"; cssSave.type = "button"; cssSave.value = "Save"; cssSave.onclick = async function () { await GM.setValue('custom-css',css.value) }; padder.appendChild(cssSave); var cssReset = document.createElement("input"); cssReset.id = "AO3-MS-css-reset"; cssReset.className = "AO3-MS-css-button"; cssReset.type = "button"; cssReset.value = "Reset to Default"; cssReset.onclick = resetCSS; padder.appendChild(cssReset); css.onchange = saveCSS; return updateCSS; } function toggleSettings() { var settings = document.getElementById("AO3-MS-settings"); if (settings === null) return; if (settings.style.display == "flex") { settings.style.top = "-50%"; settings.style.display = "none"; } else { settings.style.display = "flex"; settings.style.top = "50%"; } } function addTab(container, name, view, active, action) { var classView = "AO3-MS-view"; var classActive = "AO3-MS-active"; var classTab = "AO3-MS-tab"; var link = document.createElement("a"); link.className = classTab; link.href = "javascript:void(0)"; var text = document.createTextNode(name); link.appendChild(text); container.appendChild(link); var viewElement = document.getElementById(view); viewElement.className = classView; link.onclick = function(e) { var views = document.getElementsByClassName(classView); for (let i = 0; i < views.length; ++i) { views[i].className = classView; } var tabs = document.getElementsByClassName(classTab); for (let i = 0; i < tabs.length; ++i) { tabs[i].className = classTab; } viewElement.className += " " + classActive; link.className += " " + classActive; if (action !== undefined && action !== null) { action(); } }; if (active) { viewElement.className += " " + classActive; link.className += " " + classActive; } } function makeActionLink(text, action) { var link = document.createElement("a"); link.href = "javascript:void(0)"; link.className = "AO3-MS-action"; link.onclick = action; link.appendChild(document.createTextNode(text)); return link; } async function makeSettings() { // Create the settings window. var settings = document.createElement("div"); settings.id = "AO3-MS-settings"; var header = document.createElement("div"); header.id = "AO3-MS-title"; header.appendChild(document.createTextNode("Selective Muting")); settings.appendChild(header); var closeButton = document.createElement("input"); closeButton.id = "AO3-MS-closeMenu"; closeButton.type = "button"; closeButton.value = "X"; closeButton.onclick = function () { toggleSettings() }; header.appendChild(closeButton); var tabs = document.createElement("div"); tabs.id = "AO3-MS-tablist"; settings.appendChild(tabs); var viewport = document.createElement("div"); viewport.id = "AO3-MS-viewport"; settings.appendChild(viewport); document.body.appendChild(settings); // Create the tabs and add them. var blurblistRefresh = await makeMutingTab(viewport, 'blurblist', 'Works & Series'); var userlistRefresh = await makeMutingTab(viewport, 'userlist', 'Users'); var taglistRefresh = await makeMutingTab(viewport, 'taglist', 'Tags'); var generalRefresh = await makeFilterTab(viewport); var colorsRefresh = await makeColorsTab(viewport); var importRefresh = await makeImportTab(viewport); var exportRefresh = await makeExportTab(viewport); addTab(tabs, "Works/Series", "AO3-MS-mutedBlurbs", true, blurblistRefresh); addTab(tabs, "Users", "AO3-MS-mutedUsers", false, userlistRefresh); addTab(tabs, "Tags", "AO3-MS-mutedTags", false, taglistRefresh); addTab(tabs, "Settings", "AO3-MS-general", false, generalRefresh); addTab(tabs, "CSS", "AO3-MS-colors", false, colorsRefresh); addTab(tabs, "Import", "AO3-MS-importMain", false, importRefresh); addTab(tabs, "Export", "AO3-MS-exportMain", false, exportRefresh); } async function makeCollapseBlurb(blurb,blurbInfo) { let container = document.createElement('li') container.classList.add('AO3-MS-collapseBlurb') container.classList.add('blurb') container.classList.add('AO3-MS-collapsed-' + blurbInfo[0]) container.classList.add(blurb.id) blurb.parentElement.insertBefore(container,blurb) await addToCollapseBlurb(container, blurbInfo, blurb) } async function addToCollapseBlurb(container, blurbInfo, blurb) { let mutePhrase = document.createElement('span') mutePhrase.classList.add('AO3-MS-mutePhrase') mutePhrase.innerText = 'Hidden content' let reason = document.createElement('span') reason.classList.add('AO3-MS-mutePhraseReason') if (blurbInfo[1].split('/tags/').length > 1) { if (blurb) { makeTogglebox(container, '(show)', container) // Add a * or something in front to fast unmute? Ugh } if (container.innerText.includes('because of tag:')) { mutePhrase.innerHTML = ', <a href="' + blurbInfo[1] + '">' + blurbInfo[2] + '</a>' } else if (await GM.getValue('showName')) { mutePhrase.innerHTML += ' because of tag: <a href="' + blurbInfo[1] + '">' + blurbInfo[2] + '</a>' } if (await GM.getValue('showReason') && blurbInfo[3]) { reason.innerText += ' (' + blurbInfo[3] + ')' } container.appendChild(mutePhrase) container.appendChild(reason) } else if (blurbInfo[0].split('work').length > 1 || blurbInfo[0].split('series').length > 1) { if (blurb) makeTogglebox(blurb,'m',container) if (await GM.getValue('showName')) { mutePhrase.innerText += ': ' + blurbInfo[1] + ' by ' + blurbInfo[2] } if (await GM.getValue('showReason') && blurbInfo[3]) { reason.innerText += ' (' + blurbInfo[3] + ')' } container.appendChild(mutePhrase) container.appendChild(reason) } else if (!blurb?.classList?.contains('bookmark')) { if (await GM.getValue('showName')) { mutePhrase.innerText += ' by ' + blurbInfo[2] } if (await GM.getValue('showReason') && blurbInfo[3]) { reason.innerText += ' (' + blurbInfo[3] + ')' } container.appendChild(mutePhrase) container.appendChild(reason) if (blurb) makeTogglebox(blurb,'X',container) } } function getBlurbs(arrayName,value) { let blurbuser = arrayName.split('list')[0], toHide = '' if (blurbuser === 'blurb') { toHide = '.' + value[0] } else if (blurbuser === 'user') { value[1].split(',').forEach(w => { toHide += ',' + w + '.' + value[0] }) toHide = toHide.substring(1) } else if (blurbuser === 'tag') { toHide = '.blurb:has(a[href$="' + value[1] + '"])' } if (!toHide) return null return document.querySelectorAll(toHide) } async function addVal(arrayName,value) { // Add the entry to the saved array let temp = await GM.getValue(arrayName,[]) for (let i = 0; i < temp.length; ++i) { if (temp[i][0] === value[0]) { return null; } } let reason = '' if (value[3] == 'undefined' && await GM.getValue('addReason',false)) { let muteObj = value[1] if (arrayName === 'userlist') { muteObj = value[2] } reason = prompt("Reason for muting " + muteObj + ":", ''); if (reason === null) { return ''; } } if (value[3] == undefined) value.push(reason); value.push(Date.now()); temp.push(value); await GM.setValue(arrayName,temp) // Collapse/mute all works on the page that meet the criteria let mute = await GM.getValue('mute', false) let blurbs = [] if (arrayName === 'taglist') { blurbs = document.querySelectorAll('.blurb:has(a[href$="' + value[1] + '"])') } else { blurbs = getBlurbs(arrayName,value) } for (let blurb of blurbs) { if (!mute && (arrayName === 'blurblist' || blurb.classList.contains('work') || blurb.classList.contains('series')) ) { const collapseBlurb = document.querySelector('.' + blurb.id) if (!collapseBlurb) { await makeCollapseBlurb(blurb, value) } else { await addToCollapseBlurb(collapseBlurb, value) } } blurb.classList.add('AO3-MS-muted') } let refresh = refreshFilterList(arrayName) refresh() return true } async function delVal(arrayName,key) { // Remove the entry from the saved array let temp = await GM.getValue(arrayName,false) let value = []; let i = 0; while (i < temp.length) { if (temp[i][0] === key) { value = temp[i]; temp.splice(i, 1); } else { ++i; } } await GM.setValue(arrayName,temp) // Uncollapse/unmute all works on the page that meet the criteria let mute = await GM.getValue('mute',false) let blurbs = getBlurbs(arrayName,value) if (!mute) { for (let c of document.querySelectorAll('.AO3-MS-collapsed-' + key)) { c.remove(); } } for (let blurb of blurbs) { blurb.classList.remove('AO3-MS-muted') } if(arrayName === 'userlist') { let delMe = [] for (let i = 0; i < commentMute.length; i++) { if (commentMute[i].includes(value[0])) { let delMe = commentMute.splice(i,1) delMe ? makeStyleSheet(makeStyleCode(addMuteRules(commentMute.join(','))),'AO3-MS-muteByStyle') : null } } setInitialMute('blurblist') } let refresh = refreshFilterList(arrayName) refresh() } async function getVal(arrayName, key, localArray) { let temp = [] if (localArray) { temp = arrayName } else { temp = await GM.getValue(arrayName, []); } if (!temp) return null; for (let i = 0; i < temp.length; ++i) { if (temp[i][0] === key) { return temp[i] } } return null; } function getBlurbUserID(blurb) { for (let e of blurb.classList) { if (e.includes('user-')) { return e; } } if (document.querySelector('#main.dashboard') && document.querySelector('#subscription_subscribable_type')?.value === 'User') { return document.querySelector('#subscription_subscribable_id')?.value } return null; } async function toggleBlurb(blurb,toggleboxA) { let blurbInfo = []; if (blurb) { for (let e of blurb.classList) { if (e.includes('work-') || e.includes('series-')) { blurbInfo.push(e) } } // record title blurbInfo.push(blurb.querySelectorAll('.heading a')[1].textContent) // record creators if (!blurb.querySelectorAll('h4.heading [rel="author"]').length) { blurbInfo.push('(Anonymous creator)') } else { let authorNames = '' blurb.querySelectorAll('h4.heading [rel="author"]').forEach (a => { authorNames += ', ' + a.innerText }) authorNames = authorNames.substring(2) blurbInfo.push(authorNames) } } else if (document.querySelectorAll('h2.title.heading').length) { blurbInfo.push(getIdFromWorkPage()) blurbInfo.push(document.querySelector('h2.title.heading').textContent.substring(1).trim()) if (!document.querySelectorAll('a[rel="author"]').length) { blurbInfo.push('(Anonymous creator)') } else { blurbInfo.push(document.querySelector('a[rel="author"]').innerText) } } else if (document.querySelectorAll('.series-show h2.heading').length) { blurbInfo.push(getIdFromSeriesPage()) blurbInfo.push(document.querySelector('.series-show h2.heading').textContent.substring(1).trim()) if (!document.querySelectorAll('a[rel="author"]').length) { blurbInfo.push('(Anonymous creator)') } else { blurbInfo.push(document.querySelector('a[rel="author"]').innerText) } } if(toggleboxA.textContent == 'M') { await addVal('blurblist',blurbInfo) && !blurb ? toggleboxA.textContent = 'm' : '' // The M->m change only actually matters on title pages; on blurblists M vs m are actually in two different blurbs } else { await delVal('blurblist',blurbInfo[0]) !blurb ? toggleboxA.textContent = 'M' : '' } } async function toggleUser(blurb,togglebox,userID,username) { if (togglebox.parentNode.querySelectorAll('.AO3-MS-usermenu').length){ togglebox.parentNode.querySelector('.AO3-MS-usermenu').remove() return null; } let usermenu = document.createElement('div'); usermenu.id = 'AO3-MS-usermenu-' + userID; usermenu.classList.add('AO3-MS-usermenu') if(!blurb) { // Prevent the menu from being enormous on profile pages usermenu.style.fontSize = fontSizing.refreshFont + 'px' usermenu.style.display = 'block' } let items = [['.work','Works'], ['.work,.series','Works & Series'], ['.comment','Comments'], ['','Mute all'], ['none','Unmute']] const currEntry = await getVal('userlist',userID) let reason = '' async function createItem(item) { let container = document.createElement('span') container.classList.add('AO3-MS-usermenu-item') usermenu.appendChild(container) // check what the current option is, and highlight it if (currEntry) { if (currEntry[1] === item[0]) { container.classList.add('AO3-MS-usermenu-item-selected') } } else if (item[0] === 'none') { container.classList.add('AO3-MS-usermenu-item-selected') } let a = document.createElement('a') a.href = "javascript:void(0)"; a.appendChild(document.createTextNode(item[1])); container.appendChild(a) a.onclick = async function() { if (currEntry) { if (item[0] != currEntry[1]) { reason = currEntry[3] await delVal('userlist',userID) } } if (item[0] != 'none') { let newEntry = [userID,item[0],username,reason]; await addVal('userlist',newEntry); } // remove all menues on the page because things have probably changed let rl = document.querySelectorAll('.AO3-MS-usermenu') for (let ri of rl) { ri.remove() } } item.push(container) } for (let i = 0; i < items.length; i++) { await createItem(items[i]) } togglebox.parentElement.appendChild(usermenu) // Fix if it's likely to get squished -- not great, but eh if (blurb && (await GM.getValue('forceMobile',false) || window.innerWidth < 900)) { usermenu.style.display = 'block' } } function makeTogglebox(blurb, tType, pin) { let togglebox = document.createElement('div'); let toggleboxA = document.createElement('a'); togglebox.classList.add('AO3-MS-toggle'); togglebox.appendChild(toggleboxA); toggleboxA.href = "javascript:void(0)"; toggleboxA.appendChild(document.createTextNode(tType)); if (blurb) { if (tType == 'X') { toggleboxA.onclick = async function() { let username = blurb.querySelector('.heading a[rel="author"]').textContent await toggleUser(blurb,togglebox,getBlurbUserID(blurb),username) }; } else if (tType == '(show)') { if (blurb.className.includes(' work_') || blurb.className.includes(' series_')) { toggleboxA.onclick = function () { let originalBlurbId = blurb.className.split(' work_') if (originalBlurbId.length > 1) { originalBlurbId = 'work_' + originalBlurbId[1].split(' ')[0] } else { originalBlurbId = 'series_' + blurb.className.split(' series_')[1].split(' ')[0] } let originalBlurb = document.querySelector('#' + originalBlurbId) originalBlurb.classList.remove('AO3-MS-muted') blurb.remove() } } } else { toggleboxA.onclick = async function() { await toggleBlurb(blurb,toggleboxA) }; } } else { if (tType == 'X') { toggleboxA.onclick = async function() { let username = document.querySelector('h2.heading').innerText.split(' ')[0] await toggleUser(false,togglebox,getIdFromProfilePage(),username) } } else { toggleboxA.onclick = async function() { await toggleBlurb(false,toggleboxA) }; } } if (tType == 'X') { pin.append(togglebox) } else { pin.prepend(togglebox) } } function addMuteButtonToBlurbs() { const blurbs = document.querySelectorAll('.blurb.work,.blurb.series,.blurb.bookmark'); for (let blurb of blurbs) { let blurbTitle = blurb.querySelector('.heading') makeTogglebox(blurb,'M',blurbTitle) if(blurb.querySelectorAll('h4.heading a').length == 3 && !blurb.classList.contains('bookmark')) { makeTogglebox(blurb,'X',blurbTitle) } } } async function setInitialMute(identifier) { let entries = await GM.getValue(identifier,[]); if (!entries) { return null; } let mute = await GM.getValue('mute',false), blurbs = '' for (const entry of entries) { if (identifier === 'userlist') { let typeToHide = '' if (entry[1].includes('work')) { entry[1].split(',').forEach(w => { typeToHide += ',' + w + '.' + entry[0] }) } else { typeToHide = ',.work.' + entry[0] + ',.series.' + entry[0] } typeToHide = typeToHide.substring(1) typeToHide ? blurbs = document.querySelectorAll(typeToHide) : null } else if (identifier === 'taglist') { blurbs = document.querySelectorAll('.blurb:has(a[href$="' + entry[1] + '"])') } else { blurbs = document.querySelectorAll('.' + entry[0]) } for (let blurb of blurbs) { //if (!blurb.classList.contains('AO3-MS-muted')) { if (!mute) { const collapseBlurb = document.querySelector('.' + blurb.id) if (!collapseBlurb) { await makeCollapseBlurb(blurb,entry) } else { await addToCollapseBlurb(collapseBlurb, entry) } } blurb.classList.add('AO3-MS-muted') //} } } } function getIdFromWorkPage() { if (document.querySelectorAll('li.download a').length > 1) { if (isNaN(document.querySelectorAll('li.download a')[1].href.split('/')[4])) { return null; } else { return 'work-' + document.querySelectorAll('li.download a')[1].href.split('/')[4] } } return null; } function getIdFromSeriesPage() { let parts = window.location.pathname.split('/') for (let i = 0; i < parts.length - 1; i++) { if (parts[i] === 'series' && !isNaN(parts[i+1])) { return 'series-' + parts[i+1] } } return null } function getIdFromProfilePage() { if(document.querySelectorAll('.meta').length) { let parts = document.querySelector('.meta').innerText.split('\n') for (let i = 0; i < parts.length - 1; i++) { if (parts[i] === 'My user ID is:' && !isNaN(parts[i+1])) { return 'user-' + parts[i+1] } } } else if (document.querySelector('#main.dashboard') && document.querySelector('#subscription_subscribable_type')?.value === 'User') { // dashboard, logged in return 'user-' + document.querySelector('#subscription_subscribable_id')?.value } return null } async function toggleTag (tagInfo, toggleboxA) { let entries = await GM.getValue('taglist',[]); let blurbInfo = []; if (toggleboxA.innerText === "M") { toggleboxA.innerText = "m" await addVal('taglist', tagInfo) } else { toggleboxA.innerText = 'M' await delVal('taglist', tagInfo[0]) } } function makeTagTogglebox(tType, pin, tagInfo) { let togglebox = document.createElement('div'); let toggleboxA = document.createElement('a'); togglebox.classList.add('AO3-MS-toggle'); togglebox.appendChild(toggleboxA); toggleboxA.href = "javascript:void(0)"; toggleboxA.appendChild(document.createTextNode(tType)); toggleboxA.onclick = async function() { await toggleTag(tagInfo, toggleboxA) }; pin.append(togglebox) } function makeTagKey(tagLink) { return tagLink.split('/')[2].replaceAll(/[^a-zA-Z0-9 :]/g, '_'); } async function setTagMute() { let parts = window.location.pathname.split('/') let h2 = document.querySelector('h2') let tagInfo = [] tagInfo.push('') if (parts[1] === 'tags' && parts[2]) { if (parts[3]) { // If you are on the tag's works or bookmarks listing if (await GM.getValue('showTagToggles', false)) return; // abort early, this will be taken care of in addTogglesToWorkTags() if (['works', 'bookmarks'].includes(parts[3])) { let a = h2?.querySelector('a') tagInfo.push(a.href.split('.org')[1] + '/works') // tagLink tagInfo.push(a.innerText) // tagName } } else { // If you're directly on the tag page tagInfo.push(window.location.pathname + '/works') // tagLink tagInfo.push(h2?.innerText) // tagName } tagInfo[0] = makeTagKey(tagInfo[1]) let tType = 'M' if (await getVal('taglist', tagInfo[0])) tType = 'm' makeTagTogglebox(tType, h2, tagInfo) } } async function addTogglesToWorkTags() { const tags = document.querySelectorAll('a.tag') const taglist = await GM.getValue('taglist', []) for (const tag of tags) { let tagInfo = [] const tagLink = tag.href.split('.org')[1] const tagKey = makeTagKey(tagLink) tagInfo.push(tagKey) tagInfo.push(tagLink) tagInfo.push(tag.innerText) const tType = await getVal(taglist, tagKey, true) ? 'm' : 'M' const container = tag.parentNode if (container.tagName !== 'LI') { const newContainer = document.createElement('span') tag.after(newContainer) newContainer.appendChild(tag) } makeTagTogglebox(tType, tag.parentNode, tagInfo) } } async function setTitleMute() { let blurbId = '' async function determineMm(blurbId) { if (await getVal('blurblist',blurbId)) { return 'm' } return 'M' } if (document.querySelectorAll('h2.title.heading').length) { // work or chapter page blurbId = getIdFromWorkPage() blurbId ? makeTogglebox(false,await determineMm(blurbId),document.querySelector('h2.title.heading')) : null if (!await GM.getValue('showTagToggles', true)) addTogglesToWorkTags() // only fire here if the setting is off -- otherwise it's already run } else if (document.querySelectorAll('.series-show h2.heading').length) { // series page blurbId = getIdFromSeriesPage() blurbId ? makeTogglebox(false,await determineMm(blurbId),document.querySelector('.series-show h2.heading')) : null } else if (document.querySelector('.profile-show h2.heading') || document.querySelector('.dashboard h2.heading')) { // profile or dashboard blurbId = getIdFromProfilePage() blurbId ? makeTogglebox(false,'X',document.querySelector('h2.heading')) : null } // else if (document.querySelector('#main.dashboard') && document.querySelector('#subscription_subscribable_type')?.value === 'User') { // dashboard, logged in // blurbId = document.querySelector('#subscription_subscribable_id')?.value // makeTogglebox(false, 'X', document.querySelector('.dashboard h2.heading')) // } } function addToNavBar() { let searchNode = document.querySelector('li.search'), li = document.createElement('li'), a = document.createElement('a') a.href = 'javascript:void(0)' a.appendChild(document.createTextNode('Muting')) a.onclick = function() { toggleSettings(); }; li.classList.add('dropdown') li.appendChild(a) searchNode.parentNode.insertBefore(li,searchNode) } (async function() { 'use strict'; const filterSettings = await getFilterSettings(); const darkmode = await GM.getValue('darkmode', false); const useMobile = (window.innerWidth < 900 ? true : false); fontSizing = standardSize if (useMobile || await GM.getValue('forceMobile',false)) {fontSizing = mobileSize} fontSizing.menuWidth > (window.innerWidth - 10) ? fontSizing.menuWidth = window.innerWidth -10 : '' fontSizing.menuHeight > (window.innerHeight -10) ? fontSizing.menuHeight = window.innerHeight -10 : '' addToNavBar(); makeSettings(); var userlist = await GM.getValue('userlist',[]) for (const u of userlist) { if (u[1] === '.comment') { commentMute.push('.comment.' + u[0]) } else if (!u[1]) { commentMute.push('.' + u[0] + ':not(.work,.series)') } } var muteByStyle = commentMute.join(',') makeStyleSheet(makeStyleCode(getMenuRules(darkmode)),'AO3-MS-mainStyle'); makeStyleSheet(filterSettings['custom-css'],'AO3-MS-custom-css'); makeStyleSheet(makeStyleCode(addMuteRules(commentMute.join(','))),'AO3-MS-muteByStyle') await setInitialMute('userlist'); await setInitialMute('blurblist'); await setInitialMute('taglist'); await setTitleMute(); await setTagMute(); if (await GM.getValue('showTagToggles', false)) addTogglesToWorkTags() addMuteButtonToBlurbs(); })();