Book More

Quick access buttons for Anna (ISBN/Title) and Libby (copies title) on Douban, NeoDb, and Goodreads

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Book More
// @namespace    https://example.com/
// @version      1.1.1
// @author       cccccc
// @description  Quick access buttons for Anna (ISBN/Title) and Libby (copies title) on Douban, NeoDb, and Goodreads
// @match        https://neodb.social/book/*
// @match        https://book.douban.com/subject/*
// @match        https://www.goodreads.com/book/*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // --------- Helper: safely extract book JSON-LD ----------
  function extractBookData() {
    const script = document.querySelector('script[type="application/ld+json"]');
    if (!script) return { title: '', isbn: '' };

    let data;
    try {
      data = JSON.parse(script.textContent.trim());
    } catch (_) {
      return { title: '', isbn: '' };
    }

    if (Array.isArray(data)) {
      data = data.find(x => x['@type'] === 'Book') || data[0] || {};
    }


    function decodeHtml(html) {
                const txt = document.createElement("textarea");
                txt.innerHTML = html;
                return txt.value;}

    const title = decodeHtml(data.name || data.title || '');
    const isbn = (data.isbn || '').replace(/-/g, '');
    return { title, isbn };
  }

  const { title, isbn } = extractBookData();
  if (!title) return;

  // --------- Build search URLs ----------
  const links = {
    annaIsbn: isbn ? `https://annas-archive.org/search?q=${isbn}` : null,
    annaTitle: `https://annas-archive.org/search?q=${encodeURIComponent(title)}`,
    libby: 'https://libbyapp.com/search'
  };

  // --------- Helper: apply simple hover ----------
  function applyHover(btn, bg, hoverBg, color, hoverColor) {
    btn.addEventListener('mouseenter', () => {
      btn.style.backgroundColor = hoverBg;
      btn.style.color = hoverColor;
    });
    btn.addEventListener('mouseleave', () => {
      btn.style.backgroundColor = bg;
      btn.style.color = color;
    });
  }

  // --------- Site configuration ----------
  const siteConfig = {
    neodb: {
      match: /^https:\/\/neodb\.social\/book\//,
      insertTarget() {
        const anchor = document.querySelector('.right.mark');
        return { parent: anchor?.parentNode, before: anchor };
      },
      container(el) {
        el.id = 'book-more-links';
        el.style.cssText = `
          float: right;
          clear: right;
          width: 25%;
          margin: 2rem 0;
          text-align: center;
        `;
      },
      buttonClass: 'bm-btn-neodb',
      hover: (btn) =>
        applyHover(
          btn,
          'transparent',
          'var(--pico-primary-hover-background)',
          'var(--pico-primary)',
          'var(--pico-primary-inverse)'
        )
    },

    douban: {
      match: /^https:\/\/book\.douban\.com\/subject\//,
      insertTarget() {
        const aside = document.querySelector('.subjectwrap .aside') ||
                      document.querySelector('.aside');
        return { parent: aside, before: aside?.firstChild };
      },
      container(el) {
        el.className = 'gray_ad no-border';
      },
      wrapper() {
        const w = document.createElement('div');
        w.className = 'mb8 pl';
        return w;
      },
      buttonWrapper(btn) {
        const meta = document.createElement('div');
        meta.className = 'meta';
        meta.appendChild(btn);
        return meta;
      },
      buttonClass: 'bm-btn-douban'
    },

    goodreads: {
      match: /^https:\/\/www\.goodreads\.com\/book\//,
      insertTarget() {
        const h = document.querySelector('#bookTitle') ||
                  document.querySelector('h1');
        return { parent: h?.parentNode, before: h };
      },
      container(el) {
        el.id = 'book-more-links';
        el.style.cssText = `
          margin: 1rem 0;
          padding: 0.5rem 0;
          display: flex;
          flex-wrap: wrap;
          gap: 0.5rem;
        `;
      },
      buttonClass: 'bm-btn-goodreads',
      hover: (btn) => applyHover(btn, '#f5f5f1', '#ddd', '#333', '#333')
    }
  };

  // --------- Detect site ----------
  let active = null;
  for (const key in siteConfig) {
    if (siteConfig[key].match.test(location.href)) {
      active = siteConfig[key];
      break;
    }
  }
  if (!active) return;

  // --------- Insert shared CSS ----------
  const css = `
    .bm-btn-neodb {
      display: block;
      width: 75%;
      margin: 0.3rem auto;
      padding: 0.5rem 1rem;
      font-size: 0.9rem;
      text-align: center;
      border-radius: var(--pico-border-radius);
      border: 1px solid var(--pico-primary-border);
      background: transparent;
      color: var(--pico-primary);
      text-decoration: none;
      cursor: pointer;
    }
    .bm-btn-douban {
      text-decoration: none;
      cursor: pointer;
    }
    .bm-btn-goodreads {
      display: inline-block;
      margin: 0.3rem;
      padding: 0.4rem 0.8rem;
      font-size: 0.85rem;
      border-radius: 4px;
      border: 1px solid #d0d0ce;
      background: #f5f5f1;
      color: #333;
      text-decoration: none;
      cursor: pointer;
    }
  `;
  const styleTag = document.createElement('style');
  styleTag.textContent = css;
  document.head.appendChild(styleTag);

  // --------- Button creation ----------
  function makeButton(label, href) {
    const btn = document.createElement('a');
    btn.href = href;
    btn.target = '_blank';
    btn.rel = 'noopener noreferrer';
    btn.textContent = label;
    btn.className = active.buttonClass;
    if (active.hover) active.hover(btn);
    return btn;
  }

  function makeLibbyButton() {
    const btn = document.createElement('a');
    btn.href = '#';
    btn.textContent = 'Libby';
    btn.className = active.buttonClass;
    if (active.hover) active.hover(btn);

    btn.addEventListener('click', async (e) => {
      e.preventDefault();
      try {
        await navigator.clipboard.writeText(title);
      } catch (err) {
        alert('Copy to clipboard failed.');
      }
      window.open(links.libby, '_blank', 'noopener');
    });

    return btn;
  }

  // --------- Construct DOM ----------
  const container = document.createElement('div');
  active.container(container);

  const wrapper = active.wrapper ? active.wrapper() : container;

  const addBtn = (el) => {
    const node = active.buttonWrapper ? active.buttonWrapper(el) : el;
    wrapper.appendChild(node);
  };

  if (links.annaIsbn) addBtn(makeButton('Anna (ISBN)', links.annaIsbn));
  addBtn(makeButton('Anna (Title)', links.annaTitle));
  addBtn(makeLibbyButton());

  if (wrapper !== container) {
    container.appendChild(wrapper);
  }

  const { parent, before } = active.insertTarget();
  if (parent) parent.insertBefore(container, before);
})();