bangumi-copy-title

Copy bangumi title to clipboard

// ==UserScript==
// @name        bangumi-copy-title
// @version     0.0.3
// @description Copy bangumi title to clipboard
// @author      Flynn Cao
// @namespace   https://flynncao.uk/
// @match       https://bangumi.tv/*
// @match       https://chii.in/*
// @match       https://bgm.tv/*
// @include     /^https?:\/\/(((fast\.)?bgm\.tv)|chii\.in|bangumi\.tv)*/
// @license     MIT
// ==/UserScript==
'use strict';

function createButton(
  { id, text, icon, className, onClick, disabled = false },
  userSettings = {},
) {
  // Create button with base class
  const button = $('<strong></strong>').html(icon).addClass(className)[0];

  button.id = id;

  if (
    Object.prototype.hasOwnProperty.call(userSettings, 'showText') &&
    userSettings.showText === true
  ) {
    // add a text named "显示标题' following the svg icon with font size  21px 21px
    const textNode = document.createTextNode(text);
    const span = document.createElement('span');
    span.append(textNode);
    button.append(span);
  }

  button.addEventListener('click', onClick);
  button.disabled = disabled;

  return button
}

function createCheckbox(
  { id, label, className, onChange, checked, disabled = false },
  userSettings = {},
) {
  // Create the checkbox container
  const labelEl = document.createElement('label');
  labelEl.className = className;

  // Create the checkbox input
  const inputEl = document.createElement('input');
  inputEl.type = 'checkbox';
  inputEl.id = id;
  inputEl.checked = checked;

  // Create the custom checkmark span
  const checkmarkEl = document.createElement('span');
  checkmarkEl.className = 'bct-checkmark';

  // Create the label text span
  const textSpan = document.createElement('span');
  textSpan.textContent = label;

  // Append elements to the label
  labelEl.append(inputEl);
  labelEl.append(checkmarkEl);
  labelEl.append(textSpan);

  inputEl.addEventListener('change', onChange);
  inputEl.disabled = disabled;

  return labelEl
}

const BGM_SUBJECT_REGEX =
  /^https:\/\/(((fast\.)?bgm\.tv)|(chii\.in)|(bangumi\.tv))\/subject\/\d+/;

const STORAGE_NAMESPACE = 'BangumiCopyTitle';

