Greasy Fork is available in English.

Pixiv - Fast add bookmark

Add, edit or remove bookmark (illustration/animation/manga/novel) with one click.

// ==UserScript==
// @name         Pixiv - Fast add bookmark
// @namespace    https://www.github.com/soosad
// @version      2.3.0
// @description  Add, edit or remove bookmark (illustration/animation/manga/novel) with one click.
// @author       https://www.github.com/soosad
// @match        *://www.pixiv.net/*
// @run-at       document-end
// @require      https://unpkg.com/react@16/umd/react.production.min.js
// @require      https://unpkg.com/react-dom@16/umd/react-dom.production.min.js
// @license      MIT
// ==/UserScript==

(function main() {
  const isReact = () => typeof globalInitData === 'object';
  const token = () => (isReact() ? globalInitData.token : pixiv.context.token);
  const getUserId = () => (isReact() ? globalInitData.userData.id : pixiv.user.id);
  const rc = React.Component;
  const rce = React.createElement;
  const rdr = ReactDOM.render;
  const rdcp = ReactDOM.createPortal;
  const pfb = {
    pathData: {
      heartPathBorder:
        'M21,5.5 C24.8659932,5.5 28,8.63400675 28,12.5 C28,18.2694439 24.2975093,'
        + '23.1517313 17.2206059,27.1100183'
        + 'C16.4622493,27.5342993 15.5379984,27.5343235 14.779626,27.110148 C7.70250208,'
        + '23.1517462 4,18.2694529 4,12.5'
        + 'C4,8.63400691 7.13400681,5.5 11,5.5 C12.829814,5.5 14.6210123,6.4144028 16,7.8282366'
        + 'C17.3789877,6.4144028 19.170186,5.5 21,5.5 Z',
      heartPathBackground:
        'M16,11.3317089 C15.0857201,9.28334665 13.0491506,7.5 11,7.5'
        + 'C8.23857625,7.5 6,9.73857647 6,12.5 C6,17.4386065 9.2519779,21.7268174 15.7559337,'
        + '25.3646328'
        + 'C15.9076021,25.4494645 16.092439,25.4494644 16.2441073,25.3646326 C22.7480325,'
        + '21.7268037 26,17.4385986 26,12.5'
        + 'C26,9.73857625 23.7614237,7.5 21,7.5 C18.9508494,7.5 16.9142799,9.28334665 16,'
        + '11.3317089 Z',
      padlockPathBorder:
        'M29.9796 20.5234C31.1865 21.2121 32 22.511 32 24V28C32 30.2091 30.2091 '
        + '32 28 32H21'
        + 'C18.7909 32 17 30.2091 17 28V24C17 22.511 17.8135 21.2121 19.0204 20.5234C19.2619 '
        + '17.709 21.623 15.5 24.5 15.5'
        + 'C27.377 15.5 29.7381 17.709 29.9796 20.5234Z',
      padlockPathBackground:
        'M28 22C29.1046 22 30 22.8954 30 24V28C30 29.1046 29.1046 30 28 30H21'
        + 'C19.8954 30 19 29.1046 19 28V24C19 22.8954 19.8954 22 21 22V21C21 19.067 '
        + '22.567 17.5 24.5 17.5'
        + 'C26.433 17.5 28 19.067 28 21V22ZM23 21C23 20.1716 23.6716 19.5 24.5 19.5C25.3284 19.5 '
        + '26 20.1716 26 21V22H23V21Z',
      dotMenu:
        'M16,18 C14.8954305,18 14,17.1045695 14,16 C14,14.8954305 14.8954305,14 16,'
        + '14 C17.1045695,14 18,14.8954305 18,16 C18,17.1045695 17.1045695,18 16,18 Z M9,'
        + '18 C7.8954305,18 7,17.1045695 7,16 C7,14.8954305 7.8954305,14 9,14 C10.1045695,'
        + '14 11,14.8954305 11,16 C11,17.1045695 10.1045695,18 9,18 Z M23,18 C21.8954305,18 21,'
        + '17.1045695 21,16 C21,14.8954305 21.8954305,14 23,14 C24.1045695,14 25,14.8954305 25,'
        + '16 C25,17.1045695 24.1045695,18 23,18 Z',
    },
    classList: {
      MAIN_ID: 'pfbMain',
      PORTAL_ID: 'pfbPortal',
      BUTTON_HEART: 'pfb_button-heart',
      ANCHOR_HEART: 'pfb_anchor-heart',
      HEART_EMPTY: 'pfb_heart-empty',
      HEART_PUBLIC: 'pfb_heart-public',
      HEART_PRIVATE: 'pfb_heart-private',
      HEART_BORDER: 'pfb_heart-border',
      HEART_BACKGROUND: 'pfb_heart-background',
      PADLOCK_BORDER: 'pfb_padlock-border',
      PADLOCK_BACKGROUND: 'pfb_padlock-background',
      NIGHT_THEME: 'pfb_night-theme',
      MAIN_CONTAINER: 'pfb_container',
      LIGHT_PANEL: 'pfb_light-panel',
      ADVANCED_PANEL: 'pfb_advanced-panel',
      ADD_BUTTON: 'pfb_add-button',
      REMOVE_BUTTON: 'pfb_remove-button',
      MORE_BUTTON: 'pfb_more-button',
      BUTTON_CONTAINER: 'pfb_button-container',
      BOOKMAKRED: 'pfb_bookmarked',
      ACTION_SECTION: 'pfb_action-section',
      COMMENT_SECTION: 'pfb_comment-section',
      TITLE_SECTION: 'pfb_title-section',
      TAGS_SECTION: 'pfb_tags-section',
      WORKS_TAGS: 'pfb_work-tags',
      TITLE_TAG_LIST: 'pfb_title-tag-list',
      TAG: 'pfb_tag',
      TAG_ADDED: 'pfb_tag-added',
      ADVANCED_PANEL_SECTION: 'pfb_section',
      PANEL: 'pfb_panel',
      ADVANCED_PANEL_HEADER: 'pfb_header',
      TITLE: 'pfb_title',
      CLOSE_ADVANCED_PANEL: 'pfb_close-advanced-panel',
      ACTION_BUTTONS: 'pfb_action-btns',
      ACTION_THEME: 'pfb_action-theme',
      NIGHT_THEME_BUTTON: 'pfb_night-theme-btn',
      LIGHT_THEME_BUTTON: 'pfb_light-theme-btn',
      FLOAT_CONTAINER_ID: 'pfb_float-container',
      SVG_BOOKMARKED: 'btBeIl',
      SVG_NONE: 'inFaFn',
      FLOAT_BUTTON_CONTAINER: 'pfb_f-btn-container',
      FLOAT_BUTTON: 'pfb_f-btn',
      FLOAT_BTN_BOOKMARKED: 'pfb_f-bookmarked',
      FLOAT_SVG_HEART: 'pfb_f-svg-heart',
      FLOAT_PATH_BORDER: 'pfb_f-path-heart-border',
      FLOAT_PATH_BACKGROUND: 'pfb_f-path-heart-background',
      FLOAT_PATH_PADLOCK_BORDER: 'pfb_f-path-padlock-border',
      FLOAT_PATH_PADLOCK_BACKGROUND: 'pfb_f-path-padlock-background',
      FLOAT_SVG_LINE: 'pfb_f-svg-line',
    },
    selectorsList: {
      currentWorkHeartSelector: '#root main section section > div:nth-child(3)',
      currentWorkHeartNavSelector: 'article > div > figure + section .jcSCsn + div > div > button',
      currentNovelHeartSelector: 'article section > div:nth-child(3)',
      heartButtonSelector: 'button.bAzGIE',
      heartImgSelector: '._one-click-bookmark',
      placeIllustrationSelector: '#root main section section',
      placeNovelSelector: 'article section',
      portalIllustrationSelector: '#root figure > figcaption ul + div',
      portalNovelSelector: '#root article footer + ul + div',
      numberOfBookmarksSelector: 'dd[title=Bookmarks]',
      pixivWelcomeTitle: 'h2.welcome',
      pixivErrorTitle: 'h2.error-title',
      tagNovel: '.tags > .tag > a.text',
      tagIllust: 'figcaption footer > ul > li a',
      closestDivCont: 'div[width]',
      closestDivContNovel: 'li',
      closestDivContNovelSize: 'section > div > div > div',
      anchorNovel: 'a[href*="/novel/show.php?id="]',
      anchorIllust: 'a[href*="/artworks/"]',
    },
    regexList: {
      novelPath: new RegExp('^\\/novel\\/show\\.php$', 'g'),
      novelPath2: new RegExp('\\/novel\\/show\\.php\\?.*id=\\d+.*'),
      illustPath: new RegExp('\\/artworks\\/\\d+', 'g',),
    },
    urlList: {
      workData(workType, id) {
        return `https://www.pixiv.net/ajax/${workType}/${id}`;
      },
      bookmarkDataUrl(workType, illustId) {
        return `https://www.pixiv.net/ajax/${workType}/${illustId}/bookmarkData`;
      },
      bookmarkTagsUrl(worksType, userId) {
        return `https://www.pixiv.net/ajax/user/${userId}/${worksType}/bookmark/tags`;
      },
      illustBookmarkUrl(illustId) {
        return `https://www.pixiv.net/bookmark_add.php?type=illust&illust_id=${illustId}`;
      },
      novelBookmarkUrl(novelId) {
        return `https://www.pixiv.net/novel/bookmark_add.php?id=${novelId}`;
      },
      novelBookmarkDetailUrl(novelId) {
        return `https://www.pixiv.net/novel/bookmark_detail.php?id=${novelId}`;
      },
    },
    scriptData: {
      cssVersion: '220',
      isUserScript: true,
      pfbnightTitle: 'pfbnight',
    },
    fetchData: {
      urlList: {
        addIllustBookmarkUrl: '/ajax/illusts/bookmarks/add',
        removeIllustBookmarkUrl: '/rpc/index.php',
        removeNovelBookmarkUrl: '/ajax/novels/bookmarks/delete',
        addNovelBookmarkUrl: '/ajax/novels/bookmarks/add',
      },
      args: {
        getArgs: {
          credentials: 'same-origin',
          headers: { Accept: 'application/json' },
        },
        bookmarkAdd: {
          credentials: 'same-origin',
          headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json; charset=utf-8',
            'X-CSRF-Token': token(),
          },
          method: 'POST',
        },
        bookmarkRemove: {
          credentials: 'same-origin',
          headers: {
            Accept: 'application/json',
            'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
            'X-CSRF-Token': token(),
          },
          method: 'POST',
        },
      },
      body: {
        illustRemove(bookmarkId) {
          return `mode=delete_illust_bookmark&bookmark_id=${bookmarkId}`;
        },
        novelRemove(bookmarkId) {
          return `del=1&book_id=${bookmarkId}`;
        },
      },
    },
    data: {},
    elementsList: {
      path([d, className]) {
        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.classList.add(className);
        path.setAttribute('d', d);
        return path;
      },
      heart(className, d) {
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('viewBox', '0 0 32 32');
        svg.setAttribute('width', '32');
        svg.setAttribute('height', '32');
        svg.classList.add(className);
        d.forEach((pathD) => {
          const path = this.path(pathD);
          svg.appendChild(path);
        });
        return svg;
      },
      publicHeart() {
        const { HEART_PUBLIC, HEART_BORDER, HEART_BACKGROUND } = pfb.classList;
        const { heartPathBorder, heartPathBackground } = pfb.pathData;
        const heart = this.heart(HEART_PUBLIC, [
          [heartPathBorder, HEART_BORDER],
          [heartPathBackground, HEART_BACKGROUND],
        ]);
        return heart;
      },
      privateHeart() {
        const {
          HEART_PRIVATE,
          HEART_BORDER,
          HEART_BACKGROUND,
          PADLOCK_BORDER,
          PADLOCK_BACKGROUND,
        } = pfb.classList;
        const {
          heartPathBorder,
          heartPathBackground,
          padlockPathBorder,
          padlockPathBackground,
        } = pfb.pathData;
        const heart = this.heart(HEART_PRIVATE, [
          [heartPathBorder, HEART_BORDER],
          [heartPathBackground, HEART_BACKGROUND],
          [padlockPathBorder, PADLOCK_BORDER],
          [padlockPathBackground, PADLOCK_BACKGROUND],
        ]);
        return heart;
      },
      emptyHeart() {
        const { HEART_EMPTY, HEART_BORDER, HEART_BACKGROUND } = pfb.classList;
        const { heartPathBorder, heartPathBackground } = pfb.pathData;
        const heart = this.heart(HEART_EMPTY, [
          [heartPathBorder, HEART_BORDER],
          [heartPathBackground, HEART_BACKGROUND],
        ]);
        return heart;
      },
      buttonNovel(props) {
        const { bookmarkCount, bookmarkId } = props;
        const { novelBookmarkUrl, novelBookmarkDetailUrl } = pfb.urlList;
        const { illustId } = pfb.data;
        return rce(
          'div',
          null,
          bookmarkId
            ? rce(
              'a',
              {
                href: novelBookmarkDetailUrl(illustId),
                className: 'bookmark-count _ui-tooltip',
                'data-tooltip': `${bookmarkCount} Bookmarks`,
              },
              rce('i', { className: '_icon _bookmark-icon-inline' }),
              bookmarkCount,
            )
            : null,
          rce(
            'a',
            {
              href: novelBookmarkUrl(illustId),
              className: `_bookmark-toggle-button ${
                bookmarkId ? 'bookmarked edit-bookmark' : 'add-bookmark'
              }`,
            },
            !bookmarkId ? rce('span', { className: 'bookmark-icon' }) : null,
            rce(
              'span',
              { className: 'description' },
              bookmarkId ? 'Edit bookmark' : 'Add to bookmarks',
            ),
          ),
        );
      },
      svgHeart(props) {
        const { isPrivateBookmark, bookmarkId } = props;
        const {
          HEART_EMPTY,
          HEART_PRIVATE,
          HEART_PUBLIC,
          HEART_BORDER,
          HEART_BACKGROUND,
          PADLOCK_BORDER,
          PADLOCK_BACKGROUND,
        } = pfb.classList;
        const {
          heartPathBorder,
          heartPathBackground,
          padlockPathBorder,
          padlockPathBackground,
        } = pfb.pathData;

        return rce(
          'svg',
          {
            className: `${HEART_EMPTY} ${
              bookmarkId ? `${isPrivateBookmark ? HEART_PRIVATE : HEART_PUBLIC}` : ''
            }`,
            viewBox: '0 0 32 32',
            width: 32,
            height: 32,
          },
          rce('path', { className: HEART_BORDER, d: heartPathBorder }),
          rce('path', { className: HEART_BACKGROUND, d: heartPathBackground }),
          isPrivateBookmark
            ? rce('path', { className: PADLOCK_BORDER, d: padlockPathBorder })
            : null,
          isPrivateBookmark
            ? rce('path', { className: PADLOCK_BACKGROUND, d: padlockPathBackground })
            : null,
        );
      },
      buttonHeart(props) {
        const {
          bookmarkId, addBookmark, isPrivateBookmark, disabledButtons,
        } = props;
        const { BUTTON_HEART } = pfb.classList;
        const { illustBookmarkUrl } = pfb.urlList;
        return bookmarkId
          ? rce(
            'a',
            { className: BUTTON_HEART, href: illustBookmarkUrl(pfb.data.illustId) },
            rce(pfb.elementsList.svgHeart, { isPrivateBookmark, bookmarkId }),
          )
          : rce(
            'button',
            {
              className: BUTTON_HEART,
              onClick: e => addBookmark(e, 0),
              disabled: disabledButtons,
            },
            rce(pfb.elementsList.svgHeart, { isPrivateBookmark, bookmarkId }),
          );
      },
      svgLine(props) {
        return rce(
          'svg',
          { viewBox: props.viewBox },
          props.lines.map((l, i) => rce('line', {
            key: i, x1: l[0], y1: l[1], x2: l[2], y2: l[3],
          })),
        );
      },
      svgPath(props) {
        return rce('svg', null, props.d.map((d, i) => rce('path', { d, key: i })));
      },
      buttonLight(props) {
        const { BUTTON_CONTAINER } = pfb.classList;
        return rce(
          'div',
          { className: BUTTON_CONTAINER },
          rce('button', { type: 'button', ...props }, props.children),
        );
      },
      themeButton(props) {
        const { onClick, className, title } = props;
        return rce(
          'button',
          { onClick, className, title },
          rce('svg', null, rce('rect', { x: 0, y: 0 })),
        );
      },
      header(props) {
        const {
          BOOKMAKRED,
          ADVANCED_PANEL_HEADER,
          TITLE,
          CLOSE_ADVANCED_PANEL,
          ACTION_BUTTONS,
          ACTION_THEME,
          NIGHT_THEME_BUTTON,
          LIGHT_THEME_BUTTON,
        } = pfb.classList;
        const {
          toggleMoreOptions,
          bookmarkId,
          isPrivateBookmark,
          addBookmark,
          removeBookmark,
          disabledButtons,
        } = props;
        return rce(
          'div',
          { className: ADVANCED_PANEL_HEADER },
          rce(
            'div',
            { className: TITLE },
            rce('h1', null, rce('div', null, rce('span', null, 'Update bookmark'))),
            rce(
              'button',
              { className: CLOSE_ADVANCED_PANEL, onClick: e => toggleMoreOptions(e, false) },
              rce(pfb.elementsList.svgLine, {
                viewBox: '0 0 8 8',
                lines: [['1', '1', '7', '7'], ['7', '1', '1', '7']],
              }),
            ),
          ),
          rce(
            'div',
            { className: ACTION_BUTTONS },
            rce(
              'button',
              {
                className: `${isPrivateBookmark === false ? BOOKMAKRED : ''}`,
                onClick: e => addBookmark(e, '0'),
                disabled: disabledButtons,
              },
              'Public',
            ),
            rce(
              'button',
              {
                className: `${isPrivateBookmark === true ? BOOKMAKRED : ''}`,
                onClick: e => addBookmark(e, '1'),
                disabled: disabledButtons,
              },
              'Private',
            ),
            bookmarkId
              ? rce(
                'button',
                {
                  onClick: e => removeBookmark(e, bookmarkId),
                  disabled: disabledButtons,
                },
                'Remove',
              )
              : null,
          ),
          rce(
            'div',
            { className: ACTION_THEME },
            rce('span', null, 'Theme:'),
            rce(
              'div',
              null,
              rce(pfb.elementsList.themeButton, {
                className: NIGHT_THEME_BUTTON,
                title: 'Night',
                onClick: e => pfb.changeTheme(e, true),
              }),
              rce(pfb.elementsList.themeButton, {
                className: LIGHT_THEME_BUTTON,
                title: 'Light',
                onClick: e => pfb.changeTheme(e, false),
              }),
            ),
          ),
        );
      },
      inputSection(props) {
        const {
          className, count, limit, onChange, value, title, placeholder, maxLength,
        } = props;
        return rce(
          'div',
          null,
          rce(
            'div',
            { className },
            rce('h2', null, title),
            rce('div', null, rce('span', null, count), rce('span', null, limit)),
          ),
          rce('input', {
            type: 'text', placeholder, value, onChange, maxLength,
          }),
        );
      },
      tagList(props) {
        const { TAG_ADDED, TITLE_TAG_LIST, TAG } = pfb.classList;
        const {
          listOfTags, tagsForBookmark, addTagToInput, text,
        } = props;
        return rce(
          'div',
          null,
          rce('span', { className: TITLE_TAG_LIST }, text),
          listOfTags.map((item, id) => rce(
            'span',
            {
              className: `${TAG} ${
                tagsForBookmark.includes(`${item.tag}`.toLowerCase()) ? TAG_ADDED : ''
              }`,
              key: id,
              onClick: e => addTagToInput(e, item.tag),
            },
            `${item.tag} (${item.cnt})`,
          )),
        );
      },
      action(props) {
        const {
          handleChange,
          inputTags,
          inputComment,
          workTags,
          addTagToInput,
          userTags,
          sortUserTags,
          workTagsLowerCase,
          nameSort,
          countSort,
        } = props;
        const { privateTags, publicTags } = userTags;
        const {
          ACTION_SECTION,
          COMMENT_SECTION,
          TITLE_SECTION,
          TAGS_SECTION,
          WORKS_TAGS,
          TITLE_TAG_LIST,
          TAG,
          TAG_ADDED,
        } = pfb.classList;
        const charCount = inputComment.length;
        const tags = inputTags.match(/\S+/g, '') || [];
        const tagsForBookmark = tags.map(item => `${item}`.toLowerCase());
        const tagsCount = tagsForBookmark.length;
        return rce(
          'div',
          { className: ACTION_SECTION },
          rce(
            'div',
            { className: COMMENT_SECTION },
            rce(pfb.elementsList.inputSection, {
              className: TITLE_SECTION,
              count: charCount,
              limit: '/140',
              onChange: e => handleChange(e, true),
              value: inputComment,
              title: 'Bookmark comment',
              placeholder: 'Leave a comment...',
              maxLength: 140,
              isTagsSection: false,
            }),
          ),
          rce(
            'div',
            { className: TAGS_SECTION },
            rce(pfb.elementsList.inputSection, {
              className: TITLE_SECTION,
              count: tagsCount,
              limit: '/10',
              onChange: e => handleChange(e, false),
              value: inputTags,
              title: 'Bookmark tags',
              placeholder: 'Add tags for your favourite bookmark',
              maxLength: 140,
              isTagsSection: true,
            }),
            rce(
              'div',
              { className: WORKS_TAGS },
              rce(
                'div',
                null,
                rce('span', { className: TITLE_TAG_LIST }, 'Tags for this work'),
                rce(
                  'div',
                  null,
                  [['Lower case', true], ['Orignal', false]].map((item, i) => rce(
                    'span',
                    {
                      className: 'pfb_work-tags-options',
                      key: i,
                      onClick: e => workTagsLowerCase(e, item[1]),
                    },
                    item[0],
                  )),
                ),
              ),
              rce(
                'div',
                null,
                workTags.map((item, id) => rce(
                  'span',
                  {
                    className: `${TAG} ${
                      tagsForBookmark.includes(`${item}`.toLowerCase()) ? TAG_ADDED : ''
                    }`,
                    key: id,
                    onClick: e => addTagToInput(e, item),
                  },
                  item,
                )),
              ),
            ),
            rce(
              'div',
              { className: WORKS_TAGS },
              rce(
                'div',
                null,
                rce('span', { className: TITLE_TAG_LIST }, 'Your bookmark tags'),
                rce(
                  'div',
                  null,
                  [
                    ['Sort by name', 0, 'nameSort', nameSort],
                    ['Sort by count', 1, 'countSort', countSort],
                  ].map((item, i) => rce(
                    'span',
                    {
                      className: 'pfb_work-tags-options',
                      key: i,
                      onClick: e => sortUserTags(e, item[1], item[2], item[3]),
                    },
                    item[0],
                  )),
                ),
              ),
              rce(
                'div',
                null,
                publicTags.length
                  ? rce(pfb.elementsList.tagList, {
                    listOfTags: publicTags,
                    addTagToInput,
                    tagsForBookmark,
                    text: 'Public:',
                  })
                  : null,
                privateTags.length
                  ? rce(pfb.elementsList.tagList, {
                    listOfTags: privateTags,
                    addTagToInput,
                    tagsForBookmark,
                    text: 'Private:',
                  })
                  : null,
              ),
            ),
          ),
        );
      },
      advancedPanel(props) {
        const { ADVANCED_PANEL, ADVANCED_PANEL_SECTION, PANEL } = pfb.classList;
        const {
          toggleMoreOptions,
          bookmarkId,
          isPrivateBookmark,
          addBookmark,
          inputComment,
          workTags,
          userTags,
          workTagsLowerCase,
          inputTags,
          detectClickOutsidePanel,
          addTagToInput,
          sortUserTags,
          nameSort,
          countSort,
          handleChange,
          removeBookmark,
          disabledButtons,
        } = props;
        return rce(
          'div',
          { className: ADVANCED_PANEL, onClick: e => detectClickOutsidePanel(e), title: '' },
          rce(
            'div',
            { className: ADVANCED_PANEL_SECTION },
            rce(
              'div',
              { className: PANEL },
              rce(pfb.elementsList.header, {
                toggleMoreOptions,
                bookmarkId,
                isPrivateBookmark,
                addBookmark,
                removeBookmark,
                disabledButtons,
              }),
              rce(pfb.elementsList.action, {
                handleChange,
                userTags,
                workTagsLowerCase,
                sortUserTags,
                workTags,
                nameSort,
                countSort,
                addTagToInput,
                inputComment,
                inputTags,
              }),
            ),
          ),
        );
      },
      lightPanel(props) {
        const {
          LIGHT_PANEL, MORE_BUTTON, ADD_BUTTON, REMOVE_BUTTON, BOOKMAKRED,
        } = pfb.classList;
        const { dotMenu } = pfb.pathData;
        const { buttonLight, svgLine, svgPath } = pfb.elementsList;
        const {
          isPrivateBookmark,
          bookmarkId,
          removeBookmark,
          addBookmark,
          toggleMoreOptions,
          disabledButtons,
        } = props;
        return rce(
          'div',
          { className: LIGHT_PANEL },
          rce(
            buttonLight,
            {
              className: MORE_BUTTON,
              title: 'More options',
              onClick: e => toggleMoreOptions(e, true),
            },
            rce(svgPath, { d: [dotMenu] }),
          ),
          bookmarkId
            ? rce(
              buttonLight,
              {
                className: REMOVE_BUTTON,
                title: 'Remove bookmark',
                onClick: e => removeBookmark(e, bookmarkId),
                disabled: disabledButtons,
              },
              rce(svgLine, {
                viewBox: '0 0 8 8',
                lines: [['1', '1', '7', '7'], ['7', '1', '1', '7']],
              }),
            )
            : null,
          rce(
            buttonLight,
            {
              className: `${ADD_BUTTON} ${isPrivateBookmark === true ? BOOKMAKRED : ''}`,
              onClick: e => addBookmark(e, '1'),
              disabled: disabledButtons,
            },
            'Private',
          ),
          rce(
            buttonLight,
            {
              className: `${ADD_BUTTON} ${isPrivateBookmark === false ? BOOKMAKRED : ''}`,
              onClick: e => addBookmark(e, '0'),
              disabled: disabledButtons,
            },
            'Public',
          ),
        );
      },
      floatContainer() {
        const { FLOAT_CONTAINER_ID } = pfb.classList;
        const div = document.createElement('div');
        div.id = FLOAT_CONTAINER_ID;
        return div;
      },
    },

    miniBookmarkInitialize() {
      const { FLOAT_CONTAINER_ID } = this.classList;
      document.body.appendChild(this.elementsList.floatContainer());
      class Mini extends rc {
        constructor(props) {
          super(props);
          this.state = {
            illustsMini: {},
            novelsMini: {},
            detected: false,
            show: false,
            currentWork: {
              currentId: null,
              isNovel: null,
              position: { top: '0px', left: '0px' },
            },
            btnsDisabled: true,
          };
          this.detectHeart = this.detectHeart.bind(this);
        }

        componentDidMount() {
          document.addEventListener('mouseover', this.detectHeart);
        }

        detectHeart(e) {
          const { target } = e;
          if (target.closest(`#${FLOAT_CONTAINER_ID}`)) return;
          const { heartButtonSelector, heartImgSelector } = pfb.selectorsList;
          const button = target.closest(heartButtonSelector) || target.closest(heartImgSelector);
          if (!button) {
            if (this.state.detected) {
              this.setState({ detected: false, show: false, btnsDisabled: true });
            }
            return;
          }
          if (this.state.detected) return;
          this.setState({ detected: true });
          const ss = (stateM, isNovel, id, isPrivate, bookmarkId, position, show, btnsDisabled) => {
            this.setState(prevState => ({
              [stateM]: {
                ...prevState[stateM],
                [id]: { id, isPrivate, bookmarkId },
              },
              currentWork: { currentId: id, position, isNovel },
              show,
              btnsDisabled,
            }));
          };
          let id;
          let isBookmarked;
          let isNovel;
          if (pfb.data.isReactApp) {
            const { novelPath } = pfb.regexList;
            const {
              closestDivCont,
              closestDivContNovel,
              closestDivContNovelSize,
              anchorNovel,
              anchorIllust,
            } = pfb.selectorsList;
            const { SVG_BOOKMARKED, SVG_NONE } = pfb.classList;
            const elem = button.closest(closestDivCont)
              || button.closest(closestDivContNovel)
              || button.closest(closestDivContNovelSize);
            if (!elem) return;
            const { href } = elem.querySelector(anchorNovel) || elem.querySelector(anchorIllust);
            isNovel = new URL(href).pathname.match(novelPath);
            const illId = href.match(/\/artworks\/\d+/)[0].match(/\d+/)[0];
            id = +illId;
            const svg = button.querySelector('svg');
            isBookmarked = svg.classList.contains(SVG_BOOKMARKED)
              || !svg.classList.contains(SVG_NONE);
          } else {
            const { id: workId, type } = button.dataset;
            isNovel = type === 'novel';
            id = workId;
            isBookmarked = button.classList.contains('on');
          }
          const { top, left } = button.getBoundingClientRect();
          const position = {
            top: `${top + window.pageYOffset}px`,
            left: `${left + window.pageXOffset}px`,
          };
          const stateM = isNovel ? 'novelsMini' : 'illustsMini';
          if (isBookmarked) {
            if (this.state[stateM][id]) {
              this.setState({
                currentWork: { currentId: id, position, isNovel },
                show: true,
                btnsDisabled: false,
              });
              return;
            }
            pfb.loadBookmarkData(isNovel, id).then((response) => {
              const { error, body } = response;
              if (error) return;
              const { private: isPriv = null, id: bookId = null } = body.bookmarkData || {};
              ss(stateM, isNovel, id, isPriv, bookId, position, true, false);
            });
          } else ss(stateM, isNovel, id, null, null, position, true, false);
        }

        addBookmark(e, illustId, isNovel, restrict) {
          this.setState({ btnsDisabled: true });
          const { bookmarkAdd } = pfb.fetchData.args;
          const id = isNovel ? 'novel_id' : 'illust_id';
          const data = {
            [id]: illustId,
            restrict,
            comment: '',
            tags: [],
          };
          const body = JSON.stringify(data);
          const args = { ...bookmarkAdd, body };
          const ss = (stateM, props) => {
            this.setState(prevState => ({
              [stateM]: {
                ...prevState[stateM],
                [illustId]: {
                  ...prevState[stateM][illustId],
                  ...props,
                },
              },
              btnsDisabled: false,
            }));
            pfb.updateHeart(illustId, +restrict, isNovel);
          };
          pfb.saveBookmark(isNovel, args).then((response) => {
            const { body: respBody, error } = response;
            if (error) this.setState({ btnsDisabled: false });
            if (isNovel) {
              pfb.loadBookmarkData(isNovel, illustId).then((resp) => {
                const { error: err, body: bd } = resp;
                if (err) return;
                const { private: isPrivate = null, id: bookmarkId = null } = bd.bookmarkData || {};
                ss('novelsMini', { isPrivate, bookmarkId });
              });
            } else {
              const { last_bookmark_id: bookmarkId } = respBody;
              const isPrivateBookmark = !!+restrict;
              if (bookmarkId) ss('illustsMini', { isPrivate: isPrivateBookmark, bookmarkId });
              else ss('illustsMini', { isPrivate: isPrivateBookmark });
            }
          });
        }

        removeBookmark(e, id, bookmarkId, isNovel) {
          this.setState({ btnsDisabled: true });
          const { bookmarkRemove } = pfb.fetchData.args;
          const { illustRemove, novelRemove } = pfb.fetchData.body;
          if (!bookmarkId) return;
          const body = isNovel ? novelRemove(bookmarkId) : illustRemove(bookmarkId);
          const args = { ...bookmarkRemove, body };
          const stateM = isNovel ? 'novelsMini' : 'illustsMini';
          pfb.removeBookmark(isNovel, args).then((response) => {
            if (!response.error) {
              this.setState(prevState => ({
                [stateM]: {
                  ...prevState[stateM],
                  [id]: {
                    ...prevState[stateM][id],
                    isPrivate: null,
                    bookmarkId: null,
                  },
                },
                btnsDisabled: false,
              }));
              pfb.updateHeart(id, 2, isNovel);
            } else this.setState({ btnsDisabled: false });
          });
        }

        render() {
          const {
            currentWork: {
              currentId,
              isNovel,
              position: { top, left },
            },
            btnsDisabled: disabled,
            detected,
            show,
          } = this.state;
          const workType = isNovel ? 'novelsMini' : 'illustsMini';
          const { id, bookmarkId, isPrivate } = this.state[workType][currentId] || {};
          const {
            heartPathBorder,
            heartPathBackground,
            padlockPathBorder,
            padlockPathBackground,
          } = pfb.pathData;
          const {
            FLOAT_BUTTON_CONTAINER,
            FLOAT_BUTTON,
            FLOAT_BTN_BOOKMARKED,
            FLOAT_SVG_HEART,
            FLOAT_PATH_BORDER,
            FLOAT_PATH_BACKGROUND,
            FLOAT_PATH_PADLOCK_BORDER,
            FLOAT_PATH_PADLOCK_BACKGROUND,
            FLOAT_SVG_LINE,
          } = pfb.classList;
          const style = { top, left };
          return show && detected
            ? rce(
              'div',
              { style },
              rce(
                'div',
                { className: FLOAT_BUTTON_CONTAINER },
                rce(
                  'button',
                  {
                    onClick: e => this.addBookmark(e, id, isNovel, 0),
                    disabled,
                    className: `${FLOAT_BUTTON} ${
                      bookmarkId && !isPrivate ? FLOAT_BTN_BOOKMARKED : ''
                    }`,
                    title: 'Public bookmark',
                  },
                  rce(
                    'svg',
                    { className: FLOAT_SVG_HEART, viewBox: '0 0 34 34' },
                    rce('path', { className: FLOAT_PATH_BORDER, d: heartPathBorder }),
                    rce('path', {
                      className: FLOAT_PATH_BACKGROUND,
                      d: heartPathBackground,
                    }),
                  ),
                ),
              ),
              rce(
                'div',
                { className: FLOAT_BUTTON_CONTAINER },
                rce(
                  'button',
                  {
                    onClick: e => this.addBookmark(e, id, isNovel, 1),
                    disabled,
                    className: `${FLOAT_BUTTON} ${
                      bookmarkId && isPrivate ? FLOAT_BTN_BOOKMARKED : ''
                    }`,
                    title: 'Private bookmark',
                  },
                  rce(
                    'svg',
                    { className: FLOAT_SVG_HEART, viewBox: '0 0 34 34' },
                    rce('path', { className: FLOAT_PATH_BORDER, d: heartPathBorder }),
                    rce('path', {
                      className: FLOAT_PATH_BACKGROUND,
                      d: heartPathBackground,
                    }),
                    rce('path', {
                      className: FLOAT_PATH_PADLOCK_BORDER,
                      d: padlockPathBorder,
                    }),
                    rce('path', {
                      className: FLOAT_PATH_PADLOCK_BACKGROUND,
                      d: padlockPathBackground,
                    }),
                  ),
                ),
              ),
              bookmarkId
                ? rce(
                  'div',
                  { className: FLOAT_BUTTON_CONTAINER },
                  rce(
                    'button',
                    {
                      onClick: e => this.removeBookmark(e, currentId, bookmarkId, isNovel),
                      className: FLOAT_BUTTON,
                      title: 'Remove bookmark',
                      disabled,
                    },
                    rce(
                      'svg',
                      { className: FLOAT_SVG_LINE, viewBox: '0 0 8 8' },
                      rce('line', {
                        x1: '1', y1: '1', x2: '7', y2: '7',
                      }),
                      rce('line', {
                        x1: '7', y1: '1', x2: '1', y2: '7',
                      }),
                    ),
                  ),
                )
                : null,
            )
            : null;
        }
      }
      rdr(rce(Mini, null), document.getElementById(FLOAT_CONTAINER_ID));
    },
    pfbElementsInitialize() {
      const { isNovel } = this.data;
      const { MAIN_ID, PORTAL_ID } = this.classList;
      const {
        placeIllustrationSelector,
        placeNovelSelector,
        portalIllustrationSelector,
        portalNovelSelector,
        currentNovelHeartSelector,
        currentWorkHeartSelector,
      } = this.selectorsList;
      const placeSelector = isNovel ? placeNovelSelector : placeIllustrationSelector;
      // const portalSelector = isNovel ? portalNovelSelector : placeIllustrationSelector;
      const mainParent = document.querySelector(placeSelector);
      const portalParent = isNovel
        ? document.querySelector(portalNovelSelector)
        : document.querySelector(placeIllustrationSelector).parentElement.parentElement;
      const position = 'beforeend';
      if (isNovel && portalParent) {
        portalParent.insertAdjacentHTML('beforeend', '<span id="pfb-nv"></span>');
      }
      mainParent.insertAdjacentHTML(position, `<div id="${MAIN_ID}"></div>`);
      if (portalParent && !document.getElementById('pfbPortal')) {
        portalParent.insertAdjacentHTML('afterend', `<div id="${PORTAL_ID}"></div>`);
      }
      const modalRoot = document.getElementById(`${PORTAL_ID}`);
      const mainBookBtn = isNovel ? currentNovelHeartSelector : currentWorkHeartSelector;
      const buttonRoot = document.querySelector(mainBookBtn);
      class Modal extends rc {
        constructor(props) {
          super(props);
          this.container = document.createElement('div');
        }

        componentDidMount() {
          if (this.props.btn) {
            const childs = [...this.props.place.querySelectorAll('*')];
            childs.forEach(n => n.remove());
          }
          this.props.place.appendChild(this.container);
        }

        componentWillUnmount() {
          this.props.place.removeChild(this.container);
        }

        render() {
          return rdcp(this.props.children, this.container);
        }
      }
      class Container extends rc {
        constructor(props) {
          super(props);
          this.state = {
            bookmarkId: null,
            isPrivateBookmark: null,
            showAdvancedPanel: false,
            disabledButtons: true,
            nameSort: true,
            countSort: true,
            bookmarkCount: 1,
            inputTags: '',
            inputComment: '',
            workTags: [],
            originalWorkTags: [],
            allTags: [],
            userTags: { publicTags: [], privateTags: [] },
            userTagsOriginal: { publicTags: [], privateTags: [] },
          };
          this.toggleMoreOptions = this.toggleMoreOptions.bind(this);
          this.addBookmark = this.addBookmark.bind(this);
          this.removeBookmark = this.removeBookmark.bind(this);
          this.handleChange = this.handleChange.bind(this);
          this.addTagToInput = this.addTagToInput.bind(this);
          this.detectClickOutsidePanel = this.detectClickOutsidePanel.bind(this);
          this.workTagsLowerCase = this.workTagsLowerCase.bind(this);
          this.sortUserTags = this.sortUserTags.bind(this);
        }

        componentDidMount() {
          this.fetchUserTags();
          this.fetchWorkData();
        }

        fetchWorkData() {
          const { isNovel, illustId } = pfb.data;
          pfb.loadWorkData(isNovel, illustId).then((response) => {
            if (response.error) {
              this.getWorkTags();
              this.fetchBookmarkData();
            } else {
              const {
                bookmarkData,
                tags: { tags },
                isOriginal,
                userName,
                bookmarkCount,
              } = response.body;
              const {
                private: isPrivateBookmark = null, id: bookmarkId = null,
              } = bookmarkData || {};
              let workTags = [];
              if (userName) workTags.push(userName);
              if (isOriginal) workTags.push('Original');
              if (tags) {
                tags.forEach((i) => {
                  if (!i) return;
                  workTags.push(i.tag);
                  if (i.romaji) workTags.push(i.romaji);
                  if (i.translation) {
                    const transTags = Object.values(i.translation);
                    workTags = [...workTags, ...transTags];
                  }
                });
              }
              const mappedWT = workTags.map(tag => tag.replace(/\s+/g, ''));
              this.setState({
                isPrivateBookmark,
                bookmarkId,
                workTags: mappedWT,
                originalWorkTags: mappedWT,
                bookmarkCount,
                disabledButtons: false,
              });
            }
          });
        }

        addTagToInput(e, tag) {
          this.setState(prevState => ({ inputTags: `${prevState.inputTags} ${tag}` }));
        }

        handleChange(e, isCommentInput) {
          const { target } = e;
          const propState = isCommentInput ? 'inputComment' : 'inputTags';
          this.setState({ [propState]: target.value });
        }

        fetchUserTags() {
          const { userId, isNovel } = pfb.data;
          pfb.loadUserTags(userId, isNovel).then((response) => {
            const { error, body } = response;
            if (error) return;
            const { private: privateTags, public: publicTags } = body;
            privateTags.shift();
            publicTags.shift();
            const pub = publicTags.map(i => i.tag);
            const priv = privateTags.map(i => i.tag);
            const allTags = [...pub, ...priv, ...this.state.workTags];
            this.setState({
              userTags: { publicTags, privateTags },
              userTagsOriginal: { publicTags, privateTags },
              allTags,
            });
          });
        }

        getWorkTags() {
          const { isNovel } = pfb.data;
          const { tagNovel, tagIllust } = pfb.selectorsList;
          const tagSelector = isNovel ? tagNovel : tagIllust;
          const tagsEl = document.querySelectorAll(tagSelector);
          const tags = [...tagsEl].map(item => item.innerText.replace(/\s+/g, ''));
          this.setState({ workTags: tags, originalWorkTags: tags });
        }

        sortUserTags(e, type, property, rev) {
          let publicTags;
          let privateTags;
          const {
            publicTags: oldPublicTags,
            privateTags: oldPrivateTags,
          } = this.state.userTagsOriginal;
          const sortTagsByCount = (arr, isDesc) => arr.sort((vote1, vote2) => {
            if (+vote1.cnt > +vote2.cnt) return isDesc ? -1 : 1;
            if (+vote1.cnt < +vote2.cnt) return isDesc ? 1 : -1;
            if (`${vote1.tag}` > `${vote2.tag}`) return 1;
            if (`${vote1.tag}` < `${vote2.tag}`) return -1;
            return 0;
          });
          const sortTagsByName = (arr, isDesc) => arr.sort((vote1, vote2) => {
            if (`${vote1.tag}` > `${vote2.tag}`) return isDesc ? -1 : 1;
            if (`${vote1.tag}` < `${vote2.tag}`) return isDesc ? 1 : -1;
            if (+vote1.cnt > +vote2.cnt) return 1;
            if (+vote1.cnt < +vote2.cnt) return -1;
            return 0;
          });
          switch (type) {
            case 1:
              publicTags = sortTagsByCount(oldPublicTags, rev);
              privateTags = sortTagsByCount(oldPrivateTags, rev);
              break;
            case 0:
            default:
              publicTags = sortTagsByName(oldPublicTags, rev);
              privateTags = sortTagsByName(oldPrivateTags, rev);
              break;
          }
          this.setState({ userTags: { publicTags, privateTags }, [property]: !rev });
        }

        workTagsLowerCase(e, bool) {
          this.setState(prevState => ({
            workTags: bool
              ? prevState.originalWorkTags.map(i => `${i}`.toLowerCase())
              : prevState.originalWorkTags,
          }));
        }

        fetchBookmarkData() {
          const { isNovel, illustId } = pfb.data;
          pfb.loadBookmarkData(isNovel, illustId).then((data) => {
            const { error, body } = data;
            if (error) return;
            const {
              private: isPrivateBookmark = null, id: bookmarkId = null,
            } = body.bookmarkData || {};
            this.setState({ isPrivateBookmark, bookmarkId, disabledButtons: false });
          });
        }

        toggleMoreOptions(e, show) {
          this.setState({ showAdvancedPanel: show });
          if (show) document.body.classList.add('pfb_overflow');
          else document.body.classList.remove('pfb_overflow');
        }

        detectClickOutsidePanel(e) {
          const {
            target: { classList },
          } = e;
          if (classList.contains('pfb_section') || classList.contains('pfb_advanced-panel')) {
            this.toggleMoreOptions(null, false);
          }
        }

        addBookmark(e, restrict) {
          this.setState({ disabledButtons: true });
          const { inputComment, inputTags } = this.state;
          const { illustId, isNovel } = pfb.data;
          const { bookmarkAdd } = pfb.fetchData.args;
          const id = isNovel ? 'novel_id' : 'illust_id';
          const data = {
            [id]: illustId,
            restrict,
            comment: inputComment,
            tags: inputTags.match(/\S+/g, '') || [],
          };
          const body = JSON.stringify(data);
          const args = { ...bookmarkAdd, body };
          pfb.saveBookmark(isNovel, args).then((response) => {
            const { body: respBody, error } = response;
            if (error) this.setState({ disabledButtons: false });
            if (isNovel) this.fetchBookmarkData();
            else {
              const { last_bookmark_id: bookmarkId } = respBody;
              const isPrivateBookmark = !!+restrict;
              if (bookmarkId) {
                this.setState({ bookmarkId, isPrivateBookmark, disabledButtons: false });
              } else this.setState({ isPrivateBookmark, disabledButtons: false });
              if (!isNovel) {
                const { currentWorkHeartNavSelector } = pfb.selectorsList;
                const parent = document.querySelector(currentWorkHeartNavSelector);
                if (parent) pfb.replaceHeartSVG(parent, +restrict);
              }
            }
          });
        }

        removeBookmark(e, id) {
          this.setState({ disabledButtons: true });
          const { isNovel } = pfb.data;
          const { bookmarkId } = this.state;
          const { bookmarkRemove } = pfb.fetchData.args;
          const { illustRemove, novelRemove } = pfb.fetchData.body;
          if (!bookmarkId) return;
          const body = isNovel ? novelRemove(id) : illustRemove(id);
          const args = { ...bookmarkRemove, body };
          pfb.removeBookmark(isNovel, args).then((response) => {
            if (!response.error) {
              this.setState({ bookmarkId: null, isPrivateBookmark: null, disabledButtons: false });
              if (!isNovel) {
                const { currentWorkHeartNavSelector } = pfb.selectorsList;
                const parent = document.querySelector(currentWorkHeartNavSelector);
                if (parent) pfb.replaceHeartSVG(parent, 2);
              }
            } else this.setState({ disabledButtons: false });
          });
        }

        render() {
          const {
            bookmarkId,
            isPrivateBookmark,
            showAdvancedPanel,
            disabledButtons,
            inputComment,
            workTags,
            userTags,
            bookmarkCount,
            nameSort,
            countSort,
            inputTags,
          } = this.state;
          const {
            addBookmark,
            removeBookmark,
            toggleMoreOptions,
            handleChange,
            sortUserTags,
            addTagToInput,
            detectClickOutsidePanel,
            workTagsLowerCase,
          } = this;
          const { MAIN_CONTAINER } = pfb.classList;
          return rce(
            'div',
            { className: MAIN_CONTAINER },
            rce(pfb.elementsList.lightPanel, {
              bookmarkId,
              isPrivateBookmark,
              addBookmark,
              removeBookmark,
              toggleMoreOptions,
              disabledButtons,
            }),
            showAdvancedPanel
              ? rce(
                Modal,
                { place: modalRoot, btn: false },
                rce(pfb.elementsList.advancedPanel, {
                  bookmarkId,
                  detectClickOutsidePanel,
                  isPrivateBookmark,
                  addBookmark,
                  removeBookmark,
                  addTagToInput,
                  nameSort,
                  countSort,
                  userTags,
                  sortUserTags,
                  workTagsLowerCase,
                  workTags,
                  handleChange,
                  toggleMoreOptions,
                  disabledButtons,
                  inputComment,
                  inputTags,
                }),
              )
              : null,
            !pfb.data.isNovel && buttonRoot
              ? rce(
                Modal,
                { place: buttonRoot, btn: true },
                rce(pfb.elementsList.buttonHeart, {
                  bookmarkId,
                  isPrivateBookmark,
                  addBookmark,
                  disabledButtons,
                }),
              )
              : null,
          );
        }
      }
      rdr(rce(Container, null), document.getElementById(`${MAIN_ID}`));
    },
    css() {
      const style = document.createElement('style');
      style.type = 'text/css';
      style.innerText = '#root .pfb_container{margin-right:20px}#root .pfb_light-panel{width:auto}.pfb_li'
        + 'ght-panel{width:400px;display:flex;flex-direction:row-reverse}.pfb_light-panel>d'
        + 'iv:first-child{border-radius:0px 8px 8px 0px}.pfb_light-panel>div:last-child{bor'
        + 'der-radius:8px 0px 0px 8px}.pfb_button-container{height:32px;transition:backgrou'
        + 'nd-color 0.2s}.pfb_button-container>button{border:none;background:none;margin:0;'
        + 'padding:0;height:32px;cursor:pointer;font-weight:700;line-height:1;font-size:14p'
        + 'x}.pfb_container button:focus,.pfb_advanced-panel button:focus,#pfb_float-contai'
        + 'ner button:focus{outline:0}.pfb_button-container>button>svg{width:32px;height:32'
        + 'px}.pfb_add-button{padding:9px 14px !important}.pfb_remove-button{padding:8px 11'
        + 'px !important;width:38px}.pfb_remove-button>svg{stroke-linecap:round;stroke-widt'
        + 'h:2;width:16px !important;height:16px !important}.pfb_more-button{padding:0px 3p'
        + 'x !important}.pfb_overflow{overflow:hidden}.pfb_advanced-panel{display:flex;widt'
        + 'h:100%;height:100%;z-index:9999;position:fixed;font-size:20px;line-height:24px;f'
        + 'ont-weight:bold;top:0;left:0;overflow:auto}.pfb_section{width:800px;display:flex'
        + ';margin:auto;padding:40px;flex:none}.pfb_panel{width:100%;border-radius:8px}.pfb'
        + '_header{border-radius:8px 8px 0 0}.pfb_title{align-items:center;flex:none;displa'
        + 'y:flex;padding:16px;line-height:1;text-align:center;font-size:16px;font-weight:7'
        + '00}.pfb_title>h1{flex:auto;padding:0 24px;font-size:18px;margin:0}.pfb_title>h1>'
        + 'div{justify-content:center;display:flex;align-items:center}.pfb_close-advanced-p'
        + 'anel{align-self:flex-start;box-sizing:content-box;padding:4px;width:16px;margin-'
        + 'left:-24px;border:none;flex:none;outline:none;background:transparent;line-height'
        + ':0;font-size:0;cursor:pointer}.pfb_close-advanced-panel>svg{stroke-linecap:round'
        + ';stroke-width:2px;width:16px;height:16px}.pfb_action-btns{display:flex;margin:16'
        + 'px 16px 26px 16px;justify-content:center}.pfb_action-btns>button:first-child{bor'
        + 'der-radius:50px 0 0 50px}.pfb_action-btns>button:last-child{border-radius:0 50px'
        + ' 50px 0}.pfb_action-btns>button{background:none;padding:12px 0 !important;width:'
        + '200px;line-height:1;border:none;font-size:14px;font-weight:700;cursor:pointer;ma'
        + 'rgin:0;transition:background-color 0.4s}.pfb_action-theme{display:flex;justify-c'
        + 'ontent:flex-end;margin:0 24px}.pfb_action-theme>span{margin-right:10px;font-size'
        + ':14px;margin-top:2px}.pfb_action-theme>div{padding:5px 8px 0 8px}.pfb_action-the'
        + 'me>div>button{border:none;background:none;line-height:1;padding:0;cursor:pointer'
        + ';margin:0}.pfb_action-theme>div>button>svg{width:20px;height:20px}.pfb_action-th'
        + 'eme>div>button>svg>rect{width:100%;height:100%}.pfb_night-theme-btn{margin-right'
        + ':5px !important}.pfb_action-section{width:100%;border-radius:0 0 8px 8px}.pfb_co'
        + 'mment-section{padding:25px 35px 35px;border-bottom:1px solid}.pfb_tags-section>d'
        + 'iv,.pfb_comment-section>div{text-align:center}.pfb_tags-section>div>input,.pfb_c'
        + 'omment-section>div>input{overflow:hidden;resize:none;font-size:14px;height:25px;'
        + 'padding:6px 10px;border:none;border-radius:4px;width:643px;border:1px solid}.pfb'
        + '_title-section{display:flex;padding:0px 35px;justify-content:space-between}.pfb_'
        + 'title-section>h2{margin:5px 0 10px 0;font-size:16px}.pfb_title-section>div{font-'
        + 'size:12px;padding-top:6px;margin-right:5px}.pfb_work-tags-options{margin-right:5'
        + 'px;font-size:12px;cursor:pointer}.pfb_tags-section{padding:20px 35px}.pfb_tags-s'
        + 'ection>.pfb_title-section{padding-bottom:25px}.pfb_tags-section>div:nth-child(1)'
        + '{padding-bottom:30px;position:relative}.pfb_work-tags{font-size:14px;padding:0 3'
        + '2px;margin-bottom:16px}.pfb_work-tags>div{display:flex;flex-wrap:wrap}.pfb_work-'
        + 'tags>div:nth-child(1){margin:0 2px;justify-content:space-between}.pfb_work-tags>'
        + 'div:nth-child(2){border:1px solid;border-radius:5px;padding:5px 4px 3px}.pfb_wor'
        + 'k-tags>div:nth-child(2)>div{display:flex;flex-wrap:wrap}.pfb_work-tags>div:nth-c'
        + 'hild(2)>div:first-child{border-bottom:1px solid;margin-bottom:3px;padding-bottom'
        + ':2px}.pfb_work-tags>div:nth-child(2)>div:last-child{border-bottom:none;margin-bo'
        + 'ttom:0;padding-bottom:0}.pfb_title-tag-list{margin:0 3px}.pfb_tag{padding:0px 5p'
        + 'x;font-size:12px;margin:0px 1px 2px;cursor:pointer;font-size:12px;border-radius:'
        + '3px;transition:background-color 0.4s ease 0s}.pfb_tag-added{background:none}.pfb'
        + '_tag-added:hover{background:none}.pfb_button-heart{display:inline-block;box-sizi'
        + 'ng:content-box;padding:0;color:inherit;background:none;border:none;line-height:1'
        + ';height:32px;cursor:pointer}.pfb_heart-empty,.pfb_heart-public,.pfb-heart-privat'
        + 'e{box-sizing:border-box;line-height:0;font-size:0px;vertical-align:top;transitio'
        + 'n:color 0.2s ease 0s, fill 0.2s ease 0s}.pfb_heart-background{transition:fill 0.'
        + '2s ease 0s}.pfb_padlock-border,.pfb_padlock-background{fill-rule:evenodd;clip-ru'
        + 'le:evenodd}.pfb_action-btns>button:disabled,button.pfb_add-button:disabled{opaci'
        + 'ty:0.4}#pfb_float-container>div{z-index:9999;position:absolute;display:flex;heig'
        + 'ht:36px}#pfb_float-container>div>div:first-child>button{border-radius:5px 0 0 5p'
        + 'x}#pfb_float-container>div>div:last-child>button{border-radius:0 5px 5px 0}#pfb_'
        + 'float-container>div>.pfb_f-btn-container:nth-child(3)>button{padding-top:3px}#pf'
        + 'b_float-container button:disabled{opacity:0.9}#pfb_float-container .pfb_f-btn-co'
        + 'ntainer{background:none !important}.pfb_f-btn{padding:0 2px;border:none;width:40'
        + 'px;height:36px;cursor:pointer}.pfb_f-svg-heart{padding:3px 0 0 3px;height:33px}.'
        + 'pfb_f-svg-heart>path{fill-rule:evenodd}.pfb_f-svg-line{stroke-linecap:round;stro'
        + 'ke-width:2;width:18px !important;height:18px !important}.pfb_bookmarked{color:#0'
        + '086e0 !important}.pfb_light-panel .pfb_button-container{background-color:#ebebeb'
        + '}.pfb_light-panel .pfb_button-container>button{color:#666}.pfb_light-panel .pfb_'
        + 'button-container:hover{background-color:#dcdcdc}.pfb_light-panel .pfb_more-butto'
        + 'n>svg{fill:#666}.pfb_light-panel .pfb_remove-button>svg{stroke:#666}.pfb_advance'
        + 'd-panel{color:#333333;background-color:#00000066}.pfb_panel{background:#eee}.pfb'
        + '_header{background:#fff}.pfb_title{color:#333}.pfb_close-advanced-panel>svg{stro'
        + 'ke:#ccc}.pfb_action-btns>button{color:#666;background-color:#ededed}.pfb_action-'
        + 'btns>button:hover{background-color:#e2e2e2 !important}.pfb_action-theme>span{col'
        + 'or:#333}.pfb_action-theme>div{background:#eee}.pfb_night-theme-btn rect{fill:#22'
        + '2}.pfb_light-theme-btn rect{fill:#fff}.pfb_comment-section{border-color:#fff}.pf'
        + 'b_tags-section>div>input,.pfb_comment-section>div>input{background-color:#fff;bo'
        + 'rder-color:#222}.pfb_work-tags>div:nth-child(2){border-color:#666;background-col'
        + 'or:#ccc}.pfb_work-tags>div:nth-child(2)>div:first-child{border-color:#222}.pfb_t'
        + 'ag{color:#fff;background-color:#3e5b71}.pfb_tag:hover{background-color:#3f7186}.'
        + 'pfb_tag-added{color:#3e5b71;background:none}'
        + '.pfb_tags-section input[type="text"],.pfb_comment-se'
        + 'ction input[type="text"]{color:#333;background-color:#fff}.pfb_tags-section inpu'
        + 't[type="text"]:focus,.pfb_comment-section input[type="text"]:focus{color:#333;ba'
        + 'ckground-color:#fff}.pfb_heart-empty{color:#1f1f1f;fill:#222}.pfb_padlock-border'
        + ',.pfb_heart-background{fill:#fff}.pfb_heart-public,.pfb_heart-private{color:#ff4'
        + '060;fill:#ff4060}.pfb_heart-public .pfb_heart-background,.pfb_heart-private .pfb'
        + '_heart-background{fill:#ff4060}.pfb_padlock-background{fill:#1f1f1f}.pfb_f-btn{b'
        + 'ackground-color:#222}.pfb_f-btn:hover{background-color:#333}#pfb_float-container'
        + ' button:disabled{background-color:#777}.pfb_f-path-heart-border,.pfb_f-path-padl'
        + 'ock-background{fill:#111}.pfb_f-path-heart-background,.pfb_f-path-padlock-border'
        + '{fill:#fff}.pfb_f-bookmarked .pfb_f-path-heart-background{fill:#ff4060}.pfb_f-sv'
        + 'g-line{stroke:#fff}.pfb_night-theme .pfb_light-panel .pfb_button-container{backg'
        + 'round-color:#333}.pfb_night-theme .pfb_light-panel .pfb_button-container>button{'
        + 'color:#fff}.pfb_night-theme .pfb_light-panel .pfb_button-container:hover{backgro'
        + 'und-color:#555}.pfb_night-theme .pfb_light-panel .pfb_more-button>svg{fill:#fff}'
        + '.pfb_night-theme .pfb_light-panel .pfb_remove-button>svg{stroke:#fff}.pfb_night-'
        + 'theme .pfb_work-tags>div:nth-child(2){border-color:#222;background-color:#555}.p'
        + 'fb_night-theme .pfb_tag-added{background:none;color:#fff}.pfb_night-theme .pfb_p'
        + 'anel{color:#fff;background-color:#333}.pfb_night-theme .pfb_header{background-co'
        + 'lor:#444}.pfb_night-theme .pfb_title{color:#fff}.pfb_night-theme .pfb_action-the'
        + 'me>span{color:#fff}.pfb_night-theme .pfb_action-theme>div{background-color:#333}'
        + '.pfb_night-theme .pfb_comment-section{border-color:#222}.pfb_night-theme .pfb_ac'
        + 'tion-section input{background-color:#444;color:#fff}.pfb_night-theme .pfb_action'
        + '-section input[type="text"]:focus{background-color:#444;color:#fff}.pfb_night-th'
        + 'eme .pfb_action-btns>button{color:#fff;background-color:#333}.pfb_night-theme .p'
        + 'fb_action-btns>button:hover{background-color:#555 !important}';
      document.head.appendChild(style);
    },
    updateHeart(id, type, isNovel) {
      if (pfb.data.isReactApp) {
        const { heartButtonSelector, closestDivContNovelSize } = pfb.selectorsList;
        const a = isNovel ? `a[href*="/novel/show.php?id=${id}"]` : `a[href*="/artworks/${id}"]`;
        const elems = [...document.querySelectorAll(`div[width] ${a}`)];
        elems.forEach((elem) => {
          const div = isNovel
            ? elem.closest('li') || elem.closest(closestDivContNovelSize)
            : elem.closest('div[width]');
          const button = div.querySelector(heartButtonSelector);
          pfb.replaceHeartSVG(button, type);
        });
      } else {
        const dataType = isNovel ? 'novel' : 'illust';
        const elems = [
          ...document.querySelectorAll(
            `div._one-click-bookmark[data-type="${dataType}"][data-id="${id}"]`,
          ),
        ];
        elems.forEach(elem => pfb.replaceHeartImg(elem, type));
      }
    },
    replaceHeartImg(button, type) {
      if (!button) return;
      switch (type) {
        case 0:
          button.classList.remove('private');
          button.classList.add('on');
          break;
        case 1:
          button.classList.add('on', 'private');
          break;
        case 2:
        default:
          button.classList.remove('on', 'private');
          break;
      }
    },
    replaceHeartSVG(parent, heartType) {
      if (!parent) return;
      const child = parent.querySelector('*');
      let heart;
      switch (heartType) {
        case 0:
          heart = this.elementsList.publicHeart();
          break;
        case 1:
          heart = this.elementsList.privateHeart();
          break;
        case 2:
          heart = this.elementsList.emptyHeart();
          break;
        default:
          break;
      }
      if (parent) parent.replaceChild(heart, child);
    },
    changeTheme(e, isNight) {
      const { pfbnightTitle } = pfb.scriptData;
      const { NIGHT_THEME } = this.classList;
      const data = JSON.stringify({ night: isNight });
      localStorage.setItem(pfbnightTitle, data);
      const method = isNight ? 'add' : 'remove';
      document.body.classList[method](NIGHT_THEME);
    },
    async loadLocalStorage(title) {
      const data = await localStorage.getItem(title);
      return data ? JSON.parse(data) : {};
    },
    async loadWorkData(isNovel, illustId) {
      const workType = isNovel ? 'novel' : 'illust';
      const { workData } = this.urlList;
      const url = workData(workType, illustId);
      const { getArgs } = this.fetchData.args;
      const response = await fetch(url, getArgs);
      return response.json();
    },
    async loadBookmarkData(isNovel, illustId) {
      const workType = isNovel ? 'novel' : 'illust';
      const { bookmarkDataUrl } = this.urlList;
      const { getArgs } = this.fetchData.args;
      const url = bookmarkDataUrl(workType, illustId);
      const response = await fetch(url, getArgs);
      return response.json();
    },
    async loadUserTags(userId, isNovel) {
      const { bookmarkTagsUrl } = this.urlList;
      const { getArgs } = this.fetchData.args;
      const worksType = isNovel ? 'novels' : 'illusts';
      const url = bookmarkTagsUrl(worksType, userId);
      const response = await fetch(url, getArgs);
      return response.json();
    },
    async saveBookmark(isNovel, args) {
      const { addIllustBookmarkUrl, addNovelBookmarkUrl } = this.fetchData.urlList;
      const url = isNovel ? addNovelBookmarkUrl : addIllustBookmarkUrl;
      const response = await fetch(url, args);
      return response.json();
    },
    async removeBookmark(isNovel, args) {
      const { removeIllustBookmarkUrl, removeNovelBookmarkUrl } = this.fetchData.urlList;
      const url = isNovel ? removeNovelBookmarkUrl : removeIllustBookmarkUrl;
      const response = await fetch(url, args);
      return response.json();
    },
    runObserver() {
      const { placeIllustrationSelector, placeNovelSelector } = this.selectorsList;
      const { MAIN_ID } = this.classList;
      const { illustPath, novelPath2 } = this.regexList;
      const observer = new MutationObserver(() => {
        const elementIllust = document.querySelector(placeIllustrationSelector);
        const elementNovel = document.querySelector(placeNovelSelector);
        const isIllust = window.location.href.match(illustPath);
        const isNovel = window.location.href.match(novelPath2);
        if (elementIllust && isIllust) {
          const mc = isIllust ? MAIN_ID : 'pfb-nv';
          if (!document.getElementById(mc)) {
            const m1 = window.location.pathname.match(/\/artworks\/\d+/);
            if (m1) {
              const m2 = m1[0].match(/\d+/);
              if (m2) {
                const illustId = +m2[0];
                this.data.illustId = illustId;
                this.pfbElementsInitialize();
              }
            }
          }
        }
      });
      observer.observe(document, {
        childList: true,
        subtree: true,
      });
    },
    initialize() {
      const { pixivWelcomeTitle, pixivErrorTitle } = this.selectorsList;
      const { novelPath } = this.regexList;
      const { pfbnightTitle } = pfb.scriptData;
      const welcomeTitle = document.querySelector(pixivWelcomeTitle);
      const errorTitle = document.querySelector(pixivErrorTitle);
      const isNovel = window.location.pathname.match(novelPath);
      if (welcomeTitle || errorTitle) return;
      this.data.token = token();
      this.data.isReactApp = isReact();
      this.data.userId = getUserId();
      this.data.isNovel = isNovel;
      this.css();
      this.loadLocalStorage(pfbnightTitle).then((localData) => {
        if (localData.night) {
          const { NIGHT_THEME } = this.classList;
          document.body.classList.add(NIGHT_THEME);
        }
      });
      this.miniBookmarkInitialize();
      if (this.data.isReactApp) this.runObserver();
    },
  };
  pfb.initialize();
}());