// ==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 // @downloadURL none // ==/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 = `
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(); })();