var butterupStyles = ".toaster{\n\tfont-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,\n\tNoto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n\tbox-sizing: border-box;\n\tpadding: 0;\n\tmargin: 0;\n\tlist-style: none;\n\toutline: none;\n\tz-index: 999999999;\n\tposition: fixed;\n\tpadding: 5px;\n}\n\n@keyframes spin {\nfrom {\n\ttransform: rotate(0deg);\n}\nto {\n\ttransform: rotate(360deg);\n}\n}\n\n.animate-spin {\nanimation: spin 1s linear infinite;\n}\n\n.toaster.bottom-right{\n\tbottom: 20px;\n\tright: 20px;\n}\n\n.toaster.bottom-left{\n\tbottom: 20px;\n\tleft: 20px;\n}\n\n.toaster.top-right{\n\ttop: 20px;\n\tright: 20px;\n}\n\n.toaster.top-left{\n\ttop: 20px;\n\tleft: 20px;\n}\n\n.toaster.bottom-center{\n\tbottom: 20px;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n}\n\n.toaster.top-center{\n\ttop: 20px;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n}\n\n.toaster.top-center ol.rack{\n\tflex-direction: column-reverse;\n}\n\n.toaster.top-left ol.rack{\n\tflex-direction: column-reverse;\n}\n\n.toaster.top-right ol.rack{\n\tflex-direction: column-reverse;\n}\n\n.toaster.bottom-center ol.rack{\n\tflex-direction: column;\n}\n\n.toaster.bottom-left ol.rack{\n\tflex-direction: column;\n}\n\n.toaster.bottom-right ol.rack{\n\tflex-direction: column;\n}\n\nol.rack{\n\tlist-style: none;\n\tpadding: 0;\n\tmargin: 0;\n\t/* reverse the list order so that the newest items are at the top */\n\tdisplay: flex;\n}\n\nol.rack li{\n\tmargin-bottom: 16px;\n}\n\n/* Stacked Toasts Enabled */\nol.rack.upperstack li{\n\tmargin-bottom: -35px;\n\ttransition: all 0.3s ease-in-out;\n}\n\nol.rack.upperstack li:hover{\n\tmargin-bottom: 16px;\n\tscale: 1.03;\n\ttransition: all 0.3s ease-in-out;\n}\n\nol.rack.lowerstack li{\n\tmargin-top: -35px;\n}\n\n\nol.rack.lowerstack{\n margin-bottom: 0px;\n}\n\n.butteruptoast{\n\tborder-radius: 8px;\n\tbox-shadow: 0 4px 12px #0000001a;\n\tfont-size: 13px;\n\tdisplay: flex;\n\tpadding: 16px;\n\tborder: 1px solid hsl(0, 0%, 93%);\n\tbackground-color: white;\n\tgap: 6px;\n\tcolor: #282828;\n\twidth: 325px;\n\ttransition: all 0.3s ease-in-out;\n}\n\n.butteruptoast.dismissable{\n\tcursor: pointer;\n}\n\n.butteruptoast .icon{\n\tdisplay: flex;\n\talign-items: start;\n\tflex-direction: column;\n}\n\n.butteruptoast .icon svg{\n\twidth: 20px;\n\theight: 20px;\n\tfill: #282828;\n\tpadding: 0;\n\tmargin: 0;\n}\n\n.butteruptoast .notif .desc{\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tpadding: 0;\n\tmargin: 0;\n}\n\n.butteruptoast .notif .desc .title{\n\tfont-weight: 600;\n\tline-height: 1.5;\n\tpadding: 0;\n\tmargin: 0;\n\n}\n\n.butteruptoast .notif .desc .message{\n\tfont-weight: 400;\n\tline-height: 1.4;\n\tpadding: 0;\n\tmargin: 0;\n}\n\n.butteruptoast.success{\n\tbackground-color: #ebfef2;\n\tcolor: hsl(140, 100%, 27%);\n\tborder: solid 1px hsl(145, 92%, 91%);\n}\n\n.butteruptoast.success .icon svg{\n\tfill: hsl(140, 100%, 27%);\n}\n\n.butteruptoast.error .icon svg{\n\tfill: hsl(0, 100%, 27%);\n}\n\n.butteruptoast.warning .icon svg{\n\tfill: hsl(50, 100%, 27%);\n}\n\n.butteruptoast.info .icon svg{\n\tfill: hsl(210, 100%, 27%);\n}\n\n.butteruptoast.error{\n\tbackground-color: #fef0f0;\n\tcolor: hsl(0, 100%, 27%);\n\tborder: solid 1px hsl(0, 92%, 91%);\n}\n\n.butteruptoast.warning{\n\tbackground-color: #fffdf0;\n\tcolor: hsl(50, 100%, 27%);\n\tborder: solid 1px hsl(50, 92%, 91%);\n}\n\n.butteruptoast.info{\n\tbackground-color: #f0f8ff;\n\tcolor: hsl(210, 100%, 27%);\n\tborder: solid 1px hsl(210, 92%, 91%);\n}\n\n/* Buttons */\n.toast-buttons{\n\tdisplay: flex;\n\tgap: 8px;\n\twidth: 100%;\n\talign-items: center;\n\tflex-direction: row;\n\tmargin-top: 16px;\n}\n\n.toast-buttons .toast-button.primary{\n\tbackground-color: #282828;\n\tcolor: white;\n\tpadding: 8px 16px;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\tborder: none;\n\twidth: 100%;\n}\n\n.toast-buttons .toast-button.secondary{\n\tbackground-color: #f0f8ff;\n\tcolor: hsl(210, 100%, 27%);\n\tborder: solid 1px hsl(210, 92%, 91%);\n\tpadding: 8px 16px;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\twidth: 100%;\n}\n\n/* Success toast buttons */\n.butteruptoast.success .toast-button.primary {\n\tbackground-color: hsl(145, 63%, 42%);\n\tcolor: white;\n}\n\n.butteruptoast.success .toast-button.secondary {\n\tbackground-color: hsl(145, 45%, 90%);\n\tcolor: hsl(145, 63%, 32%);\n\tborder: solid 1px hsl(145, 63%, 72%);\n}\n\n/* Error toast buttons */\n.butteruptoast.error .toast-button.primary {\n\tbackground-color: hsl(354, 70%, 54%);\n\tcolor: white;\n}\n\n.butteruptoast.error .toast-button.secondary {\n\tbackground-color: hsl(354, 30%, 90%);\n\tcolor: hsl(354, 70%, 44%);\n\tborder: solid 1px hsl(354, 70%, 74%);\n}\n\n/* Warning toast buttons */\n.butteruptoast.warning .toast-button.primary {\n\tbackground-color: hsl(45, 100%, 51%);\n\tcolor: hsl(45, 100%, 15%);\n}\n\n.butteruptoast.warning .toast-button.secondary {\n\tbackground-color: hsl(45, 100%, 96%);\n\tcolor: hsl(45, 100%, 31%);\n\tborder: solid 1px hsl(45, 100%, 76%);\n}\n\n/* Info toast buttons */\n.butteruptoast.info .toast-button.primary {\n\tbackground-color: hsl(207, 90%, 54%);\n\tcolor: white;\n}\n\n.butteruptoast.info .toast-button.secondary {\n\tbackground-color: hsl(207, 90%, 94%);\n\tcolor: hsl(207, 90%, 34%);\n\tborder: solid 1px hsl(207, 90%, 74%);\n}\n\n\n\n\n/* Entrance animations */\n/*  Note: These animations need to differ depending on the location of the toaster\n\tElements that are in the top should slide and fade down from the top\n\tElemennts that are in the bottom should slide and fade up from the bottom\n*/\n\n.toastUp{\n\tanimation: slideUp 0.5s ease-in-out;\n\tanimation-fill-mode: forwards;\n}\n\n.toastDown{\n\tanimation: slideDown 0.5s ease-in-out;\n\tanimation-fill-mode: forwards;\n}\n\n@keyframes slideDown {\n\t0% {\n\t\t\topacity: 0;\n\t\t\ttransform: translateY(-100%);\n\t}\n\t100% {\n\t\t\topacity: 1;\n\t\t\ttransform: translateY(0);\n\t}\n}\n\n@keyframes slideUp {\n\t0% {\n\t\t\topacity: 0;\n\t\t\ttransform: translateY(100%);\n\t}\n\t100% {\n\t\t\topacity: 1;\n\t\t\ttransform: translateY(0);\n\t}\n}\n\n.fadeOutToast{\n\tanimation: fadeOut 0.3s ease-in-out;\n\tanimation-fill-mode: forwards;\n}\n\n@keyframes fadeOut {\n\t0% {\n\t\t\topacity: 1;\n\t}\n\t100% {\n\t\t\topacity: 0;\n\t}\n}\n\n/*  Additional Styles\n\tThese styles are an alternative to the standard option. A user can choose to use these\n\tstyles by setting the theme: variable per toast\n*/\n\n/* Glass */\n\n.butteruptoast.glass{\n\tbackground-color: rgba(255, 255, 255, 0.42) !important;\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n\tborder: none;\n\tbox-shadow: 0 4px 12px #0000001a;\n\tcolor: #282828;\n}\n\n.butteruptoast.glass.success{\n\tbackground-color: rgba(235, 254, 242, 0.42) !important;\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n\tborder: none;\n\tbox-shadow: 0 4px 12px #0000001a;\n\tcolor: hsl(140, 100%, 27%);\n}\n\n.butteruptoast.glass.error{\n\tbackground-color: rgba(254, 240, 240, 0.42) !important;\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n\tborder: none;\n\tbox-shadow: 0 4px 12px #0000001a;\n\tcolor: hsl(0, 100%, 27%);\n}\n\n.butteruptoast.glass.warning{\n\tbackground-color: rgba(255, 253, 240, 0.42) !important;\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n\tborder: none;\n\tbox-shadow: 0 4px 12px #0000001a;\n\tcolor: hsl(50, 100%, 27%);\n}\n\n.butteruptoast.glass.info{\n\tbackground-color: rgba(240, 248, 255, 0.42) !important;\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n\tborder: none;\n\tbox-shadow: 0 4px 12px #0000001a;\n\tcolor: hsl(210, 100%, 27%);\n}\n\n/* brutalist */\n.butteruptoast.brutalist{\n\tborder-radius: 0px;\n\tbox-shadow: 0 4px 12px #0000001a;\n\tborder: solid 2px #282828;\n\tfont-size: 13px;\n\talign-items: center;\n\tdisplay: flex;\n\tpadding: 16px;\n\tbackground-color: white;\n\tgap: 6px;\n\tcolor: #282828;\n\twidth: 325px;\n}\n\n.butteruptoast.brutalist.success{\n\tbackground-color: #ebfef2;\n\tcolor: hsl(140, 100%, 27%);\n\tborder: solid 2px hsl(140, 100%, 27%);\n}\n\n.butteruptoast.brutalist.error{\n\tbackground-color: #fef0f0;\n\tcolor: hsl(0, 100%, 27%);\n\tborder: solid 2px hsl(0, 100%, 27%);\n}\n\n.butteruptoast.brutalist.warning{\n\tbackground-color: #fffdf0;\n\tcolor: hsl(50, 100%, 27%);\n\tborder: solid 2px hsl(50, 100%, 27%);\n}\n\n.butteruptoast.brutalist.info{\n\tbackground-color: #f0f8ff;\n\tcolor: hsl(210, 100%, 27%);\n\tborder: solid 2px hsl(210, 100%, 27%);\n}\n";

