您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Track your manga reading progress with bookmarks, want-to-read list, and remove titles.
// ==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 // ==/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 = ` <div style="padding: 20px;"> <div class="settings-tabs"> <button class="settings-tab-btn active" data-settings-tab="general">🔧 General</button> <button class="settings-tab-btn" data-settings-tab="colors">🎨 Colors</button> <button class="settings-tab-btn" data-settings-tab="hidden">🚫 Hidden</button> </div> <div id="settings-content"></div> </div> `; const settingsContent = container.querySelector('#settings-content'); const settingsTabs = container.querySelectorAll('.settings-tab-btn'); let currentSettingsTab = 'general'; function updateSettingsContent(tab) { if (tab === 'general') { settingsContent.innerHTML = ` <div class="settings-section"> <h4>📤 Import/Export Data</h4> <button id="export-btn" style="background: #4b0082; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; margin-right: 10px;"> 📤 Export All Data </button> <button id="import-btn" style="background: #4b0082; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer;"> 📥 Import Data </button> <input type="file" id="import-file" accept=".json" style="display: none;"> <div style="font-size: 12px; color: #888; margin-top: 10px;"> Export saves all your bookmarks, want-to-read, and hidden lists to a JSON file.<br> Import will merge data with existing entries (newer entries take priority). </div> </div> <div class="settings-section"> <h4>🗑️ Quick Actions</h4> <button id="clear-all-btn" style="background: #dc2626; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer;"> 🗑️ Clear All Data </button> </div> `; // Add event listeners for general settings document.getElementById('export-btn').onclick = () => { const allData = { bookmarks: load(bookmarkKey), wantToRead: load(wantKey), hidden: load(hideKey), colors: colors, exportDate: new Date().toISOString() }; const blob = new Blob([JSON.stringify(allData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `asura-bookmarks-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); }; // Import functionality document.getElementById('import-btn').onclick = () => { document.getElementById('import-file').click(); }; document.getElementById('import-file').onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const importedData = JSON.parse(e.target.result); // Merge bookmarks if (importedData.bookmarks) { const currentBookmarks = load(bookmarkKey); Object.assign(currentBookmarks, importedData.bookmarks); save(bookmarkKey, currentBookmarks); } // Merge want-to-read if (importedData.wantToRead) { const currentWant = load(wantKey); Object.assign(currentWant, importedData.wantToRead); save(wantKey, currentWant); } // Merge hidden if (importedData.hidden) { const currentHidden = load(hideKey); Object.assign(currentHidden, importedData.hidden); save(hideKey, currentHidden); } // Reload data bookmarks = load(bookmarkKey); wantToRead = load(wantKey); hidden = load(hideKey); alert('Data imported successfully!'); updateTitleButtons(); } catch (error) { alert('Error importing data: ' + error.message); } }; reader.readAsText(file); }; // Clear all functionality document.getElementById('clear-all-btn').onclick = () => { if (confirm('Are you sure you want to clear ALL bookmark data? This cannot be undone!')) { localStorage.removeItem(bookmarkKey); localStorage.removeItem(wantKey); localStorage.removeItem(hideKey); bookmarks = {}; wantToRead = {}; hidden = {}; alert('All data cleared!'); updateTitleButtons(); } }; } else if (tab === 'colors') { settingsContent.innerHTML = ` <div class="settings-section"> <h4>🎨 Title Colors</h4> <div class="color-input-group"> <label>Bookmarked titles:</label> <input type="color" id="color-bookmarked" value="${colors.bookmarked}"> <input type="text" id="text-bookmarked" value="${colors.bookmarked}"> </div> <div class="color-input-group"> <label>Want to read titles:</label> <input type="color" id="color-wantToRead" value="${colors.wantToRead}"> <input type="text" id="text-wantToRead" value="${colors.wantToRead}"> </div> <div class="color-input-group"> <label>Default titles:</label> <input type="color" id="color-defaultTitle" value="${colors.defaultTitle}"> <input type="text" id="text-defaultTitle" value="${colors.defaultTitle}"> </div> </div> <div class="settings-section"> <h4>📖 Chapter Colors</h4> <div class="color-input-group"> <label>Last read chapter:</label> <input type="color" id="color-chapterBookmarked" value="${colors.chapterBookmarked}"> <input type="text" id="text-chapterBookmarked" value="${colors.chapterBookmarked}"> </div> <div class="color-input-group"> <label>Unread chapters:</label> <input type="color" id="color-chapterUnread" value="${colors.chapterUnread}"> <input type="text" id="text-chapterUnread" value="${colors.chapterUnread}"> </div> <div class="color-input-group"> <label>Last read background:</label> <input type="color" id="color-chapterBookmarkedBg" value="${colors.chapterBookmarkedBg}"> <input type="text" id="text-chapterBookmarkedBg" value="${colors.chapterBookmarkedBg}"> </div> <div class="color-input-group"> <label>Unread background:</label> <input type="color" id="color-chapterUnreadBg" value="${colors.chapterUnreadBg}"> <input type="text" id="text-chapterUnreadBg" value="${colors.chapterUnreadBg}"> </div> </div> <div class="settings-section"> <button id="reset-colors-btn" style="background: #dc2626; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer;"> 🔄 Reset to Default Colors </button> </div> `; // Fix: Always update both color and text inputs and colors object on change Object.keys(colors).forEach(colorKey => { const colorInput = document.getElementById(`color-${colorKey}`); const textInput = document.getElementById(`text-${colorKey}`); if (colorInput && textInput) { // Color picker changes text and value colorInput.addEventListener('input', (e) => { const newColor = e.target.value; textInput.value = newColor; colors[colorKey] = newColor; saveColors(); updateTitleButtons(); }); // Text input changes color and value textInput.addEventListener('input', (e) => { const newColor = e.target.value; if (/^#([0-9A-F]{3}){1,2}$/i.test(newColor)) { colorInput.value = newColor; colors[colorKey] = newColor; saveColors(); updateTitleButtons(); } }); } }); document.getElementById('reset-colors-btn').onclick = () => { if (confirm('Reset all colors to default values?')) { colors = { bookmarked: '#c084fc', wantToRead: '#FFD700', defaultTitle: '#00BFFF', chapterBookmarked: '#c084fc', chapterUnread: '#1cdf2d', chapterBookmarkedBg: '#45025f', chapterUnreadBg: '#414101' }; saveColors(); updateSettingsContent('colors'); updateTitleButtons(); } }; } else if (tab === 'hidden') { const hiddenItems = Object.entries(hidden).map(([title, obj]) => ({ title, chapter: '', url: '', cover: obj.cover || '' })); let hiddenHTML = ` <div class="settings-section"> <h4>🚫 Hidden Manga (${hiddenItems.length})</h4> `; if (hiddenItems.length === 0) { hiddenHTML += '<p style="color: #888; font-style: italic;">No hidden manga</p>'; } 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 += ` <div class="panel-entry"> <img src="${obj.cover || ''}" alt=""> <div class="info"> <strong>${cleanTitle || 'No title'}</strong> <span> <button class="asura-btn" onclick="unhideItem('${cleanTitle}')" title="Move to Bookmarks">📌</button> <button class="asura-btn" onclick="moveToWant('${cleanTitle}')" title="Move to Want to Read">📙</button> <button class="asura-btn" onclick="removeItem('${cleanTitle}')" title="Remove completely">Remove</button> </span> </div> </div> `; }); } hiddenHTML += '</div>'; settingsContent.innerHTML = hiddenHTML; // Add global functions for hidden item management window.unhideItem = (title) => { const obj = hidden[title]; if (obj) { bookmarks[title] = { ...obj, title, chapter: obj.chapter || 'Chapter 0' }; delete hidden[title]; save(bookmarkKey, bookmarks); save(hideKey, hidden); updateSettingsContent('hidden'); updateTitleButtons(); } }; window.moveToWant = (title) => { const obj = hidden[title]; if (obj) { wantToRead[title] = { ...obj, title }; delete hidden[title]; save(wantKey, wantToRead); save(hideKey, hidden); updateSettingsContent('hidden'); updateTitleButtons(); } }; window.removeItem = (title) => { delete bookmarks[title]; delete wantToRead[title]; delete hidden[title]; save(bookmarkKey, bookmarks); save(wantKey, wantToRead); save(hideKey, hidden); updateSettingsContent('hidden'); updateTitleButtons(); }; } } settingsTabs.forEach(tab => { tab.addEventListener('click', () => { settingsTabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); currentSettingsTab = tab.dataset.settingsTab; updateSettingsContent(currentSettingsTab); }); }); updateSettingsContent(currentSettingsTab); } // --- 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="want">📙 Want to Read</button> <button class="tab-btn" data-tab="settings">⚙️ Settings</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; if (currentTab === 'settings') { updateSettingsPanel(contentArea); } else { 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') { if (currentTab === 'settings') { updateSettingsPanel(contentArea); } else { 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}`; if (tabType === 'settings') tab.textContent = `⚙️ Settings`; }); } // --- TITLE BUTTONS --- let debouncedUpdateTitleButtons; function updateTitleButtons() { // --- Existing grid page logic --- 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; // Remove or comment out the debug log to avoid console spam // console.log('Main page title:', title); 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()); // Use old logic for cover image on non-/series/ pages const imgSrc = container?.querySelector('img.rounded-md.object-cover')?.src || ''; const btnGroup = document.createElement('span'); btnGroup.className = 'asura-btn-group'; // --- Enhanced fuzzy matching function --- function findMatchingKey(searchTitle, dataObject) { const normalizedSearch = searchTitle.toLowerCase() .replace(/[''`]/g, '') // Remove all types of apostrophes .replace(/\s+/g, ' ') .replace(/[^\w\s]/g, '') // Remove special characters except spaces .trim(); for (const key in dataObject) { const normalizedKey = key.toLowerCase() .replace(/[''`]/g, '') // Remove all types of apostrophes .replace(/\s+/g, ' ') .replace(/[^\w\s]/g, '') // Remove special characters except spaces .trim(); if (normalizedKey === normalizedSearch || normalizedKey.includes(normalizedSearch) || normalizedSearch.includes(normalizedKey)) { return key; } } return null; } // Set title color based on status using user-selected colors const matchingBookmarkKey = findMatchingKey(title, bookmarks); const matchingWantKey = findMatchingKey(title, wantToRead); if (matchingWantKey) { const isLocked = wantToRead[matchingWantKey].locked; if (isLocked) { titleLink.style.color = colors.wantToRead; // use user color for want to read } else { titleLink.style.color = colors.defaultTitle; // use user color for default } } else if (matchingBookmarkKey) { titleLink.style.color = colors.bookmarked; // use user color for bookmarked } else { titleLink.style.color = colors.defaultTitle; // use user color for default } // Bookmarked 📌 if (matchingBookmarkKey) { const pinBtn = document.createElement('button'); pinBtn.className = 'asura-btn'; pinBtn.textContent = '📌'; pinBtn.title = 'Marked as read'; pinBtn.onclick = (e) => { e.preventDefault(); delete bookmarks[matchingBookmarkKey]; save(bookmarkKey, bookmarks); setTimeout(updateTitleButtons, 0); // Force full refresh }; btnGroup.appendChild(pinBtn); } else { const isWantLocked = matchingWantKey && wantToRead[matchingWantKey].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[matchingWantKey]; } else { if (matchingBookmarkKey) delete bookmarks[matchingBookmarkKey]; // Use existing matching title if found, otherwise use current title const titleToUse = findMatchingKey(title, wantToRead) || title; wantToRead[titleToUse] = { title: titleToUse, chapter: 'Chapter 0', url: href, cover: imgSrc, locked: true }; } save(bookmarkKey, bookmarks); save(wantKey, wantToRead); setTimeout(updateTitleButtons, 0); // Force full refresh }; 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(); if (matchingWantKey) delete wantToRead[matchingWantKey]; // Use existing matching title if found, otherwise use current title const titleToUse = findMatchingKey(title, bookmarks) || title; bookmarks[titleToUse] = { title: titleToUse, chapter: 'Chapter 0', url: href, cover: imgSrc }; save(bookmarkKey, bookmarks); save(wantKey, wantToRead); setTimeout(updateTitleButtons, 0); // Force full refresh }; btnGroup.appendChild(markBtn); // ❌ Hide const hideBtn = document.createElement('button'); hideBtn.className = 'asura-btn'; hideBtn.textContent = '❌'; hideBtn.title = 'Hide comic'; hideBtn.onclick = (e) => { e.preventDefault(); // Use existing matching title if found, otherwise use current title const titleToUse = findMatchingKey(title, hidden) || title; hidden[titleToUse] = { cover: imgSrc }; save(hideKey, hidden); setTimeout(updateTitleButtons, 0); // Force full refresh }; btnGroup.appendChild(hideBtn); } } titleLink.parentElement.appendChild(btnGroup); // --- Chapter Highlighting (last read = purple, above = yellow, below = red) --- const bookmarkedChapterRaw = (matchingBookmarkKey ? bookmarks[matchingBookmarkKey]?.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 = () => { // 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(); })();