// ==UserScript== // @name Extend "AO3: Kudosed and seen history" | Export/Import + Standalone Light/Dark mode toggle // @description Add Export/Import history to TXT buttons at the bottom of the page. │ Fix back-navigation not being collapsed. │ Color and rename the confusing Seen/Unseen buttons. │ Enhance the title. │ Fix "Mark as seen on open" triggering on external links. ║ Standalone feature: Light/Dark site skin toggle button. // @author C89sd // @version 1.23 // @match https://archiveofourown.org/* // @grant GM_xmlhttpRequest // @namespace https://greasyfork.org/users/1376767 // @downloadURL none // ==/UserScript== const ENHANCED_SEEN_BUTTON = true; // Seen button is colored and renamed / Immediately mark seen / Blink when navigating back const COLORED_TITLE_LINK = true; // Title becomes a colored link const ENHANCED_MARK_SEEN_ON_OPEN = true; // Enable improved "Mark seen on open" feature with a distinction between SEEN Now and Old SEEN const IGNORE_EXTERNAL_LINKS = true; // Mark as seen when a link is clicked on AO3 but not from other sites (e.g. reddit). If false, 'New SEEN' will tell you if it was a new or old link. const SITE_SKINS = [ "Default", "Reversi" ]; // A fic is marked as seen after loading not when the title is clicked. // Thus the AO3 script requires a manual reload to refresh after a link is clicked: // - Clicked fics are not collpased and when navigating back // - Seen changes made from the other page are not taken into account. // To fix this: // 1. When unloading a tab, store the current fic id and the seen sate. // Update this state every time the seen button is clicked. // Upon back navigation, check this data, try to find the fic link on-screen, and toggle its collapsed status. // Note: This is cheaper than reloading the page. // 2. Intercept clicks on links and manually trigger the 'seen' button for immediate collapse. let currentSeenState = null; // Updated down if (ENHANCED_SEEN_BUTTON) { const isWork = /^(https:\/\/archiveofourown\.org\/works\/\d+)/ let state = {}; window.addEventListener("beforeunload", function (event) { // Note: Doing this in 'unload' caused 'event.persisted' to be always false. const match = window.location.href.match(isWork); if (match) { localStorage.setItem("C89AO3_state", JSON.stringify({"ref": match[1], "state": currentSeenState})); } }); window.addEventListener("pageshow", function (event) { // On page load/back navigation/etc. let data = localStorage.getItem("C89AO3_state"); state = JSON.parse(data ? data : '{}'); }); function injectFlashCSS() { if (!document.querySelector('#flash-styles')) { const style = document.createElement('style'); style.id = 'flash-styles'; // Add an ID to ensure only one style tag is added style.innerHTML = ` .flashFilter { animation: flash-animation 0.5s ease-in-out !important; } @keyframes flash-animation { 0% { filter: brightness(0.5); } 50% { filter: brightness(0.8); } 100% { filter: brightness(1); } } .flashBg { animation: flash-animation2 0.5s ease-in-out !important; } @keyframes flash-animation2 { 0% { background-color: rgba(169, 169, 169, 0.5); } 50% { background-color: rgba(169, 169, 169, 0.2); } 100% { background-color: transparent; } } `; document.head.appendChild(style); } } window.addEventListener('pageshow', (event) => { if (event.persisted) { // If we navigated back, and came from a fic. if (state?.ref && isWork.test(state.ref)) { // Try finding the link of the fic we navigated back from and toggle its parent visibility. const titleLink = document.querySelector(`h4.heading > a[href^="${state.ref}"]`); // Note: ^= because there can be ?foo at the end. if (titleLink) { const article = titleLink.closest('li[role="article"]'); if (article) { let flash = true; if ( state?.state === true && !article.classList.contains('marked-seen')) { article.classList.add('marked-seen'); seen = true; } else if (state?.state === false && article.classList.contains('marked-seen')) { flash = true; article.classList.remove('marked-seen'); } if (flash) { injectFlashCSS(); let fx = article.classList.contains('marked-seen') ? 'flashFilter' : 'flashBg'; article.classList.add(fx); setTimeout(() => { article.classList.remove(fx); }, 500); } } } } } }); } // 2. Intercept all clicks. If a title was clicked, manually dispatch the "seen" button before opening the fic. document.addEventListener('click', function(event) { const titleLink = event.target.closest('h4.heading > a'); if (titleLink) { const article = titleLink.closest('li[role="article"]'); if (article) { const seenButton = article.querySelector('div.kh-toggles>a') if (seenButton) { // Click the seen button (unless the fic is collapsed - that would unmark it!). if (!article.classList.contains('marked-seen')) { seenButton.click() } // Give that "seen" action time to execute before loading the page. event.preventDefault(); setTimeout(function() { window.location.href = titleLink.href; }, 100); } } } }); // GET the preferences form, find the current skin_id, and POST the next skin_id. function getPreferencesForm(user) { // GET the preferences fetch(`https://archiveofourown.org/users/${user}/preferences`, { method: 'GET', headers: { 'Content-Type': 'text/html' } }) .then(response => response.text()) .then(responseText => { const doc = new DOMParser().parseFromString(responseText, 'text/html'); // Extract the authenticity token const authenticity_token = doc.querySelector('input[name="authenticity_token"]')?.value; if (authenticity_token) { // console.log('authenticity_token: ', authenticity_token); // Log the token } else { alert('[userscript:Extend AO3] Error\n[authenticity_token] not found!'); return; } // Find the