var styles = "\n\n.bct-button {\n  /* --button-size: 2rem;\n  width: var(--button-size);\n  height: var(--button-size); */\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  color: #000;\n  transform: translateY(4px);\n  padding: 2px 5px;\n  border: 1px solid transparent;\n}\n\n[data-theme=\"dark\"] .bct-button {\n\tcolor: #f5f5f5;\n} \n\n.bct-button:hover {\n\tborder: 1px solid lightgray;\n\tborder-radius: 4px;\n\ttransition: all 0.2s ease-in-out;\n}\n\n.bct-button svg {\n  width: 100%;\n  height: 100%;\n  /* Let the button control the size */\n  flex: 1;\n}\n\n.bct-button svg {\n  max-width: 21px;\n  max-height: 21px;\n}\n\n\n.bct-button span{\n\tfont-size: 12px!important;\n\tfont-weight: normal!important;\n\tpadding-right: 4px!important;\n}\n[data-theme=\"dark\"] .bct-button svg {\n\tfilter:invert(1)\n}\n\n.bct-checkbox {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n  height: 20px;\n  cursor: pointer;\n}\n\n.bct-checkbox .bct-checkmark {\n  /* You had no styles for this in the original, but leave this as a placeholder */\n\n}\n\n\n.bct-checkbox span:last-child {\n\tmargin-left: 2px;\n}\n";

