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