您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
New Bookmark made to be clear enables you to have clear updates and saves and displays all youre data in a open way
当前为
// ==UserScript== // @name Asura Bookmark Panel with Title Buttons + Import/Export // @namespace Violentmonkey Scripts // @match https://asuracomic.net/* // @grant none // @version 2 // @description New Bookmark made to be clear enables you to have clear updates and saves and displays all youre data in a open way // ==/UserScript== (function () { 'use strict'; const bookmarkKey = 'asuraManualBookmarks'; const hideKey = 'asuraManualHidden'; const wantKey = 'asuraManualWantToRead'; const load = (key) => JSON.parse(localStorage.getItem(key) || '{}'); const save = (key, data) => localStorage.setItem(key, JSON.stringify(data)); let bookmarks = load(bookmarkKey); let hidden = load(hideKey); let wantToRead = load(wantKey); // --- Styles --- const style = document.createElement('style'); style.textContent = ` .floating-panel-btn { position: fixed; top: 5px; right: 20px; background-color: #4b0082; color: white; padding: 10px 14px; border-radius: 8px; z-index: 9999; border: none; cursor: pointer; } .bookmark-panel { position: fixed; top: 60px; right: 40px; width: 600px; background: #1a1a1a; color: #fff; border: 1px solid #4b0082; border-radius: 10px; padding: 10px; z-index: 9999; display: none; max-height: 80vh; /* Make the panel a stacking context for sticky children */ overflow: hidden; display: flex; flex-direction: column; } .import-export { text-align: center; margin: 8px 0; border-radius: 8px; padding: 2px 0 2px 0; box-shadow: 0 2px 8px 0 rgba(0,0,0,0.10); border: none; background: none; } .import-export button { margin: 0 8px; padding: 6px 10px; border: none; border-radius: 6px; background-color: #4b0082; color: white; cursor: pointer; font-weight: bold; font-size: 15px; transition: background 0.2s; } .import-export button:hover { background-color: #6a0dad; } .panel-tabs { display: flex; gap: 10px; margin-bottom: 10px; justify-content: center; position: sticky; top: 0; background: #1a1a1a; z-index: 2; padding: 14px 0 14px 0; min-height: 48px; border-radius: 10px 10px 0 0; box-shadow: 0 4px 16px 0 rgba(0,0,0,0.18); } .tab-btn { flex: 1; padding: 12px 16px; cursor: pointer; background: #2a2a2a; text-align: center; border: none; color: white; font-weight: bold; border-radius: 10px; } .tab-btn.active { background: #4b0082; } .panel-content { display: flex; flex-direction: column; overflow-y: auto; max-height: calc(80vh - 60px); padding-top: 0; padding-bottom: 0; } .panel-entry { display: flex; gap: 10px; margin: 4px 0; padding: 6px; background: #2a2a2a; border-radius: 6px; align-items: center; } .panel-entry img { width: 90px; height: 120px; object-fit: cover; border-radius: 4px; } .panel-entry .info { display: flex; flex-direction: column; justify-content: space-between; flex-grow: 1; } .panel-entry button { align-self: flex-start; background: #6a0dad; border: none; color: white; border-radius: 4px; padding: 2px 6px; font-size: 12px; cursor: pointer; margin-top: 6px; } .asura-btn { margin-left: 6px; font-size: 14px; cursor: pointer; border: none; background: none; } .asura-hidden { display: none !important; } .chapter-bookmarked { color: #c084fc !important; font-weight: bold; } a[href*='/chapter/'].chapter-unread { color:rgb(28, 223, 45) !important; background: #222200 !important; font-weight: bold; } `; document.head.appendChild(style); // --- Utility --- function debounce(func, delay = 100) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), delay); }; } // --- Panel Rendering --- function updatePanel(container, tab) { container.innerHTML = ''; let items = []; // Only show Export All / Import All in the Hidden tab (at the top) if (tab === 'hidden') { const globalImportExport = document.createElement('div'); globalImportExport.className = 'import-export'; const globalExportBtn = document.createElement('button'); globalExportBtn.textContent = '📤 Export All'; globalExportBtn.onclick = () => { const allData = { bookmarks, hidden, wantToRead }; navigator.clipboard.writeText(JSON.stringify(allData, null, 2)) .then(() => alert('All data copied to clipboard!')); }; const globalImportBtn = document.createElement('button'); globalImportBtn.textContent = '📥 Import All'; globalImportBtn.onclick = () => { const input = prompt('Paste full JSON (bookmarks, hidden, wantToRead):'); try { const data = JSON.parse(input); if (data.bookmarks) Object.assign(bookmarks, data.bookmarks); if (data.hidden) Object.assign(hidden, data.hidden); if (data.wantToRead) Object.assign(wantToRead, data.wantToRead); save(bookmarkKey, bookmarks); save(hideKey, hidden); save(wantKey, wantToRead); updatePanel(container, tab); updateTitleButtons(); alert('All data imported successfully!'); } catch (e) { alert('Invalid JSON for import'); } }; globalImportExport.appendChild(globalExportBtn); globalImportExport.appendChild(globalImportBtn); container.appendChild(globalImportExport); } if (tab === 'bookmarks') { items = Object.values(bookmarks).sort((a, b) => (b.lastRead || 0) - (a.lastRead || 0)); } else if (tab === 'want') { items = Object.values(wantToRead); } else if (tab === 'hidden') { items = Object.entries(hidden).map(([title, obj]) => ({ title, chapter: '', url: '', cover: obj.cover || '' })); } items.forEach(obj => { const entry = document.createElement('div'); entry.className = 'panel-entry'; const img = document.createElement('img'); img.src = obj.cover || ''; entry.appendChild(img); const info = document.createElement('div'); info.className = 'info'; const link = document.createElement('a'); link.href = obj.url?.split('/chapter/')[0] || '#'; link.target = '_blank'; link.style.color = 'white'; link.textContent = obj.title || 'No title'; const titleEl = document.createElement('strong'); titleEl.appendChild(link); const chapterEl = document.createElement('div'); chapterEl.textContent = obj.chapter || ''; info.appendChild(titleEl); info.appendChild(chapterEl); const btn = document.createElement('button'); if (tab === 'bookmarks') { btn.textContent = 'Remove'; btn.onclick = () => { delete bookmarks[obj.title]; save(bookmarkKey, bookmarks); updatePanel(container, tab); updateTitleButtons(); }; } else if (tab === 'want') { btn.textContent = 'Remove'; btn.onclick = () => { delete wantToRead[obj.title]; save(wantKey, wantToRead); updatePanel(container, tab); updateTitleButtons(); }; } else if (tab === 'hidden') { btn.textContent = 'Unhide'; btn.onclick = () => { delete hidden[obj.title]; save(hideKey, hidden); updatePanel(container, tab); updateTitleButtons(); }; } info.appendChild(btn); entry.appendChild(info); container.appendChild(entry); }); } // --- Title Extraction --- function extractTitleFromHref(href) { const match = href.match(/\/series\/([a-z0-9-]+)/i); if (!match) return null; const slug = match[1].split('-'); if (slug.length > 2 && /^[a-z0-9]{6,}$/.test(slug[slug.length - 1])) slug.pop(); return slug.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); } // --- UI Creation --- function createUI() { const btn = document.createElement('button'); btn.textContent = '📂 Bookmarks'; btn.className = 'floating-panel-btn'; document.body.appendChild(btn); const panel = document.createElement('div'); panel.className = 'bookmark-panel'; panel.innerHTML = ` <div class="panel-tabs"> <button class="tab-btn active" data-tab="bookmarks">📌 Bookmarks</button> <button class="tab-btn" data-tab="hidden">🚫 Hidden</button> <button class="tab-btn" data-tab="want">📙 Want to Read</button> </div> <div class="panel-content"></div> `; document.body.appendChild(panel); const contentArea = panel.querySelector('.panel-content'); let currentTab = 'bookmarks'; const tabs = panel.querySelectorAll('.tab-btn'); tabs.forEach(tab => { tab.addEventListener('click', () => { tabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); currentTab = tab.dataset.tab; updatePanel(contentArea, currentTab); updateTabCounts(tabs); }); }); // Hide the panel by default on page load panel.style.display = 'none'; btn.onclick = () => { panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; if (panel.style.display === 'block') { updatePanel(contentArea, currentTab); updateTabCounts(tabs); } }; updatePanel(contentArea, currentTab); updateTabCounts(tabs); } // --- Tab Count Update --- function updateTabCounts(tabs) { tabs.forEach(tab => { const tabType = tab.dataset.tab; let count = 0; if (tabType === 'bookmarks') count = Object.keys(bookmarks).length; if (tabType === 'hidden') count = Object.keys(hidden).length; if (tabType === 'want') count = Object.keys(wantToRead).length; if (tabType === 'bookmarks') tab.textContent = `📌 Bookmarks - ${count}`; if (tabType === 'hidden') tab.textContent = `🚫 Hidden - ${count}`; if (tabType === 'want') tab.textContent = `📙 Want to Read - ${count}`; }); } // --- Title Buttons --- let debouncedUpdateTitleButtons; function updateTitleButtons() { const cards = document.querySelectorAll('.col-span-9'); cards.forEach(card => { const titleLink = card.querySelector('a[href^="/series/"]'); if (!titleLink) return; const href = titleLink.getAttribute('href'); const title = extractTitleFromHref(href); if (!title) return; const container = card.closest('.grid-cols-12'); if (hidden[title]) container?.classList.add('asura-hidden'); else container?.classList.remove('asura-hidden'); card.querySelectorAll('.asura-btn-group').forEach(el => el.remove()); const imgSrc = container?.querySelector('img.rounded-md.object-cover')?.src || ''; const btnGroup = document.createElement('span'); btnGroup.className = 'asura-btn-group'; // Set title color based on status if (wantToRead[title]) { const isLocked = wantToRead[title].locked; if (isLocked) { titleLink.style.color = '#FFD700'; // yellow (📙 only) } else { titleLink.style.color = '#00BFFF'; // blue (📙 + 📍❌) } } else if (bookmarks[title]) { titleLink.style.color = '#c084fc'; // purple for 📌 } else { titleLink.style.color = '#00BFFF'; // also blue } // Bookmarked 📌 if (bookmarks[title]) { const pinBtn = document.createElement('button'); pinBtn.className = 'asura-btn'; pinBtn.textContent = '📌'; pinBtn.title = 'Marked as read'; pinBtn.onclick = (e) => { e.preventDefault(); delete bookmarks[title]; save(bookmarkKey, bookmarks); updateTitleButtons(); }; btnGroup.appendChild(pinBtn); } else { const isWantLocked = wantToRead[title] && wantToRead[title].locked; // 📙 Want to Read const wantBtn = document.createElement('button'); wantBtn.className = 'asura-btn'; wantBtn.textContent = '📙'; wantBtn.title = 'Want to read'; wantBtn.onclick = (e) => { e.preventDefault(); if (isWantLocked) { delete wantToRead[title]; } else { delete bookmarks[title]; wantToRead[title] = { title, chapter: '', url: href, cover: imgSrc, locked: true }; } save(bookmarkKey, bookmarks); save(wantKey, wantToRead); updateTitleButtons(); }; btnGroup.appendChild(wantBtn); if (!isWantLocked) { // 📍 Mark as Read const markBtn = document.createElement('button'); markBtn.className = 'asura-btn'; markBtn.textContent = '📍'; markBtn.title = 'Mark as read'; markBtn.onclick = (e) => { e.preventDefault(); delete wantToRead[title]; bookmarks[title] = { title, chapter: '', url: href, cover: imgSrc }; save(bookmarkKey, bookmarks); save(wantKey, wantToRead); updateTitleButtons(); }; btnGroup.appendChild(markBtn); // ❌ Hide const hideBtn = document.createElement('button'); hideBtn.className = 'asura-btn'; hideBtn.textContent = '❌'; hideBtn.title = 'Hide comic'; hideBtn.onclick = (e) => { e.preventDefault(); hidden[title] = { cover: imgSrc }; save(hideKey, hidden); updateTitleButtons(); }; btnGroup.appendChild(hideBtn); } } titleLink.parentElement.appendChild(btnGroup); // --- Chapter Highlighting (last read = purple, above = yellow, below = red) --- const bookmarkedChapterRaw = bookmarks[title]?.chapter || ''; let bookmarkedNum = null; const bookmarkedMatch = bookmarkedChapterRaw.match(/(\d+(?:\.\d+)?)/); if (bookmarkedMatch) bookmarkedNum = parseFloat(bookmarkedMatch[1]); const chapterLinks = card.querySelectorAll('a[href*="/chapter/"]'); chapterLinks.forEach(chapLink => { // Try to get chapter number from <p> inside the link, then fallback to text, then URL let chapterNum = null; let chapterText = ''; const p = chapLink.querySelector('p'); if (p && p.textContent) { chapterText = p.textContent.trim(); } else { // Try to find any text node with a number const walker = document.createTreeWalker(chapLink, NodeFilter.SHOW_TEXT, null); let node; while ((node = walker.nextNode())) { if (/\d/.test(node.textContent)) { chapterText = node.textContent.trim(); break; } } if (!chapterText && chapLink.textContent) { chapterText = chapLink.textContent.trim(); } } chapterText = chapterText.replace(/,/g, '').replace(/\s+/g, ' '); let match = chapterText.match(/(\d+(?:\.\d+)?)/); if (match) { chapterNum = parseFloat(match[1]); } else { const chapterHref = chapLink.getAttribute('href'); const urlMatch = chapterHref.match(/chapter\/([\d.]+)/i); if (urlMatch) chapterNum = parseFloat(urlMatch[1]); } chapLink.classList.remove('chapter-bookmarked', 'chapter-unread', 'chapter-read'); // Debug output // console.log('Chapter link:', chapLink, 'chapterNum:', chapterNum, 'bookmarkedNum:', bookmarkedNum); if (bookmarkedNum !== null && chapterNum !== null) { if (chapterNum === bookmarkedNum) { chapLink.classList.add('chapter-bookmarked'); // Purple (last read) // console.log('Applied: chapter-bookmarked'); } else if (chapterNum > bookmarkedNum) { chapLink.classList.add('chapter-unread'); // Yellow (unread/new) // console.log('Applied: chapter-unread'); } } // Save on middle or left click const saveClick = () => { bookmarks[title] = { title, chapter: chapterText, url: chapLink.getAttribute('href'), cover: imgSrc, lastRead: Date.now() }; save(bookmarkKey, bookmarks); debouncedUpdateTitleButtons(); }; chapLink.addEventListener('auxclick', e => { if (e.button === 1) saveClick(); }); chapLink.addEventListener('click', e => { if (e.button === 0) saveClick(); }); }); }); } debouncedUpdateTitleButtons = debounce(updateTitleButtons, 200); // --- Wait for Content --- function waitForContent() { const observer = new MutationObserver((_, obs) => { if (document.querySelector('.grid-cols-12')) { obs.disconnect(); createUI(); updateTitleButtons(); } }); observer.observe(document.body, { childList: true, subtree: true }); } waitForContent(); })();