// Forked from https://github.com/dgtlss/butterup
const butterup = {
  options: {
    maxToasts: 5, // Max number of toasts that can be on the screen at once
    toastLife: 5000, // How long a toast will stay on the screen before fading away
    currentToasts: 0, // Current number of toasts on the screen
  },
  toast({
    title,
    message,
    type,
    location,
    icon,
    theme,
    customIcon,
    dismissable,
    onClick,
    onRender,
    onTimeout,
    customHTML,
    primaryButton,
    secondaryButton,
    maxToasts,
    duration,
  }) {
    /* Check if the toaster exists. If it doesn't, create it. If it does, check if there are too many toasts on the screen.
			If there are too many, delete the oldest one and create a new one. If there aren't too many, create a new one. */
    if (document.querySelector('#toaster') == null) {
      // toaster doesn't exist, create it
      const toaster = document.createElement('div');
      toaster.id = 'toaster';
      if (location == null) {
        toaster.className = 'toaster top-right';
      } else {
        toaster.className = `toaster ${location}`;
      }

      // Create the toasting rack inside of the toaster
      document.body.append(toaster);

      // Create the toasting rack inside of the toaster
      if (document.querySelector('#butterupRack') == null) {
        const rack = document.createElement('ol');
        rack.id = 'butterupRack';
        rack.className = 'rack';
        toaster.append(rack);
      }
    } else {
      const toaster = document.querySelector('#toaster');
      // check what location the toaster is in
      toaster.classList.forEach(function (item) {
        // remove any location classes from the toaster
        if (
          item.includes('top-right') ||
          item.includes('top-center') ||
          item.includes('top-left') ||
          item.includes('bottom-right') ||
          item.includes('bottom-center') ||
          item.includes('bottom-left')
        ) {
          toaster.classList.remove(item);
        }
      });
      if (location == null) {
        toaster.className = 'toaster top-right';
      } else {
        toaster.className = `toaster ${location}`;
      }
      document.querySelector('#butterupRack');
    }
    // Load Custom Options
    if (maxToasts != null) {
      butterup.options.maxToasts = maxToasts;
    }
    if (duration != null) {
      butterup.options.toastLife = duration;
    }
    // Check if there are too many toasts on the screen
    if (butterup.options.currentToasts >= butterup.options.maxToasts) {
      // there are too many toasts on the screen, delete the oldest one
      var oldestToast = document.querySelector('#butterupRack').firstChild;
      document.querySelector('#butterupRack').removeChild(oldestToast);
      butterup.options.currentToasts--;
    }

    // Create the toast
    const toast = document.createElement('li');
    butterup.options.currentToasts++;
    toast.className = 'butteruptoast';
    // Add entrance animation class
    toast.className += ' toast-enter';
    // if the toast class contains a top or bottom location, add the appropriate class to the toast
    if (
      toaster.className.includes('top-right') ||
      toaster.className.includes('top-center') ||
      toaster.className.includes('top-left')
    ) {
      toast.className += ' toastDown';
    }
    if (
      toaster.className.includes('bottom-right') ||
      toaster.className.includes('bottom-center') ||
      toaster.className.includes('bottom-left')
    ) {
      toast.className += ' toastUp';
    }
    toast.id = `butterupToast-${butterup.options.currentToasts}`;
    if (type != null) {
      toast.className += ` ${type}`;
    }

    if (theme != null) {
      toast.className += ` ${theme}`;
    }

    // Add the toast to the rack
    document.querySelector('#butterupRack').append(toast);

    // check if the user wants an icon
    if (icon != null && icon == true) {
      // add a div inside the toast with a class of icon
      const toastIcon = document.createElement('div');
      toastIcon.className = 'icon';
      toast.append(toastIcon);
      // check if the user has added a custom icon
      if (customIcon) {
        toastIcon.innerHTML = customIcon;
      }
      if (type != null && customIcon == null) {
        // add the type class to the toast
        toast.className += ` ${type}`;
        if (type == 'success') {
          toastIcon.innerHTML =
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">' +
            '<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />' +
            '</svg>';
        }
        if (type == 'error') {
          toastIcon.innerHTML =
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">' +
            '<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />' +
            '</svg>';
        }
        if (type == 'warning') {
          toastIcon.innerHTML =
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">' +
            '<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />' +
            '</svg>';
        }
        if (type == 'info') {
          toastIcon.innerHTML =
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">' +
            '<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />' +
            '</svg>';
        }
      }
    }

    // add a div inside the toast with a class of notif
    const toastNotif = document.createElement('div');
    toastNotif.className = 'notif';
    toast.append(toastNotif);

    // add a div inside of notif with a class of desc
    const toastDesc = document.createElement('div');
    toastDesc.className = 'desc';
    toastNotif.append(toastDesc);

    // check if the user added a title
    if (title != null) {
      const toastTitle = document.createElement('div');
      toastTitle.className = 'title';
      toastTitle.innerHTML = title;
      toastDesc.append(toastTitle);
    }

    if (customHTML != null) {
      const toastHTML = document.createElement('div');
      toastHTML.className = 'message';
      toastHTML.innerHTML = customHTML;
      toastDesc.append(toastHTML);
    }

    // check if the user added a message
    if (message != null) {
      const toastMessage = document.createElement('div');
      toastMessage.className = 'message';
      toastMessage.innerHTML = message;
      toastDesc.append(toastMessage);
    }

    // Add buttons if specified
    if (primaryButton || secondaryButton) {
      const buttonContainer = document.createElement('div');
      buttonContainer.className = 'toast-buttons';
      toastNotif.append(buttonContainer);

      if (primaryButton) {
        const primaryBtn = document.createElement('button');
        primaryBtn.className = 'toast-button primary';
        primaryBtn.textContent = primaryButton.text;
        primaryBtn.addEventListener('click', function (event) {
          event.stopPropagation();
          primaryButton.onClick(event);
        });
        buttonContainer.append(primaryBtn);
      }

      if (secondaryButton) {
        const secondaryBtn = document.createElement('button');
        secondaryBtn.className = 'toast-button secondary';
        secondaryBtn.textContent = secondaryButton.text;
        secondaryBtn.addEventListener('click', function (event) {
          event.stopPropagation();
          secondaryButton.onClick(event);
        });
        buttonContainer.append(secondaryBtn);
      }
    }

    // Check if the user has mapped any custom click functions
    if (onClick && typeof onClick === 'function') {
      toast.addEventListener('click', function (event) {
        // Prevent the click event from triggering dismissal if the toast is dismissable
        event.stopPropagation();
        onClick(event);
      });
    }

    // Call onRender callback if provided
    if (onRender && typeof onRender === 'function') {
      onRender(toast);
    }

    if (dismissable != null && dismissable == true) {
      // Add a class to the toast to make it dismissable
      toast.className += ' dismissable';
      // when the item is clicked on, remove it from the DOM
      toast.addEventListener('click', function () {
        butterup.despawnToast(toast.id);
      });
    }

    // Remove the entrance animation class after the animation has finished
    setTimeout(function () {
      toast.classList.remove('toast-enter');
    }, 300); // Adjust timing as needed

    // despawn the toast after the specified time
    setTimeout(function () {
      if (onTimeout && typeof onTimeout === 'function') {
        onTimeout(toast);
      }
      butterup.despawnToast(toast.id);
    }, butterup.options.toastLife);
  },
  despawnToast(toastId, onClosed) {
    var toast = document.getElementById(toastId);
    if (toast != null) {
      toast.classList.add('toast-exit');
      setTimeout(function () {
        try {
          toast.remove();
          butterup.options.currentToasts--;
          if (onClosed && typeof onClosed === 'function') {
            onClosed(toast);
          }
        } catch {
          // do nothing
        }
        // if this was the last toast on the screen, remove the toaster
        if (butterup.options.currentToasts == 0) {
          var toaster = document.querySelector('#toaster');
          toaster.remove();
        }
      }, 300); // Adjust timing to match your CSS animation duration
    }
  },
  promise({ promise, loadingMessage, successMessage, errorMessage, location, theme }) {
    const toastId = `butterupToast-${butterup.options.currentToasts + 1}`;

    // Create initial loading toast
    this.toast({
      message: loadingMessage || 'Loading...',
      location,
      theme,
      icon: true,
      customIcon:
        '<svg class="animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>',
      dismissable: false,
    });

    // Update toast based on promise outcome
    return promise.then(
      (result) => {
        this.updatePromiseToast(toastId, {
          type: 'success',
          message: successMessage || 'Operation successful',
          icon: true,
        });
        return result
      },
      (error) => {
        this.updatePromiseToast(toastId, {
          type: 'error',
          message: errorMessage || 'An error occurred',
          icon: true,
        });
        throw error
      },
    )
  },
  updatePromiseToast(toastId, { type, message, icon }) {
    const toast = document.getElementById(toastId);
    if (toast) {
      toast.className = toast.className.replaceAll(/success|error|warning|info/g, '');
      toast.classList.add(type);

      const messageEl = toast.querySelector('.message');
      if (messageEl) {
        messageEl.textContent = message;
      }

      const iconEl = toast.querySelector('.icon');
      if (iconEl && icon) {
        iconEl.innerHTML = this.getIconForType(type);
      }

      // Reset the toast lifetime
      clearTimeout(toast.timeoutId);
      toast.timeoutId = setTimeout(() => {
        this.despawnToast(toastId);
      }, this.options.toastLife);
    }
  },
  getIconForType(type) {
    switch (type) {
      case 'success':
        return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /></svg>'
      case 'error':
        return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" /></svg>'
      case 'warning':
        return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /></svg>'
      case 'info':
        return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /></svg>'
      default:
        return ''
    }
  },
};

