// ==UserScript== // @name Asura Bookmark Manager // @namespace Violentmonkey Scripts // @match https://asuracomic.net/* // @grant none // @version 2.1 // @description Track your manga reading progress with bookmarks, want-to-read list, and remove titles. // @author Moose, GitHub Copilot, GPT // @downloadURL https://update.greasyfork.icu/scripts/541870/Asura%20Bookmark%20Manager.user.js // @updateURL https://update.greasyfork.icu/scripts/541870/Asura%20Bookmark%20Manager.meta.js // ==/UserScript== (function () { 'use strict'; // Don't run on chapter pages if (location.pathname.includes('/chapter/')) { return; } 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); // --- Default colors (can be customized) --- let colors = { bookmarked: '#c084fc', // Purple for bookmarked titles wantToRead: '#FFD700', // Gold for want-to-read titles defaultTitle: '#00BFFF', // Blue for default titles chapterBookmarked: '#c084fc', // Purple for last read chapter chapterUnread: '#1cdf2d', // Green for unread chapters chapterBookmarkedBg: '#45025f', // Darker purple background (series page) chapterUnreadBg: '#414101' // Darker yellow/green background (series page) }; // Load saved colors function loadColors() { const savedColors = localStorage.getItem('asuraBookmarkColors'); if (savedColors) { colors = { ...colors, ...JSON.parse(savedColors) }; } updateStyles(); } // Save colors function saveColors() { localStorage.setItem('asuraBookmarkColors', JSON.stringify(colors)); updateStyles(); } // Update CSS styles with current colors function updateStyles() { const existingStyle = document.getElementById('asura-dynamic-styles'); if (existingStyle) existingStyle.remove(); const dynamicStyle = document.createElement('style'); dynamicStyle.id = 'asura-dynamic-styles'; dynamicStyle.textContent = ` /* CHAPTER HIGHLIGHTING */ .chapter-bookmarked, a[href*='/chapter/'].chapter-bookmarked { color: ${colors.chapterBookmarked} !important; font-weight: bold !important; } .chapter-unread, a[href*='/chapter/'].chapter-unread { color: ${colors.chapterUnread} !important; font-weight: bold !important; } /* Series page specific highlighting */ body[data-series-page="true"] .chapter-bookmarked, body[data-series-page="true"] a[href*='/chapter/'].chapter-bookmarked { background: ${colors.chapterBookmarkedBg} !important; } body[data-series-page="true"] .chapter-unread, body[data-series-page="true"] a[href*='/chapter/'].chapter-unread { background: ${colors.chapterUnreadBg} !important; } `; document.head.appendChild(dynamicStyle); } // --- STYLES --- const style = document.createElement('style'); style.textContent = ` /* Main panel button */ .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 */ .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; overflow: hidden; display: flex; flex-direction: column; } /* Panel tabs */ .panel-tabs { display: flex; gap: 10px; margin-bottom: 10px; justify-content: center; position: sticky; top: 0; background: #1a1a1a; z-index: 2; padding: 14px 0; 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 */ .panel-content { display: flex; flex-direction: column; overflow-y: auto; max-height: calc(80vh - 100px); padding-top: 0; padding-bottom: 20px; } .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; } /* Action buttons */ .asura-btn { margin-left: 6px; font-size: 14px; cursor: pointer; border: none; background: none; } /* Hidden manga */ .asura-hidden { display: none !important; } /* Settings styles */ .settings-section { margin-bottom: 25px; padding: 15px; background: #2a2a2a; border-radius: 8px; } .settings-section h4 { margin: 0 0 15px 0; color: #c084fc; font-size: 16px; } .color-input-group { display: flex; align-items: center; margin: 10px 0; gap: 10px; } .color-input-group label { min-width: 150px; font-size: 14px; } .color-input-group input[type="color"] { width: 50px; height: 30px; border: none; border-radius: 4px; cursor: pointer; } .color-input-group input[type="text"] { width: 80px; padding: 5px; border: 1px solid #444; border-radius: 4px; background: #1a1a1a; color: white; font-family: monospace; } .settings-tabs { display: flex; gap: 5px; margin-bottom: 15px; } .settings-tab-btn { padding: 8px 16px; background: #444; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; } .settings-tab-btn.active { background: #6a0dad; } `; document.head.appendChild(style); // --- UTILITIES --- function debounce(func, delay = 100) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), delay); }; } function extractTitleFromHref(href) { const match = href.match(/\/series\/([a-z0-9-]+)/i); if (!match) return null; let slug = match[1].replace(/-\w{6,}$/, ''); return slug.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); } function findMatchingKey(searchTitle, dataObject) { const normalizedSearch = searchTitle.toLowerCase() .replace(/[''`]/g, '') .replace(/\s+/g, ' ') .replace(/[^\w\s]/g, '') .trim(); for (const key in dataObject) { const normalizedKey = key.toLowerCase() .replace(/[''`]/g, '') .replace(/\s+/g, ' ') .replace(/[^\w\s]/g, '') .trim(); if (normalizedKey === normalizedSearch || normalizedKey.includes(normalizedSearch) || normalizedSearch.includes(normalizedKey)) { return key; } } return null; } // --- PANEL RENDERING --- function updatePanel(container, tab) { // Patch legacy bookmarks: ensure each has a chapter (default "Chapter 0" if missing) let patched = false; for (const key in bookmarks) { if (!bookmarks[key].chapter || !bookmarks[key].chapter.trim()) { bookmarks[key].chapter = 'Chapter 0'; patched = true; } } if (patched) save(bookmarkKey, bookmarks); container.innerHTML = ''; let items = []; if (tab === 'bookmarks') { // Merge duplicate bookmarks by normalized title const merged = {}; Object.values(bookmarks).forEach(obj => { let norm = (obj.title || '').replace(/ASURA\+Premium\s*/i, '').replace(/\s+/g, ' ').trim(); norm = norm.replace(/-\w{6,}$/, '').toLowerCase(); if (!norm) return; if (!merged[norm]) { merged[norm] = { ...obj }; } else { if ((obj.lastRead || 0) > (merged[norm].lastRead || 0)) merged[norm] = { ...obj }; if (!merged[norm].cover && obj.cover) merged[norm].cover = obj.cover; } }); items = Object.values(merged).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 => { let cleanTitle = (obj.title || '').replace(/\s+/g, ' ').replace(/[\r\n]+/g, '').trim(); cleanTitle = cleanTitle.replace(/-\w{6,}$/, '').replace(/^ASURA\+Premium\s*/i, '').trim(); 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 = cleanTitle || '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); // Panel Buttons const btnGroup = document.createElement('span'); if (tab === 'bookmarks') { // Move to Want to Read const wantBtn = document.createElement('button'); wantBtn.className = 'asura-btn'; wantBtn.textContent = '📙'; wantBtn.title = 'Move to Want to Read'; wantBtn.onclick = () => { wantToRead[cleanTitle] = { ...obj, title: cleanTitle }; delete bookmarks[cleanTitle]; save(bookmarkKey, bookmarks); save(wantKey, wantToRead); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(wantBtn); // Move to Hidden const hideBtn = document.createElement('button'); hideBtn.className = 'asura-btn'; hideBtn.textContent = '❌'; hideBtn.title = 'Move to Hidden'; hideBtn.onclick = () => { hidden[cleanTitle] = { cover: obj.cover }; delete bookmarks[cleanTitle]; save(bookmarkKey, bookmarks); save(hideKey, hidden); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(hideBtn); // Remove completely const removeBtn = document.createElement('button'); removeBtn.className = 'asura-btn'; removeBtn.textContent = 'Remove'; removeBtn.title = 'Remove from all lists'; removeBtn.onclick = () => { delete bookmarks[cleanTitle]; delete wantToRead[cleanTitle]; delete hidden[cleanTitle]; save(bookmarkKey, bookmarks); save(wantKey, wantToRead); save(hideKey, hidden); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(removeBtn); } // Hidden tab: 📌📙 else if (tab === 'hidden') { // 📌 Move to Bookmarks const pinBtn = document.createElement('button'); pinBtn.className = 'asura-btn'; pinBtn.textContent = '📌'; pinBtn.title = 'Move to Bookmarks'; pinBtn.onclick = () => { bookmarks[cleanTitle] = { ...obj, title: cleanTitle, chapter: obj.chapter || 'Chapter 0' }; delete hidden[cleanTitle]; save(bookmarkKey, bookmarks); save(hideKey, hidden); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(pinBtn); // 📙 Move to Want to Read const wantBtn = document.createElement('button'); wantBtn.className = 'asura-btn'; wantBtn.textContent = '📙'; wantBtn.title = 'Move to Want to Read'; wantBtn.onclick = () => { wantToRead[cleanTitle] = { ...obj, title: cleanTitle }; delete hidden[cleanTitle]; save(wantKey, wantToRead); save(hideKey, hidden); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(wantBtn); // Remove button const removeBtn = document.createElement('button'); removeBtn.className = 'asura-btn'; removeBtn.textContent = 'Remove'; removeBtn.title = 'Remove from all lists'; removeBtn.onclick = () => { delete bookmarks[cleanTitle]; delete wantToRead[cleanTitle]; delete hidden[cleanTitle]; save(bookmarkKey, bookmarks); save(wantKey, wantToRead); save(hideKey, hidden); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(removeBtn); } // Want to Read tab: 📌❌ else if (tab === 'want') { // 📌 Move to Bookmarks const pinBtn = document.createElement('button'); pinBtn.className = 'asura-btn'; pinBtn.textContent = '📌'; pinBtn.title = 'Move to Bookmarks'; pinBtn.onclick = () => { bookmarks[cleanTitle] = { ...obj, title: cleanTitle, chapter: obj.chapter || 'Chapter 0' }; delete wantToRead[cleanTitle]; save(bookmarkKey, bookmarks); save(wantKey, wantToRead); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(pinBtn); // ❌ Move to Hidden const hideBtn = document.createElement('button'); hideBtn.className = 'asura-btn'; hideBtn.textContent = '❌'; hideBtn.title = 'Move to Hidden'; hideBtn.onclick = () => { hidden[cleanTitle] = { cover: obj.cover }; delete wantToRead[cleanTitle]; save(hideKey, hidden); save(wantKey, wantToRead); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(hideBtn); // Remove button const removeBtn = document.createElement('button'); removeBtn.className = 'asura-btn'; removeBtn.textContent = 'Remove'; removeBtn.title = 'Remove from all lists'; removeBtn.onclick = () => { delete bookmarks[cleanTitle]; delete wantToRead[cleanTitle]; delete hidden[cleanTitle]; save(bookmarkKey, bookmarks); save(wantKey, wantToRead); save(hideKey, hidden); updatePanel(container, tab); updateTitleButtons(); }; btnGroup.appendChild(removeBtn); } info.appendChild(btnGroup); entry.appendChild(info); container.appendChild(entry); }); } // --- SETTINGS PANEL --- function updateSettingsPanel(container) { container.innerHTML = `
No hidden manga
'; } else { hiddenItems.forEach(obj => { let cleanTitle = (obj.title || '').replace(/\s+/g, ' ').replace(/[\r\n]+/g, '').trim(); cleanTitle = cleanTitle.replace(/-\w{6,}$/, '').replace(/^ASURA\+Premium\s*/i, '').trim(); hiddenHTML += `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 = () => { // Use enhanced fuzzy matching for chapter saves let matchingKey = findMatchingKey(title, bookmarks) || title; console.log('Main page saveClick - looking for:', title); console.log('Found matching key:', matchingKey); // Clean chapter text - extract only "Chapter X" format let cleanChapterText = chapterText; const chapterMatch = chapterText.match(/Chapter\s*(\d+(?:\.\d+)?)/i); if (chapterMatch) { cleanChapterText = `Chapter ${chapterMatch[1]}`; } // Add to top when saving new chapter progress const newBookmarkEntry = { ...(bookmarks[matchingKey] || { title: matchingKey }), title: matchingKey, chapter: cleanChapterText, url: chapLink.getAttribute('href'), cover: bookmarks[matchingKey]?.cover || imgSrc || '', lastRead: Date.now() }; // Remove existing entry and add to top delete bookmarks[matchingKey]; bookmarks = { [matchingKey]: newBookmarkEntry, ...bookmarks }; save(bookmarkKey, bookmarks); debouncedUpdateTitleButtons(); }; chapLink.addEventListener('auxclick', e => { if (e.button === 1) saveClick(); }); chapLink.addEventListener('click', e => { if (e.button === 0) saveClick(); }); }); }); // --- Simplified /series/ page logic --- if (location.pathname.startsWith('/series/')) { // Remove any previously injected button group or color const prevBtnGroup = document.querySelector('.asura-series-btn-group'); if (prevBtnGroup) prevBtnGroup.remove(); // Find the title element let titleHeader = document.querySelector('h1, h2, .font-bold.text-3xl, .font-bold.text-2xl, .font-bold.text-xl') || document.querySelector('.text-xl.font-bold'); if (!titleHeader) { const alt = document.querySelector('.text-center.sm\\:text-left .text-xl.font-bold'); if (alt) titleHeader = alt; } if (!titleHeader) return; // Get title let pageTitle = titleHeader.textContent?.trim() || ''; // If the title is just "ASURA+Premium", try to find the actual manga title if (pageTitle === 'ASURA+Premium' || pageTitle.startsWith('ASURA+Premium')) { // Try alternative selectors for the actual manga title const altTitleSelectors = [ 'h1.text-3xl.font-bold', 'h1.text-2xl.font-bold', '.text-3xl.font-bold', '.text-2xl.font-bold', 'h1[class*="font-bold"]', 'h2[class*="font-bold"]' ]; for (const selector of altTitleSelectors) { const altTitle = document.querySelector(selector); if (altTitle && altTitle.textContent?.trim() !== 'ASURA+Premium' && altTitle.textContent?.trim()) { titleHeader = altTitle; pageTitle = altTitle.textContent.trim(); break; } } // If still "ASURA+Premium", try extracting from URL if (pageTitle === 'ASURA+Premium' || pageTitle.startsWith('ASURA+Premium')) { const urlTitle = extractTitleFromHref(location.pathname); if (urlTitle) { pageTitle = urlTitle; } } } // If title contains "Chapter", use only the last word (the actual title) if (/^Chapter\s+/i.test(pageTitle)) { pageTitle = pageTitle.replace(/^Chapter\s+/i, '').trim(); } // If title contains multiple lines (e.g. "ASURA+Premium\nChapter 283"), use only the first non-empty line if (pageTitle.includes('\n')) { pageTitle = pageTitle.split('\n').map(l => l.trim()).filter(Boolean)[0] || pageTitle; } // Remove any trailing hex if present (for consistency) pageTitle = pageTitle.replace(/-\w{6,}$/, ''); // DEBUG: Add this to see what titles are generated // console.log('Series page title:', pageTitle); // Find best matching bookmark key using enhanced fuzzy matching let bookmarkKeyName = findMatchingKey(pageTitle, bookmarks) || pageTitle; // console.log('Bookmark key name:', bookmarkKeyName); // Get canonical series URL let seriesUrl = location.pathname; const canonicalLink = document.querySelector('link[rel="canonical"]'); if (canonicalLink) { seriesUrl = canonicalLink.getAttribute('href') || seriesUrl; } // Get cover image from poster const coverImg = document.querySelector('img[alt="poster"].rounded.mx-auto.md\\:mx-0')?.src || document.querySelector('img[alt="poster"].rounded.mx-auto')?.src || document.querySelector('img[alt="poster"]')?.src || ''; // Remove any previous button group if (titleHeader.parentElement.querySelector('.asura-series-btn-group')) { titleHeader.parentElement.querySelector('.asura-series-btn-group').remove(); } const btnGroup = document.createElement('span'); btnGroup.className = 'asura-series-btn-group'; btnGroup.style.marginLeft = '10px'; // 📙 Want to Read button const wantBtn = document.createElement('button'); wantBtn.className = 'asura-btn'; wantBtn.textContent = '📙'; wantBtn.title = wantToRead[bookmarkKeyName] ? 'Remove Want to Read' : 'Want to Read'; wantBtn.onclick = (e) => { e.preventDefault(); if (wantToRead[bookmarkKeyName]) { delete wantToRead[bookmarkKeyName]; } else { // Use existing matching title if found, otherwise use bookmarkKeyName const titleToUse = findMatchingKey(pageTitle, wantToRead) || bookmarkKeyName; wantToRead[titleToUse] = { title: titleToUse, chapter: 'Chapter 0', url: seriesUrl, cover: coverImg }; } save(wantKey, wantToRead); updateTitleButtons(); // immediate update for color }; btnGroup.appendChild(wantBtn); // 📌 Bookmark button const pinBtn = document.createElement('button'); pinBtn.className = 'asura-btn'; pinBtn.textContent = '📌'; pinBtn.title = bookmarks[bookmarkKeyName] ? 'Remove Bookmark' : 'Bookmark'; pinBtn.onclick = (e) => { e.preventDefault(); if (bookmarks[bookmarkKeyName]) { delete bookmarks[bookmarkKeyName]; } else { // Use existing matching title if found, otherwise use bookmarkKeyName const titleToUse = findMatchingKey(pageTitle, bookmarks) || bookmarkKeyName; bookmarks[titleToUse] = { title: titleToUse, chapter: 'Chapter 0', url: seriesUrl, cover: coverImg }; } save(bookmarkKey, bookmarks); updateTitleButtons(); // immediate update for color }; btnGroup.appendChild(pinBtn); titleHeader.parentElement.appendChild(btnGroup); // --- Set color immediately after buttons (fix: always correct color) --- if (wantToRead[bookmarkKeyName]) { titleHeader.style.color = colors.wantToRead; } else if (bookmarks[bookmarkKeyName]) { titleHeader.style.color = colors.bookmarked; } else { titleHeader.style.color = colors.defaultTitle; } // --- Chapter highlighting and save --- // Find all chapter links in the list (inside .group.w-full) const chapterGroups = document.querySelectorAll('.group.w-full'); const bookmarkedChapterRaw = bookmarks[bookmarkKeyName]?.chapter || ''; let bookmarkedNum = null; const bookmarkedMatch = bookmarkedChapterRaw.match(/(\d+(?:\.\d+)?)/); if (bookmarkedMatch) bookmarkedNum = parseFloat(bookmarkedMatch[1]); chapterGroups.forEach(groupDiv => { const chapLink = groupDiv.querySelector('a[href*="/chapter/"]'); if (!chapLink) return; let chapterNum = null; let chapterText = ''; // Try to get chapter number from h3 const h3s = chapLink.querySelectorAll('h3'); for (const h3 of h3s) { const match = h3.textContent.match(/Chapter\s*(\d+(?:\.\d+)?)/i); if (match) { chapterNum = parseFloat(match[1]); // Clean chapter text - only keep "Chapter X" format chapterText = `Chapter ${match[1]}`; break; } } if (!chapterNum) { // fallback: try to extract from href const chapterHref = chapLink.getAttribute('href'); const urlMatch = chapterHref.match(/chapter\/([\d.]+)/i); if (urlMatch) chapterNum = parseFloat(urlMatch[1]); } // Remove old classes groupDiv.classList.remove('chapter-bookmarked', 'chapter-unread'); chapLink.classList.remove('chapter-bookmarked', 'chapter-unread'); // Apply color classes to the group div and the link if (bookmarkedNum !== null && chapterNum !== null) { if (chapterNum === bookmarkedNum) { groupDiv.classList.add('chapter-bookmarked'); chapLink.classList.add('chapter-bookmarked'); } else if (chapterNum > bookmarkedNum) { groupDiv.classList.add('chapter-unread'); chapLink.classList.add('chapter-unread'); } } // Save on middle or left click const saveClick = () => { // Use enhanced fuzzy matching for chapter saves let matchingKey = findMatchingKey(pageTitle, bookmarks) || bookmarkKeyName; console.log('Series page saveClick - looking for:', pageTitle); console.log('Found matching key:', matchingKey); // Chapter text is already cleaned in the series page logic above // (it's set to `Chapter ${match[1]}` format) // Add to top when saving new chapter progress const newBookmarkEntry = { ...(bookmarks[matchingKey] || { title: matchingKey }), title: matchingKey, chapter: chapterText, url: seriesUrl, cover: bookmarks[matchingKey]?.cover || coverImg || '', lastRead: Date.now() }; // Remove existing entry and add to top delete bookmarks[matchingKey]; bookmarks = { [matchingKey]: newBookmarkEntry, ...bookmarks }; 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); // --- INITIALIZATION --- function waitForContent() { const observer = new MutationObserver((_, obs) => { if (document.querySelector('.grid-cols-12') || location.pathname.startsWith('/series/')) { obs.disconnect(); if (location.pathname.startsWith('/series/')) { document.body.setAttribute('data-series-page', 'true'); } else { document.body.removeAttribute('data-series-page'); } loadColors(); // Load colors before creating UI createUI(); updateTitleButtons(); } }); observer.observe(document.body, { childList: true, subtree: true }); } waitForContent(); })();