// ==UserScript==
// @name Asura Bookmark Manager
// @namespace Violentmonkey Scripts
// @match https://asuracomic.net/*
// @grant none
// @version 3.0
// @icon https://asuracomic.net/images/logo.webp
// @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 completedKey = 'asuraManualCompleted';
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);
let completed = load(completedKey);
// --- 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
completed: '#32CD32', // Lime green for completed 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: 5px;
background-color: #4b0082; color: white;
padding: 11px 14px; border-radius: 8px;
z-index: 9999; border: none; cursor: pointer;
}
/* Bookmark panel */
.bookmark-panel {
position: fixed; top: 60px; right: 40px; width: 630px;
background: #1a1a1a; color: #fff; border: 1px solid #4b0082;
border-radius: 10px; padding: 10px; z-index: 9999;
display: none; max-height: 90vh; 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();
// First pass: exact match
for (const key in dataObject) {
const normalizedKey = key.toLowerCase()
.replace(/[''`]/g, '')
.replace(/\s+/g, ' ')
.replace(/[^\w\s]/g, '')
.trim();
if (normalizedKey === normalizedSearch) {
return key;
}
}
// Second pass: fuzzy match with stricter rules
for (const key in dataObject) {
const normalizedKey = key.toLowerCase()
.replace(/[''`]/g, '')
.replace(/\s+/g, ' ')
.replace(/[^\w\s]/g, '')
.trim();
// Split into words for comparison
const searchWords = normalizedSearch.split(' ').filter(w => w.length > 2);
const keyWords = normalizedKey.split(' ').filter(w => w.length > 2);
// Only match if at least 70% of words overlap and word count is similar
const overlap = searchWords.filter(word => keyWords.includes(word)).length;
const overlapRatio = overlap / Math.max(searchWords.length, keyWords.length);
const lengthRatio = Math.min(searchWords.length, keyWords.length) / Math.max(searchWords.length, keyWords.length);
// Require high overlap and similar length to prevent false matches
if (overlapRatio >= 0.7 && lengthRatio >= 0.6) {
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') {
// Show want-to-read in *reverse* order (newest on top)
items = Object.values(wantToRead).reverse();
} else if (tab === 'completed') {
// Show completed in reverse order (newest completed on top)
items = Object.values(completed).reverse();
} 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 || '';
// Add last read date for all tabs
const lastReadEl = document.createElement('div');
lastReadEl.style.fontSize = '12px';
lastReadEl.style.color = '#888';
lastReadEl.textContent = `Last read: ${obj.lastRead ? new Date(obj.lastRead).toLocaleDateString() : 'Unknown'}`;
info.appendChild(titleEl);
info.appendChild(chapterEl);
info.appendChild(lastReadEl);
// 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);
// Move to Completed
const completedBtn = document.createElement('button');
completedBtn.className = 'asura-btn';
completedBtn.textContent = '✅';
completedBtn.title = 'Mark as Completed';
completedBtn.onclick = () => {
completed[cleanTitle] = { ...obj, title: cleanTitle, lastRead: Date.now() };
delete bookmarks[cleanTitle];
save(bookmarkKey, bookmarks);
save(completedKey, completed);
updatePanel(container, tab);
updateTitleButtons();
};
btnGroup.appendChild(completedBtn);
// 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 completed[cleanTitle];
delete hidden[cleanTitle];
save(bookmarkKey, bookmarks);
save(wantKey, wantToRead);
save(completedKey, completed);
save(hideKey, hidden);
updatePanel(container, tab);
updateTitleButtons();
};
btnGroup.appendChild(removeBtn);
}
// Want to read tab: 📌❌
else if (tab === 'want') {
// 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 completed[cleanTitle];
delete hidden[cleanTitle];
save(bookmarkKey, bookmarks);
save(wantKey, wantToRead);
save(completedKey, completed);
save(hideKey, hidden);
updatePanel(container, tab);
updateTitleButtons();
};
btnGroup.appendChild(removeBtn);
}
// Completed tab: 📌📙❌
else if (tab === 'completed') {
// 📌 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 completed[cleanTitle];
save(bookmarkKey, bookmarks);
save(completedKey, completed);
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 completed[cleanTitle];
save(wantKey, wantToRead);
save(completedKey, completed);
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 completed[cleanTitle];
save(hideKey, hidden);
save(completedKey, completed);
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 completed[cleanTitle];
delete hidden[cleanTitle];
save(bookmarkKey, bookmarks);
save(wantKey, wantToRead);
save(completedKey, completed);
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 completed[cleanTitle];
delete hidden[cleanTitle];
save(bookmarkKey, bookmarks);
save(wantKey, wantToRead);
save(completedKey, completed);
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="completed">✅ Completed</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),
completed: load(completedKey),
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 completed
if (importedData.completed) {
const currentCompleted = load(completedKey);
Object.assign(currentCompleted, importedData.completed);
save(completedKey, currentCompleted);
}
// 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);
completed = load(completedKey);
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(completedKey);
localStorage.removeItem(hideKey);
bookmarks = {};
wantToRead = {};
completed = {};
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>Completed titles:</label>
<input type="color" id="color-completed" value="${colors.completed}">
<input type="text" id="text-completed" value="${colors.completed}">
</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="apply-colors-btn" style="background: #4b0082; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; margin-right: 10px;">
✅ Apply Colors
</button>
<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>
`;
// Store temporary colors while user is adjusting
let tempColors = { ...colors };
// Update only temp colors without applying (no lag)
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 temp value only
colorInput.addEventListener('input', (e) => {
const newColor = e.target.value;
textInput.value = newColor;
tempColors[colorKey] = newColor;
});
// Text input changes color and temp value only
textInput.addEventListener('input', (e) => {
const newColor = e.target.value;
if (/^#([0-9A-F]{3}){1,2}$/i.test(newColor)) {
colorInput.value = newColor;
tempColors[colorKey] = newColor;
}
});
}
});
// Apply button applies all temp colors at once
document.getElementById('apply-colors-btn').onclick = () => {
colors = { ...tempColors };
saveColors();
updateTitleButtons();
};
document.getElementById('reset-colors-btn').onclick = () => {
if (confirm('Reset all colors to default values?')) {
colors = {
bookmarked: '#c084fc',
wantToRead: '#FFD700',
defaultTitle: '#00BFFF',
completed: '#32CD32',
chapterBookmarked: '#c084fc',
chapterUnread: '#1cdf2d',
chapterBookmarkedBg: '#45025f',
chapterUnreadBg: '#414101'
};
tempColors = { ...colors };
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();
};
} else if (tab === 'completed') {
const completedItems = Object.entries(completed).map(([title, obj]) => ({
title,
chapter: obj.chapter || '',
url: obj.url || '',
cover: obj.cover || '',
lastRead: obj.lastRead || 0
}));
let completedHTML = `
<div class="settings-section">
<h4>✅ Completed Manga (${completedItems.length})</h4>
`;
if (completedItems.length === 0) {
completedHTML += '<p style="color: #888; font-style: italic;">No completed manga</p>';
} else {
completedItems.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();
completedHTML += `
<div class="panel-entry">
<img src="${obj.cover || ''}" alt="">
<div class="info">
<strong>${cleanTitle || 'No title'}</strong>
<div>${obj.chapter || ''}</div>
<div style="font-size: 12px; color: #888;">Last read: ${obj.lastRead ? new Date(obj.lastRead).toLocaleDateString() : 'Unknown'}</div>
<span>
<button class="asura-btn" onclick="moveCompletedToBookmarks('${cleanTitle}')" title="Move to Bookmarks">📌</button>
<button class="asura-btn" onclick="moveCompletedToWant('${cleanTitle}')" title="Move to Want to Read">📙</button>
<button class="asura-btn" onclick="moveCompletedToHidden('${cleanTitle}')" title="Move to Hidden">❌</button>
<button class="asura-btn" onclick="removeCompletedItem('${cleanTitle}')" title="Remove completely">Remove</button>
</span>
</div>
</div>
`;
});
}
completedHTML += '</div>';
settingsContent.innerHTML = completedHTML;
// Add global functions for completed item management
window.moveCompletedToBookmarks = (title) => {
const obj = completed[title];
if (obj) {
bookmarks[title] = { ...obj, title, chapter: obj.chapter || 'Chapter 0' };
delete completed[title];
save(bookmarkKey, bookmarks);
save(completedKey, completed);
updateSettingsContent('completed');
updateTitleButtons();
}
};
window.moveCompletedToWant = (title) => {
const obj = completed[title];
if (obj) {
wantToRead[title] = { ...obj, title };
delete completed[title];
save(wantKey, wantToRead);
save(completedKey, completed);
updateSettingsContent('completed');
updateTitleButtons();
}
};
window.moveCompletedToHidden = (title) => {
const obj = completed[title];
if (obj) {
hidden[title] = { cover: obj.cover };
delete completed[title];
save(hideKey, hidden);
save(completedKey, completed);
updateSettingsContent('completed');
updateTitleButtons();
}
};
window.removeCompletedItem = (title) => {
delete bookmarks[title];
delete wantToRead[title];
delete completed[title];
delete hidden[title];
save(bookmarkKey, bookmarks);
save(wantKey, wantToRead);
save(completedKey, completed);
save(hideKey, hidden);
updateSettingsContent('completed');
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();
// First pass: exact match
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) {
return key;
}
}
// Second pass: fuzzy match with stricter rules
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();
// Split into words for comparison
const searchWords = normalizedSearch.split(' ').filter(w => w.length > 2);
const keyWords = normalizedKey.split(' ').filter(w => w.length > 2);
// Only match if at least 70% of words overlap and word count is similar
const overlap = searchWords.filter(word => keyWords.includes(word)).length;
const overlapRatio = overlap / Math.max(searchWords.length, keyWords.length);
const lengthRatio = Math.min(searchWords.length, keyWords.length) / Math.max(searchWords.length, keyWords.length);
// Require high overlap and similar length to prevent false matches
if (overlapRatio >= 0.7 && lengthRatio >= 0.6) {
return key;
}
}
return null;
}
// Set title color based on status using user-selected colors
const matchingBookmarkKey = findMatchingKey(title, bookmarks);
const matchingWantKey = findMatchingKey(title, wantToRead);
const matchingCompletedKey = findMatchingKey(title, completed);
if (matchingCompletedKey) {
titleLink.style.color = colors.completed; // use user color for completed
} else 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
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,}$/, '');
// Find best matching bookmark key using enhanced fuzzy matching
let bookmarkKeyName = findMatchingKey(pageTitle, bookmarks) || pageTitle;
let wantKeyName = findMatchingKey(pageTitle, wantToRead) || pageTitle;
let completedKeyName = findMatchingKey(pageTitle, completed) || pageTitle;
// --- Set title color based on current status (NO BUTTONS) ---
if (completed[completedKeyName]) {
titleHeader.style.color = colors.completed;
} else if (wantToRead[wantKeyName]) {
titleHeader.style.color = colors.wantToRead;
} else if (bookmarks[bookmarkKeyName]) {
titleHeader.style.color = colors.bookmarked;
} else {
titleHeader.style.color = colors.defaultTitle;
}
// --- Chapter highlighting and save (keep existing logic) ---
// 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;
let wantKeyMatch = findMatchingKey(pageTitle, wantToRead) || wantKeyName;
// Try to get a cover image if missing
let cover = bookmarks[matchingKey]?.cover;
if (!cover) {
const posterImg = document.querySelector('img[alt="poster"].rounded.mx-auto.md\\:mx-0') ||
document.querySelector('img[alt="poster"].rounded.mx-auto') ||
document.querySelector('img[alt="poster"]');
cover = posterImg?.src || '';
}
// Remove from wantToRead if present before adding to bookmarks
if (wantToRead[wantKeyMatch]) {
delete wantToRead[wantKeyMatch];
save(wantKey, wantToRead);
}
// Add to top when saving new chapter progress
const newBookmarkEntry = {
...(bookmarks[matchingKey] || { title: matchingKey }),
title: matchingKey,
chapter: chapterText,
url: location.pathname,
cover,
lastRead: Date.now()
};
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();
})();