// https://www.iconfont.cn/collections/detail?spm=a313x.user_detail.i1.dc64b3430.2d233a81lHbKxM&cid=7077
const Icons = {
  copy: '<svg t="1747748621659" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8232" data-darkreader-inline-fill="" width="256" height="256"><path d="M682.666667 341.333333h128v469.333334H341.333333v-128H213.333333V213.333333h469.333334v128z m0 85.333334v256h-256v42.666666h298.666666v-298.666666h-42.666666zM298.666667 298.666667v298.666666h298.666666V298.666667H298.666667z" fill="#444444" p-id="8233" data-darkreader-inline-fill="" style="--darkreader-inline-fill: var(--darkreader-background-444444, #33373a);"></path></svg>',
};

// eslint-disable-next-line unicorn/no-static-only-class
class Storage {
  static set(key, value) {
    localStorage.setItem(`${STORAGE_NAMESPACE}_${key}`, JSON.stringify(value));
  }

  static get(key) {
    const value = localStorage.getItem(`${STORAGE_NAMESPACE}_${key}`);
    return value ? JSON.parse(value) : undefined
  }

  static async init(settings) {
    const keys = Object.keys(settings);
    for (const key of keys) {
      const value = Storage.get(key);
      if (value === undefined) {
        Storage.set(key, settings[key]);
      }
    }
  }
}

