Book More

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();