Greasy Fork

Asura Bookmark Panel with Title Buttons + Import/Export

New Bookmark made to be clear enables you to have clear updates and saves and displays all youre data in a open way

当前为 2025-07-07 提交的版本,查看 最新版本

// ==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();
})();