您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add Export-Import buttons at the bottom of the page. Rename+color Seen/Unseen buttons and Title. Mark as seen on open from AO3 only.
当前为
// ==UserScript== // @name Extend "AO3: Kudosed and seen history" @Min_ // @description Add Export-Import buttons at the bottom of the page. Rename+color Seen/Unseen buttons and Title. Mark as seen on open from AO3 only. // @author C89sd // @version 1.6 // @match https://archiveofourown.org/* // @namespace https://greasyfork.org/users/1376767 // ==/UserScript== const MARK_SEEN_FROM_AO3_ONLY = true; (function() { 'use strict'; const footer = document.createElement('div'); footer.style.width = '100%'; footer.style.paddingTop = '5px'; footer.style.paddingBottom = '5px'; footer.style.display = 'flex'; footer.style.justifyContent = 'center'; footer.style.gap = '10px'; footer.classList.add('footer'); // Turn title into a link const firstH1 = document.querySelector('h2.title.heading'); var firstH1link = null; if (firstH1) { const title = firstH1.lastChild ? firstH1.lastChild : firstH1; const titleLink = document.createElement('a'); titleLink.href = window.location.href; if (title) { const titleClone = title.cloneNode(true); titleLink.appendChild(titleClone); title.parentNode.replaceChild(titleLink, title); } firstH1link = titleLink; } const BTN_1 = ['button']; const BTN_2 = ['button', 'button--link']; // Create Export Button const exportButton = document.createElement('button'); exportButton.textContent = 'Export'; exportButton.classList.add(...BTN_1); exportButton.addEventListener('click', exportToJson); footer.appendChild(exportButton); // Create Import Button const importButton = document.createElement('button'); importButton.textContent = 'Import'; importButton.classList.add(...BTN_1); // Create hidden file input const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.txt, .json'; fileInput.style.display = 'none'; // Hide the input element // Trigger file input on "Restore" button click importButton.addEventListener('click', () => { fileInput.click(); // Open the file dialog when the button is clicked }); // Listen for file selection and handle the import fileInput.addEventListener('change', importFromJson); footer.appendChild(importButton); // Append footer to the page const xFooter = document.getElementById('footer'); if (xFooter) { xFooter.insertAdjacentElement('beforebegin', footer); } else { document.body.appendChild(footer); } // Export function function exportToJson() { var kudos_history = { username: { data: localStorage.getItem('kudoshistory_username') }, settings: { data: localStorage.getItem('kudoshistory_settings') }, bookmarked: { data: localStorage.getItem('kudoshistory_bookmarked') || ',' }, kudosed: { data: localStorage.getItem('kudoshistory_kudosed') || ',' }, skipped: { data: localStorage.getItem('kudoshistory_skipped') || ',' }, seen: { data: localStorage.getItem('kudoshistory_seen') || ',' }, checked: { data: localStorage.getItem('kudoshistory_checked') || ',' } }; var export_lists = { username: kudos_history.username.data, settings: kudos_history.settings.data, bookmarked: kudos_history.bookmarked.data, kudosed: kudos_history.kudosed.data, skipped: kudos_history.skipped.data, seen: kudos_history.seen.data, checked: kudos_history.checked.data }; var textToSave = JSON.stringify(export_lists, null, 2); var blob = new Blob([textToSave], { type: "text/plain" }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'AO3_kudoshistory.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } // Import function function importFromJson(event) { var file = event.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(e) { try { var importedData = JSON.parse(e.target.result); if (!importedData.kudosed || !importedData.seen || !importedData.bookmarked || !importedData.skipped || !importedData.checked) { throw new Error("Missing data."); } var notes = "" var sizes_before = ['kudoshistory_kudosed', 'kudoshistory_seen', 'kudoshistory_bookmarked', 'kudoshistory_skipped', 'kudoshistory_checked'] .map(key => localStorage.getItem(key)?.length || 0); var sizes_after = ['kudosed', 'seen', 'bookmarked', 'skipped', 'checked'] .map(key => importedData[key]?.length || 0); if (importedData.kudosed) localStorage.setItem('kudoshistory_kudosed', importedData.kudosed); if (importedData.seen) localStorage.setItem('kudoshistory_seen', importedData.seen); if (importedData.bookmarked) localStorage.setItem('kudoshistory_bookmarked', importedData.bookmarked); if (importedData.skipped) localStorage.setItem('kudoshistory_skipped', importedData.skipped); if (importedData.checked) localStorage.setItem('kudoshistory_checked', importedData.checked); var diff = sizes_after.reduce((a, b) => a + b, 0) - sizes_before.reduce((a, b) => a + b, 0); notes += "\n- Data: " + sizes_before + " → " + sizes_after + " (" + (diff >= 0 ? "+" : "") + diff + ")"; if (localStorage.getItem('kudoshistory_username') == "null" && importedData.username && importedData.username != "null") { localStorage.setItem('kudoshistory_username', importedData.username); notes += "\n- Username: updated to " + importedData.username } else { notes += "\n- Username: no change" } if (importedData.settings && importedData.settings != localStorage.getItem('kudoshistory_settings')) { localStorage.setItem('kudoshistory_settings', importedData.settings); notes += "\n- Settings: updated " + importedData.settings } else { notes += "\n- Settings: no change" } alert("[userscript:Extend AO3] Success" + notes); } catch (error) { alert("[userscript:Extend AO3] Error\nInvalid file format / missing data."); } }; reader.readAsText(file); } // ========================================== let wasClicked = false; // Step 1: Wait for the button to exist and click it if it shows "Seen" function waitForSeenButton() { let attempts = 0; const maxAttempts = 100; // Stop after ~5 seconds (100 * 50ms) const buttonCheckInterval = setInterval(function() { attempts++; const seenButton = document.querySelector('.kh-seen-button a'); if (seenButton) { clearInterval(buttonCheckInterval); if (seenButton.textContent.includes('Seen ✓')) { if (!MARK_SEEN_FROM_AO3_ONLY || document.referrer.includes("archiveofourown.org")) { seenButton.click(); wasClicked = true; } } else { wasClicked = false; } // Move to Step 2 setupButtonObserver(); } else if (attempts >= maxAttempts) { clearInterval(buttonCheckInterval); } }, 50); } // Step 2: Monitor the button text and toggle it function setupButtonObserver() { toggleButtonText(true, wasClicked); // Button to observe const targetNode = document.querySelector('.kh-seen-button'); if (!targetNode) { return; } const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList' || mutation.type === 'characterData') { toggleButtonText(false, false); } }); }); const config = { childList: true, characterData: true, subtree: true }; observer.observe(targetNode, config); } function toggleButtonText(isFirst = false, wasClicked = false) { const buttonElement = document.querySelector('.kh-seen-button a'); if (!buttonElement) return; // Ignore changes we made ourselves if (buttonElement.textContent === "SEEN Now (click to unmark)" || buttonElement.textContent === "Old SEEN (click to unmark)" || buttonElement.textContent === "SEEN (click to unmark)" || buttonElement.textContent === "NOT SEEN (click to mark)" || buttonElement.textContent === "UNSEEN (click to mark)") { return; } const state_seen = buttonElement.textContent.includes('Unseen ✗') ? true : buttonElement.textContent.includes('Seen ✓') ? false : null; if (state_seen === null) { alert('[userscript:Extend AO3]\nUnknown text: ' + buttonElement.textContent); return; } const GREEN = "#33cc70"; // "#33cc70"; const GREEN_DARKER = "#00a13a"; // "#149b49"; const RED = "#ff6d50"; buttonElement.textContent = state_seen ? (isFirst ? (wasClicked ? "SEEN Now (click to unmark)" : "Old SEEN (click to unmark)") : "SEEN (click to unmark)") : "UNSEEN (click to mark)"; const color = state_seen ? (isFirst && !wasClicked ? GREEN_DARKER : GREEN) : RED; buttonElement.style.backgroundColor = color; buttonElement.style.padding = "2px 6px"; buttonElement.style.borderRadius = "3px"; buttonElement.style.boxShadow = "none"; buttonElement.style.backgroundImage = "none"; firstH1link.style.color = color; if (isFirst && wasClicked) { // blink buttonElement.style.transition = "background-color 150ms ease"; buttonElement.style.backgroundColor = GREEN; setTimeout(() => { buttonElement.style.backgroundColor = "#00e64b"; }, 150); setTimeout(() => { buttonElement.style.transition = "background-color 200ms linear"; buttonElement.style.backgroundColor = GREEN; }, 200); } else if (!isFirst) { buttonElement.style.transition = "none"; // Clear transition for subsequent calls buttonElement.style.backgroundColor = state_seen ? GREEN : RED; } } // Start the process waitForSeenButton(); })();