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