您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tracks clicked/visited links. Copies unvisited links from page and also Filters user-pasted URLs against history.
// ==UserScript== // @name Copy Unvisited Links (Custom History v1.9 - Filter Pasted URLs) // @namespace http://tampermonkey.net/ // @version 1.9 // @description Tracks clicked/visited links. Copies unvisited links from page and also Filters user-pasted URLs against history. // @author Greg Cromwell / Gemini AI // @license MIT // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_setClipboard // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const SCRIPT_VERSION = "1.9"; const CONSOLE_PREFIX = `[Custom History v${SCRIPT_VERSION}]`; const CUSTOM_HISTORY_KEY = `customUserVisitedLinks_v${SCRIPT_VERSION}`; const MAX_CUSTOM_HISTORY_SIZE = 10000; const COPY_BUTTON_ID = `copyCustomUnvisitedBtn_v${SCRIPT_VERSION}`; const PASTE_FILTER_BUTTON_ID = `pasteFilterBtn_v${SCRIPT_VERSION}`; const MODAL_ID = `pasteFilterModal_v${SCRIPT_VERSION}`; const TEXTAREA_ID = `pasteFilterTextarea_v${SCRIPT_VERSION}`; console.log(`${CONSOLE_PREFIX} Script loading.`); // --- 1. URL Normalization --- function normalizeUrl(url) { try { const urlObj = new URL(url, document.baseURI); let normalized = `${urlObj.protocol}//${urlObj.hostname}${urlObj.port ? ':' + urlObj.port : ''}${urlObj.pathname}`; if (normalized.length > 1 && normalized.endsWith('/')) { normalized = normalized.slice(0, -1); } return normalized.toLowerCase(); } catch (e) { console.warn(`${CONSOLE_PREFIX} Could not normalize URL: "${url}"`, e.message); return null; } } // --- 2. Custom History Management --- async function getCustomHistorySet() { const historyArray = await GM_getValue(CUSTOM_HISTORY_KEY, []); return new Set(Array.isArray(historyArray) ? historyArray : []); } async function addUrlToCustomHistory(url) { const normalizedUrl = normalizeUrl(url); if (!normalizedUrl) return; let historyArray = await GM_getValue(CUSTOM_HISTORY_KEY, []); if (!Array.isArray(historyArray)) { historyArray = []; } const tempHistorySet = new Set(historyArray); if (!tempHistorySet.has(normalizedUrl)) { historyArray.push(normalizedUrl); if (historyArray.length > MAX_CUSTOM_HISTORY_SIZE) { historyArray = historyArray.slice(historyArray.length - MAX_CUSTOM_HISTORY_SIZE); } await GM_setValue(CUSTOM_HISTORY_KEY, historyArray); } } // --- 3. Link Processing: Attach Click Listeners --- async function processLinksOnPageForHistory() { const links = document.querySelectorAll('a[href]'); links.forEach(link => { const linkHref = link.href; link.addEventListener('click', function handleLinkClick() { addUrlToCustomHistory(linkHref); }); }); } // --- 4. Button Functionality --- // Function for the first button: Copy "New" Links from page async function handleCopyUnvisitedClick() { console.log(`${CONSOLE_PREFIX} handleCopyUnvisitedClick called.`); alert("Processing page links... Check console (F12) for progress."); const visitedSet = await getCustomHistorySet(); const unvisitedLinksToCopy = []; let pageLinkCount = 0; const allPageLinks = document.querySelectorAll('a[href]'); pageLinkCount = allPageLinks.length; allPageLinks.forEach(link => { const originalHref = link.href; const normalizedHref = normalizeUrl(originalHref); if (normalizedHref && !normalizedHref.startsWith('javascript:') && !normalizedHref.startsWith('mailto:')) { if (!visitedSet.has(normalizedHref)) { unvisitedLinksToCopy.push(originalHref); } } }); const uniqueUnvisitedLinks = [...new Set(unvisitedLinksToCopy)]; if (uniqueUnvisitedLinks.length > 0) { GM_setClipboard(uniqueUnvisitedLinks.join('\n'), 'text'); alert(`Copied ${uniqueUnvisitedLinks.length} unique "new" link(s) (not in custom history) out of ${pageLinkCount} links to clipboard.`); } else { alert(`No "new" links found on this page (out of ${pageLinkCount} links) based on your custom history.`); } console.log(`${CONSOLE_PREFIX} handleCopyUnvisitedClick finished.`); } // --- Modal for Pasting Links --- function showPasteModal(callback) { if (document.getElementById(MODAL_ID)) { document.getElementById(MODAL_ID).style.display = 'flex'; document.getElementById(TEXTAREA_ID).value = ''; // Clear previous content document.getElementById(TEXTAREA_ID).focus(); return; } const modalOverlay = document.createElement('div'); modalOverlay.id = MODAL_ID; Object.assign(modalOverlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: '10001' }); const modalContent = document.createElement('div'); Object.assign(modalContent.style, { background: 'white', padding: '25px', borderRadius: '8px', boxShadow: '0 4px 15px rgba(0,0,0,0.2)', minWidth: '350px', maxWidth: '90%', display: 'flex', flexDirection: 'column', gap: '15px' }); const title = document.createElement('h3'); title.textContent = 'Filter Pasted Links'; Object.assign(title.style, { margin: '0 0 10px 0', textAlign: 'center' }); const instruction = document.createElement('p'); instruction.textContent = 'Paste your list of URLs below (one URL per line):'; Object.assign(instruction.style, { margin: '0 0 5px 0', fontSize: '14px' }); const textarea = document.createElement('textarea'); textarea.id = TEXTAREA_ID; Object.assign(textarea.style, { width: 'calc(100% - 20px)', minHeight: '150px', border: '1px solid #ccc', borderRadius: '4px', padding: '10px', fontSize: '13px', resize: 'vertical' }); const buttonContainer = document.createElement('div'); Object.assign(buttonContainer.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '10px' }); const processButton = document.createElement('button'); processButton.textContent = 'Filter & Copy'; Object.assign(processButton.style, { padding: '8px 15px', background: '#4CAF50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }); const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; Object.assign(cancelButton.style, { padding: '8px 15px', background: '#f44336', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }); processButton.onclick = () => { const text = textarea.value; modalOverlay.style.display = 'none'; callback(text); }; cancelButton.onclick = () => { modalOverlay.style.display = 'none'; }; modalOverlay.onclick = (event) => { // Close if backdrop is clicked if (event.target === modalOverlay) { modalOverlay.style.display = 'none'; } }; buttonContainer.append(cancelButton, processButton); modalContent.append(title, instruction, textarea, buttonContainer); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); textarea.focus(); } // Function for the second button: Process Pasted Text async function handleFilterPastedContentClick() { console.log(`${CONSOLE_PREFIX} handleFilterPastedContentClick called.`); showPasteModal(async (pastedText) => { if (!pastedText || pastedText.trim() === "") { alert("No text was pasted or processed."); console.log(`${CONSOLE_PREFIX} No text provided in modal.`); return; } const urlsFromPastedText = pastedText.split('\n').map(line => line.trim()).filter(line => line.length > 0); if (urlsFromPastedText.length === 0) { alert("No valid URLs found in the pasted text after parsing."); console.log(`${CONSOLE_PREFIX} No URLs found in pasted content.`); return; } console.log(`${CONSOLE_PREFIX} Found ${urlsFromPastedText.length} lines/URLs in pasted text.`); const visitedSet = await getCustomHistorySet(); const unvisitedFromPasted = []; urlsFromPastedText.forEach(url => { const normalizedUrl = normalizeUrl(url); if (normalizedUrl && !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('mailto:')) { if (!visitedSet.has(normalizedUrl)) { unvisitedFromPasted.push(url); // Store original URL } } }); const uniqueUnvisitedFromPasted = [...new Set(unvisitedFromPasted)]; console.log(`${CONSOLE_PREFIX} Found ${uniqueUnvisitedFromPasted.length} unique unvisited links from pasted text.`); if (uniqueUnvisitedFromPasted.length > 0) { GM_setClipboard(uniqueUnvisitedFromPasted.join('\n'), 'text'); alert(`Filtered Pasted Text: ${uniqueUnvisitedFromPasted.length} unique "new" link(s) (not in your custom history) out of ${urlsFromPastedText.length} pasted links are now on your clipboard.`); } else { alert(`No "new" links found in your pasted text (out of ${urlsFromPastedText.length} lines) based on your custom history. Clipboard not changed.`); } console.log(`${CONSOLE_PREFIX} handleFilterPastedContentClick processing finished.`); }); } // --- UI Element Creation --- function createAndAddButtons() { // Button 1: Copy "New" Links from Page if (!document.getElementById(COPY_BUTTON_ID)) { const button1 = document.createElement('button'); button1.id = COPY_BUTTON_ID; button1.textContent = `CpNew v${SCRIPT_VERSION.substring(0,3)}`; // Shorter text button1.title = `Copy "New" Links from Page (v${SCRIPT_VERSION})`; Object.assign(button1.style, { position: 'fixed', top: '75px', right: '1px', zIndex: '3001', fontSize: '10px', padding: '1px 2px', margin: '0', lineHeight: '1', minWidth: '30px', backgroundColor: 'lightblue', border: '1px solid gray', borderRadius: '2px', cursor: 'pointer', boxShadow: '1px 1px 1px rgba(0,0,0,0.1)' }); button1.addEventListener('click', handleCopyUnvisitedClick); appendElementToBody(button1, "Copy New Links button"); } // Button 2: Paste & Filter if (!document.getElementById(PASTE_FILTER_BUTTON_ID)) { const button2 = document.createElement('button'); button2.id = PASTE_FILTER_BUTTON_ID; button2.textContent = `Paste&Filt`; // Shorter text button2.title = `Paste and Filter URLs against Custom History (v${SCRIPT_VERSION})`; Object.assign(button2.style, { position: 'fixed', top: '95px', right: '1px', zIndex: '3000', fontSize: '10px', padding: '1px 2px', margin: '0', lineHeight: '1', minWidth: '30px', backgroundColor: 'lightgreen', border: '1px solid gray', borderRadius: '2px', cursor: 'pointer', boxShadow: '1px 1px 1px rgba(0,0,0,0.1)' }); button2.addEventListener('click', handleFilterPastedContentClick); appendElementToBody(button2, "Paste & Filter button"); } } function appendElementToBody(element, elementName) { if (document.body) { document.body.appendChild(element); } else { window.addEventListener('DOMContentLoaded', () => { if (document.body) document.body.appendChild(element); else console.error(`${CONSOLE_PREFIX} ${elementName}: document.body still not found.`); }, { once: true }); } } // --- 5. Initialization and Dynamic Content Handling --- function initializeScript() { addUrlToCustomHistory(window.location.href); processLinksOnPageForHistory(); createAndAddButtons(); } try { initializeScript(); } catch(e) { console.error(`${CONSOLE_PREFIX} CRITICAL ERROR during script initialization:`, e); alert(`[Custom History v${SCRIPT_VERSION}] CRITICAL ERROR: ${e.message}. Check console.`); } const observer = new MutationObserver((mutationsList) => { if (!document.getElementById(COPY_BUTTON_ID) || !document.getElementById(PASTE_FILTER_BUTTON_ID)) { createAndAddButtons(); } for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { let hasNewLinks = false; mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && (node.matches('a[href]') || node.querySelector('a[href]'))) { hasNewLinks = true; } }); if (hasNewLinks) { processLinksOnPageForHistory(); break; } } } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { window.addEventListener('DOMContentLoaded', () => { if (document.body) observer.observe(document.body, { childList: true, subtree: true }); else console.error(`${CONSOLE_PREFIX} MutationObserver: document.body not found.`); }, { once: true }); } window[`clearUserLinkHistory_v${SCRIPT_VERSION.replace(/\./g, '_')}`] = async () => { console.log(`${CONSOLE_PREFIX} clearUserLinkHistory called by user.`); if (confirm(`Are you sure you want to clear all custom link history for script version ${SCRIPT_VERSION}?`)) { await GM_setValue(CUSTOM_HISTORY_KEY, []); alert(`Custom link history (v${SCRIPT_VERSION}) cleared. Reload page for full effect.`); } else { alert(`Custom link history (v${SCRIPT_VERSION}) clearing cancelled.`); } }; console.log(`${CONSOLE_PREFIX} Script loaded. Type 'clearUserLinkHistory_v${SCRIPT_VERSION.replace(/\./g, '_')}()' in console to clear history.`); })();