(async function () {
  // Validate if the current page is a Bangumi subject page
  if (!BGM_SUBJECT_REGEX.test(location.href)) {
    return
  }

  // Storage
  Storage.init({
    copyJapaneseTitle: false,
    showText: true,
  });

  const userSettings = {
    copyJapaneseTitle: Storage.get('copyJapaneseTitle') || false,
    showText: Storage.get('showText') || true,
  };

  // Layout and Events
  const injectStyles = () => {
    const styleEl = document.createElement('style');
    styleEl.textContent = styles;
    document.head.append(styleEl);
    const butterupStyleEl = document.createElement('style');
    butterupStyleEl.textContent = butterupStyles;
    document.head.append(butterupStyleEl);
  };
  injectStyles();
  // Render a toast notification in the top-right corner of the screen

  $('h1.nameSingle').append(
    createButton(
      {
        id: 'bct-copy-title',
        text: '复制',
        icon: Icons.copy,
        className: 'bct-button',
        onClick: () => {
          const title = userSettings.copyJapaneseTitle
            ? $('h1.nameSingle').find('a').text().trim()
            : $('h1.nameSingle').find('a').attr('title');
          navigator.clipboard.writeText(title);

          butterup.toast({
            title: `已复制${userSettings.copyJapaneseTitle ? '日文名' : '中文名'}到剪切板!`,
            location: 'top-right',
            dismissable: false,
            type: 'success',
            duration: 2500,
            icon: true,
          });
        },
      },
      userSettings,
    ),
  );

  $('h1.nameSingle').append(
    createCheckbox(
      {
        id: 'bct-hide-plain-comments',
        label: '日文名',
        className: 'bct-checkbox',
        onChange: (e) => {
          userSettings.copyJapaneseTitle = e.target.checked;
          Storage.set('copyJapaneseTitle', userSettings.copyJapaneseTitle);
        },
        checked: userSettings.copyJapaneseTitle,
        disabled: false,
      },
      userSettings,
    ),
  );
})();