// ==UserScript== // @name Pixiv Infinite Scroll // @name:ja Pixiv Infinite Scroll // @name:zh-CN Pixiv Infinite Scroll // @name:zh-TW Pixiv Infinite Scroll // @namespace https://github.com/chimaha/Pixiv-Infinite-Scroll // @match https://www.pixiv.net/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @version 1.5 // @author chimaha // @description Add infinite scroll feature to Pixiv. // @description:ja Pixivに無限スクロール機能を追加します。 // @description:zh-CN 为 Pixiv 添加无限滚动功能。 // @description:zh-TW 因為Pixiv有無限移動功能。 // @license MIT license // @compatible firefox // @compatible chrome // @icon https://raw.githubusercontent.com/chimaha/Pixiv-Infinite-Scroll/main/icon.png // @supportURL https://github.com/chimaha/Pixiv-Infinite-Scroll/issues // @downloadURL none // ==/UserScript== /*! Pixiv Infinite Scroll | MIT license | https://github.com/chimaha/Pixiv-Infinite-Scroll/blob/main/LICENSE */ "use strict"; let showDividingLine = GM_getValue("dividingLine", true); // エスケープHTML function escapeText(str) { if (typeof str != "string") { return str; } return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } const fetchResponse = async (url) => { let response; let json; try { response = await fetch(url); if (!response.ok) { throw new Error(`Response Error:${response.status}`); } json = await response.json(); return json; } catch (error) { console.error(error); } } // イラストをマウスオーバーでopacity変更 function mouseover() { for (const element of document.querySelectorAll(`[data-page="${scrollPageCount + 1}"] a.khjDVZ`)) { element.addEventListener("mouseover", () => { element.style.opacity = "0.8"; }); element.addEventListener("mouseleave", () => { element.style.opacity = "1"; }); } } function addStyle() { if (document.getElementById("pis-style")) { return } document.head.insertAdjacentHTML('beforeend', ''); document.head.lastElementChild.textContent = ` .gqlfsh:visited { color: rgb(173, 173, 173); } .jtUPOE { display: flex; padding: 0px; flex-wrap: wrap; margin: -12px; list-style: none; } .hdRpMN { display: grid; grid-template-columns: repeat(6, 184px); gap: 24px; }`; } // フォロー中の無限スクロール----------------------------------------------------------------- function following_process() { // langの値によって言語を変更する const setFollowLanguage = []; const currentLanguage = document.querySelector("html").getAttribute("lang"); switch (currentLanguage) { case "ja": setFollowLanguage.push("フォロー中", "フォローする"); break; case "ko": setFollowLanguage.push("팔로우 중", "팔로우하기"); break; case "zh-CN": setFollowLanguage.push("已关注", "加关注"); break; case "zh-TW": setFollowLanguage.push("關注中", "加關注"); break; default: setFollowLanguage.push("Following", "Follow"); } let borderCount = 0; function createElement(userId, userName, userProfileImage, userComment, userFollowing, illustId, illustTitle, illustUrl, illustBookmarkData, illustAlt, illustR18, illustPageCount) { // ページ区切り線 borderCount++; if (borderCount == 1 && showDividingLine) { const borderElement = `
${borderOffset}
`; document.querySelector(".sc-1y4z60g-4").insertAdjacentHTML("beforeend", borderElement); } // フォロー中・フォローするを切り替え let changeFollowLanguage; let followClass; let followStyle = ""; if (userFollowing) { changeFollowLanguage = setFollowLanguage[0]; followClass = "cnpwVx"; followStyle = 'style="background-color: var(--charcoal-surface3); color: var(--charcoal-text2); font-weight: bold; padding-right: 24px; padding-left: 24px; border-radius: 999999px; height: 40px;"'; } else { changeFollowLanguage = setFollowLanguage[1]; followClass = "fOWAlD"; } // ブックマークを切り替え let bookmarkClass = []; let bookmarkStyle = []; for (const checkBookmark of illustBookmarkData) { if (checkBookmark) { bookmarkClass.push("bXjFLc"); bookmarkStyle.push('style="color: rgb(255, 64, 96); fill: currentcolor;"'); } else { bookmarkClass.push("dxYRhf"); } } // R18マーク let r18Element = []; for (const checkR18 of illustR18) { if (checkR18 == "R-18") { r18Element.push('
R-18
'); } else { r18Element.push(""); } } // うごくイラスト再生マーク。イラスト枚数表示 let ugoiraElement = []; let pageCountElement = []; illustPageCount.forEach((pageCount, i) => { if (illustAlt[i].slice(-4) == "うごイラ") { ugoiraElement.push(''); pageCountElement.push(""); } else { ugoiraElement.push(""); if (pageCount >= 2) { pageCountElement.push(`
${pageCount}
`); } else { pageCountElement.push(""); } } }); // イラストがない場合は表示しないようにするため、分けて作成する let illustGroup = ""; for (let i = 0; i < illustId.length; i++) { illustGroup += `
`; } let illustContainer = ""; if (illustId[0]) { illustContainer = `
    ${illustGroup}
`; } // "appendElements+="で一括追加にすると、なぜかundefinedが追加され続けるので一つずつ追加 const appendElements = `
${escapeText(userComment)}
${illustContainer}
`; document.querySelector(".sc-1y4z60g-4").insertAdjacentHTML("beforeend", appendElements); } // https://www.pixiv.net/ajax/user/*/following?offset=24&limit=24&rest=show // https://www.pixiv.net/users/*/following?p=2 const illustItems = document.querySelectorAll(".sc-1y4z60g-5"); if (illustItems.length < 24 && scrollPageCount == 1) { return; } // URL作成 const matches = location.href.match(followingRegex); let offset; let borderOffset; if ((matches[3] || matches[5]) && isValid) { if (matches[3] && scrollPageCount == 0) { saveScrollPageCount = Number(matches[3]); } else if (scrollPageCount == 0) { saveScrollPageCount = Number(matches[5]); } offset = (saveScrollPageCount * 24) + (scrollPageCount * 24); borderOffset = scrollPageCount + saveScrollPageCount + 1; } else { scrollPageCount == 0 ? saveScrollPageCount = 0 : ""; isValid = false; offset = 24 + (scrollPageCount * 24); borderOffset = scrollPageCount + 2; } scrollPageCount++; const folderTag = matches[2] ? `&tag=${matches[2]}` : ""; const showHide = matches[4] ? "hide" : "show"; if (scrollPageCount == 1) { revertURL(illustItems, 23, 30); } const url = `https://www.pixiv.net/ajax/user/${matches[1]}/following?offset=${offset}&limit=24&rest=${showHide}${folderTag}`; const fetchData = async () => { const json = await fetchResponse(url); for (let i = 0; i < Object.keys(json.body.users).length; i++) { const users = json.body.users[i]; const userId = users.userId; const userName = users.userName; const userProfileImage = users.profileImageUrl; const userComment = users.userComment.slice(0, 98); const userFollowing = users.following; const illustId = []; const illustTitle = []; const illustUrl = []; const illustBookmarkData = []; const illustAlt = []; const illustR18 = []; const illustPageCount = []; for (let j = 0; j < Object.keys(json.body.users[i].illusts).length; j++) { const illusts = json.body.users[i].illusts[j]; illustId.push(illusts.id); illustTitle.push(illusts.title); illustUrl.push(illusts.url); illustBookmarkData.push(illusts.bookmarkData); illustAlt.push(illusts.alt); illustR18.push(illusts.tags[0]); illustPageCount.push(illusts.pageCount); } createElement(userId, userName, userProfileImage, userComment, userFollowing, illustId, illustTitle, illustUrl, illustBookmarkData, illustAlt, illustR18, illustPageCount); } mouseover(); }; (async () => { await fetchData(); bookmarkAddDelete(); followAndUnfollow(setFollowLanguage); changeURL(matches[3], matches[5], 23, 30); })(); } // ----------------------------------------------------------------------------------------- // ブックマーク・フォローユーザーの作品・タグ検索・プロフィールページの無限スクロール-------------- function bookmarkAndTag_process(checkType, matches) { function createElement(illustId, illustTitle, illustUrl, userId, userName, illustPageCount, illustBookmarkData, illustAlt, userProfileImage, typeElement, typeClass, illustR18, illustMaskReason) { // langの値によって言語を変更する const setDeletedLanguage = []; const currentLanguage = document.querySelector("html").getAttribute("lang"); switch (currentLanguage) { case "ja": setDeletedLanguage.push("R18 / R18G", "作品", "閲覧制限中", "削除済み", "もしくは非公開"); break; case "ko": setDeletedLanguage.push("R-18 / R-18G", "작품", "열람 제한 중", "삭제됨", "혹은 비공개"); break; case "zh-CN": setDeletedLanguage.push("R-18 / R-18G", "作品", "浏览受限(含成人内容)", "已删除", "或不公开"); break; case "zh-TW": setDeletedLanguage.push("R-18 / R-18G", "作品", "瀏覽受限(含成人內容)", "已刪除", "或非公開"); break; default: setDeletedLanguage.push("R-18/R-18G", "works", "Restricted (Adult Content)", "Deleted", "or private"); } // ブックマークを切り替え let bookmarkClass = ""; let bookmarkStyle = ""; if (illustBookmarkData) { bookmarkClass = "bXjFLc"; bookmarkStyle = 'style="color: rgb(255, 64, 96); fill: currentcolor;"'; } else { bookmarkClass = "dxYRhf"; } // R18マーク let r18Element = ""; if (illustR18 == "R-18") { r18Element = `
R-18
`; } // うごくイラスト再生マーク・イラスト数表示 let ugoiraElement = ""; let pageCountElement = ""; if (illustAlt.slice(-4) == "うごイラ") { ugoiraElement = '
${illustPageCount}
`; } } let illustContainer = ""; let userNameContainer = ""; let addBookmarkClass = ""; let illustTitleElement; if (illustTitle == "-----") { if (illustMaskReason == "r18" || illustMaskReason == "r18g") { // R18・R18G illustContainer = `
${setDeletedLanguage[0]}
${setDeletedLanguage[1]}
`; illustTitleElement = `${setDeletedLanguage[2]}` } else { // 削除・非公開 illustContainer = `
${setDeletedLanguage[3]}
${setDeletedLanguage[4]}
`; illustTitleElement = `${escapeText(illustTitle)}` } } else { // ノーマル illustContainer = `
${escapeText(illustAlt)} ${ugoiraElement}
${r18Element}
${pageCountElement}
`; userNameContainer = ` `; illustTitleElement = `${escapeText(illustTitle)}` addBookmarkClass = " addBookmark" } if (checkType == "artwork") { appendElements += ` ${typeElement}
${illustContainer}
${illustTitleElement}
`; } else { appendElements += ` ${typeElement}
${illustContainer}
${illustTitleElement}
${userNameContainer}
`; } } function getIllustData(type, jsonBody, typeElement, typeClass, target, borderOffset, tag) { for (let i = 0; i < Object.keys(jsonBody).length; i++) { // タグ検索には1つだけ何も入っていないjsonBodyがあるので、そこだけ除外 if (!jsonBody[i].id) { continue; } const illust = jsonBody[i]; const illustId = illust.id; const illustTitle = illust.title; const illustUrl = illust.url; const userId = illust.userId; const userName = illust.userName; const illustPageCount = illust.pageCount; const illustBookmarkData = illust.bookmarkData; const illustAlt = illust.alt; const userProfileImage = illust.profileImageUrl; const illustR18 = illust.tags[0]; const illustMaskReason = illust.maskReason; createElement(illustId, illustTitle, illustUrl, userId, userName, illustPageCount, illustBookmarkData, illustAlt, userProfileImage, typeElement, typeClass, illustR18, illustMaskReason); } if (appendElements) { let ulClass; switch (type) { case "tag": ulClass = "sc-l7cibp-1 hdRpMN"; break; default: ulClass = "sc-9y4be5-1 jtUPOE"; } if (showDividingLine) { appendElements = `
${borderOffset}
` document.querySelector(target).insertAdjacentHTML("beforeend", appendElements); } else if (tag) { // タグ検索の場合詰めて表示する document.querySelector(".sc-l7cibp-1").insertAdjacentHTML("beforeend", appendElements); } else { appendElements = `` document.querySelector(target).insertAdjacentHTML("beforeend", appendElements); } } } let appendElements = ""; if (checkType == "bookmark") { // ブックマーク // https://www.pixiv.net/ajax/user/*/illusts/bookmarks?tag=&offset=0&limit=48&rest=show // https://www.pixiv.net/users/*/bookmarks/artworks?p=2 const illustItems = document.querySelectorAll(".sc-9y4be5-2"); if (illustItems.length < 48 && scrollPageCount == 1) { return; } // URL作成 let offset; let borderOffset; if (matches[3] && isValid) { scrollPageCount == 0 ? saveScrollPageCount = Number(matches[3]) : ""; offset = (saveScrollPageCount * 48) + (scrollPageCount * 48); borderOffset = scrollPageCount + saveScrollPageCount + 1; } else { scrollPageCount == 0 ? saveScrollPageCount = 0 : ""; isValid = false; offset = 48 + (scrollPageCount * 48); borderOffset = scrollPageCount + 2; } const tag = matches[2] ? matches[2] : ""; scrollPageCount++; if (scrollPageCount == 1) { revertURL(illustItems, 47, 50); } const url = `https://www.pixiv.net/ajax/user/${matches[1]}/illusts/bookmarks?tag=${tag}&offset=${offset}&limit=48&rest=show`; const fetchData = async () => { const json = await fetchResponse(url); const typeElement = `
  • `; const target = ".sc-9y4be5-0"; getIllustData("bookmark", json.body.works, typeElement, "", target, borderOffset, false); mouseover(); }; (async () => { await fetchData(); bookmarkAddDelete(); changeURL(matches[3], false, 47, 50); })(); } else if (checkType == "follow") { // フォローユーザーの作品 // https://www.pixiv.net/ajax/follow_latest/illust?p=2&mode=all // https://www.pixiv.net/bookmark_new_illust.php?p=2 const illustItems = document.querySelectorAll(".sc-9y4be5-2"); if (illustItems.length < 60 && scrollPageCount == 1) { return; } // URL作成 let offset; let borderOffset; scrollPageCount++; if ((matches[2] || matches[4]) && isValid) { if (matches[2] && scrollPageCount == 1) { saveScrollPageCount = Number(matches[2]); } else if (scrollPageCount == 1) { saveScrollPageCount = Number(matches[4]); } offset = saveScrollPageCount + scrollPageCount; borderOffset = scrollPageCount + saveScrollPageCount; } else { scrollPageCount == 1 ? saveScrollPageCount = 0 : ""; isValid = false; offset = 1 + scrollPageCount; borderOffset = scrollPageCount + 1; } const setMode = matches[1] ? "r18" : "all"; if (scrollPageCount == 1) { revertURL(illustItems, 59, 70); } const folderTag = matches[3] ? `&${matches[3]}` : ""; const url = `https://www.pixiv.net/ajax/follow_latest/illust?p=${offset}&mode=${setMode}${folderTag}`; const fetchData = async () => { const json = await fetchResponse(url); const typeElement = `
  • `; const typeClass = "gtm-followlatestpage-thumbnail-link"; const target = ".sc-9y4be5-0"; getIllustData("follow", json.body.thumbnails.illust, typeElement, typeClass, target, borderOffset, false); mouseover(); }; (async () => { await fetchData(); bookmarkAddDelete(); changeURL(matches[2], matches[4], 59, 70); })(); } else if (checkType == "tag") { // タグ検索 // https://www.pixiv.net/ajax/search/artworks/*?word=*&order=date_d&mode=all&p=1&s_mode=s_tag_full&type=all // https://www.pixiv.net/tags/*/artworks?p=2 const illustItems = document.querySelectorAll(".sc-l7cibp-2"); if (illustItems.length < 60 && scrollPageCount == 1) { return; } // URL作成 let offset; scrollPageCount++; let borderOffset; if ((matches[7] || matches[11]) && isValid) { if (matches[7] && scrollPageCount == 1) { saveScrollPageCount = Number(matches[7]); } else if (matches[11] && scrollPageCount == 1) { saveScrollPageCount = Number(matches[11]); } offset = saveScrollPageCount + scrollPageCount; borderOffset = scrollPageCount + saveScrollPageCount; } else { scrollPageCount == 1 ? saveScrollPageCount = 0 : ""; isValid = false; offset = 1 + scrollPageCount; borderOffset = scrollPageCount + 1; } if (scrollPageCount == 1) { revertURL(illustItems, 5, 0); } let setIllustType = ""; let insertIllustType; if (matches[2] == "manga") { setIllustType = "type=manga"; insertIllustType = "manga"; } else if (matches[2] == "artworks") { setIllustType = "type=all"; insertIllustType = "illustManga"; } else if (matches[9] == "illust") { setIllustType = "type=illust"; insertIllustType = "illust"; } else if (matches[9] == "ugoira") { setIllustType = "type=ugoira"; insertIllustType = "illust"; } else if (matches[2] == "illustrations") { setIllustType = "type=illust_and_ugoira"; insertIllustType = "illust"; } let tagMatchMode = ""; if (matches[8] == "s_mode=s_tag" || matches[8] == "s_mode=s_tc") { tagMatchMode = matches[8]; } else { tagMatchMode = "s_mode=s_tag_full"; } let setMode = ""; if (matches[4] == "mode=safe" || matches[4] == "mode=r18") { setMode = matches[4]; } else { setMode = "mode=all"; } const sinceDate = matches[5] ? `&${matches[5]}` : ""; const untilDate = matches[6] ? `&${matches[6]}` : ""; const otherTag = matches[10] ? `&${matches[10]}` : ""; const orderDate = matches[3] ? `order=${matches[3]}` : "order=date_d"; const url = `https://www.pixiv.net/ajax/search/${matches[2]}/${matches[1]}?word=${matches[1]}&${orderDate}&${setMode}&p=${offset}&${tagMatchMode}&${setIllustType}${sinceDate}${untilDate}${otherTag}`; const fetchData = async () => { const json = await fetchResponse(url); const typeElement = `
  • `; const target = ".sc-l7cibp-0 > .sc-1nr368f-4:first-child"; getIllustData("tag", json.body[insertIllustType].data, typeElement, "", target, borderOffset, true); mouseover(); }; (async () => { await fetchData(); bookmarkAddDelete(); changeURL(matches[7], matches[11], 5, 0); })(); } else if (checkType == "artwork") { // ユーザープロフィールのイラスト // https://www.pixiv.net/ajax/user/*/illusts/tag?tag=*&offset=*&limit=48 // https://www.pixiv.net/ajax/user/*/profile/all // https://www.pixiv.net/ajax/user/*/profile/illusts?ids[]=*&ids[]=* const illustItems = document.querySelectorAll(".sc-9y4be5-2"); if (illustItems.length < 48 && scrollPageCount == 1) { return; } // https://www.pixiv.net/users/*/illustrations/風景 // 上のURLのようにタグをつけた場合 if (matches[3]) { // URL作成 let offset; let borderOffset; if (matches[4] && isValid) { scrollPageCount == 0 ? saveScrollPageCount = Number(matches[4]) : ""; offset = (Number(matches[4]) * 48) + (scrollPageCount * 48); borderOffset = scrollPageCount + saveScrollPageCount + 1; } else { scrollPageCount == 0 ? saveScrollPageCount = 0 : ""; isValid = false; offset = 48 + (scrollPageCount * 48); borderOffset = scrollPageCount + 2; } scrollPageCount++; if (scrollPageCount == 1) { revertURL(illustItems, 47, 50); } let tag = matches[3]; let insertIllustType; if (matches[2] == "illustrations") { insertIllustType = "illusts"; } else if (matches[2] == "manga") { insertIllustType = "manga"; } else if (matches[2] == "artworks") { insertIllustType = "illustmanga"; } const url = `https://www.pixiv.net/ajax/user/${matches[1]}/${insertIllustType}/tag?tag=${tag}&offset=${offset}&limit=48`; const fetchData = async () => { const json = await fetchResponse(url); const typeElement = `
  • `; const target = ".sc-9y4be5-0"; getIllustData("artwork", json.body.works, typeElement, "", target, borderOffset, false); mouseover(); }; (async () => { await fetchData(); bookmarkAddDelete(); changeURL(matches[4], false, 47, 50); })(); } else { // タグがない場合 const url = `https://www.pixiv.net/ajax/user/${matches[1]}/profile/all`; const fetchData = async () => { // 初回のみillustIDをすべて取得 if (!artworkIllustId) { const json = await fetchResponse(url); const objectIllustId = json.body.illusts; const arrayIllustId = Object.keys(objectIllustId); const objectMangaId = json.body.manga; const arrayMangaId = Object.keys(objectMangaId); // イラストとマンガの配列を組み合わせ、番号順に並べる const arrayArtworks = arrayIllustId.concat(arrayMangaId); arrayArtworks.sort((a, b) => a - b); // 開いているページまでのillustIDを削除 function deleteIllustId(artworkIllustId) { matches[4] ? artworkIllustId.splice(-96) : artworkIllustId.splice(-48); } if (matches[2] == "illustrations") { artworkIllustId = arrayIllustId.map(item => "ids[]=" + item); deleteIllustId(artworkIllustId); } else if (matches[2] == "manga") { artworkIllustId = arrayMangaId.map(item => "ids[]=" + item); deleteIllustId(artworkIllustId); } else { artworkIllustId = arrayArtworks.map(item => "ids[]=" + item); deleteIllustId(artworkIllustId); } } scrollPageCount++; let borderOffset; if (matches[4] && isValid) { scrollPageCount == 1 ? saveScrollPageCount = Number(matches[4]) : ""; borderOffset = scrollPageCount + saveScrollPageCount; } else { borderOffset = scrollPageCount + 1; if (scrollPageCount == 1) { saveScrollPageCount = 0; isValid = false; } } if (scrollPageCount == 1) { revertURL(illustItems, 47, 50); } // 配列の後ろから48個取得して削除 const sliceIllustId = artworkIllustId.splice(-48); const url2 = `https://www.pixiv.net/ajax/user/${matches[1]}/profile/illusts?${sliceIllustId.join("&")}&work_category=illust&is_first_page=0` const json2 = await fetchResponse(url2); // jsonファイルは先頭が古いイラストで、後ろが新しいイラスト // 後ろの新しいイラストからfor文を回して要素を追加していく必要があるので、reverseメソッドを使用 const keys = Object.keys(json2.body.works).reverse(); for (const key of keys) { const illust = json2.body.works[key]; const illustId = illust.id; const illustTitle = illust.title; const illustUrl = illust.url; const userId = illust.userId; const userName = ""; const illustPageCount = illust.pageCount; const illustBookmarkData = illust.bookmarkData; const illustAlt = illust.alt; const userProfileImage = ""; const typeElement = `
  • `; const typeClass = ""; const illustR18 = illust.tags[0]; const illustMaskReason = ""; createElement(illustId, illustTitle, illustUrl, userId, userName, illustPageCount, illustBookmarkData, illustAlt, userProfileImage, typeElement, typeClass, illustR18, illustMaskReason); } if (appendElements) { if (showDividingLine) { appendElements = `
    ${borderOffset}
      ${appendElements}
    ` } else { appendElements = `
      ${appendElements}
    ` } document.querySelector(".sc-9y4be5-0").insertAdjacentHTML("beforeend", appendElements); } mouseover(); }; (async () => { await fetchData(); bookmarkAddDelete(); changeURL(matches[4], false, 47, 50); })(); } } } // ----------------------------------------------------------------------------------------- // ページ数をURLに表示する-------------------------------------------------------------------- // 元からあるイラスト function revertURL(illustItems, n, k) { // p=がついている場合はそのまま出力 const pageAdded = location.href.includes("p=") ? location.href : false; for (let i = 0; i < illustItems.length; i++) { // n個ごとにIntersectionObserverを設置 if ((i % n) == 0 && i != k) { illustItems[i].classList.add("pageCount"); const options = { rootMargin: "-45% 0%" }; const pageCountObserver = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { let newUrl; if (pageAdded) { newUrl = pageAdded; } else { const url = location.href; if (url.includes("?p=") && url.includes("&")) { newUrl = url.replace(/p=\d+&/, ""); } else if (url.includes("?p=") && !url.includes("&")) { newUrl = url.replace(/\?p=\d+/, ""); } else if (url.includes("&p=")) { newUrl = url.replace(/&p=\d+/, ""); } else { return url; } } history.replaceState(null, null, newUrl); } }, options); pageCountObserver.observe(illustItems[i]); } } } // 追加されたイラスト function changeURL(matches, matches2, n, k) { const pageCountElements = document.querySelectorAll(".addElement"); for (let i = 0; i < pageCountElements.length; i++) { pageCountElements[i].classList.remove("addElement"); // n個ごとにIntersectionObserverを設置 if ((i % n) == 0 && i != k) { pageCountElements[i].classList.add("pageCount"); const options = { rootMargin: "-45% 0%" }; const pageCountObserver = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { const pageCountNumber = pageCountElements[i].dataset.page; let newUrl; // 最初の実行がelse(p=がない)の場合は、isValidをfalseにしてelseのみ実行されるように if ((matches || matches2) && isValid) { const url = Number(saveScrollPageCount) + Number(pageCountNumber) - 1; newUrl = currentUrl.replace(/(p=)[^&]+/, "$1" + url); } else { if (currentUrl.includes("p=")) { const url = Number(saveScrollPageCount) + Number(pageCountNumber); newUrl = currentUrl.replace(/(p=)[^&]+/, "$1" + url); } else { const separate = currentUrl.includes("?") ? "&" : "?"; newUrl = currentUrl + separate + "p=" + Number(pageCountNumber); } } history.replaceState(null, null, newUrl); } }, options); pageCountObserver.observe(pageCountElements[i]); } } } // ----------------------------------------------------------------------------------------- // 新たに追加した要素でのブックマーク・フォロー機能--------------------------------------------- // x-csrf-tokenを取得 const getCsrfToken = async () => { const response = await fetch(location.origin); const data = await response.text(); const matchToken = data.match(/"token":"([a-z0-9]+)"/); return matchToken[1]; }; // ブックマーク追加・削除 function bookmarkAddDelete() { const buttonGrandElements = document.querySelectorAll(".addBookmark"); for (let i = 0; i < buttonGrandElements.length; i++) { const buttonElement = buttonGrandElements[i].querySelector("button.sc-kgq5hw-0"); const svgElement = buttonGrandElements[i].querySelector("svg.sc-j89e3c-1"); const getIdElement = buttonGrandElements[i].querySelector("a.sc-d98f2c-0[data-gtm-user-id]"); const userId = getIdElement.getAttribute("data-gtm-user-id"); const illustId = getIdElement.getAttribute("data-gtm-value"); buttonGrandElements[i].classList.remove("addBookmark"); buttonElement.addEventListener("click", () => { if (svgElement.classList.contains("dxYRhf")) { // ブックマーク追加 (async () => { try { buttonElement.setAttribute("disabled", true); const addBookmarkBody = { illust_id: illustId, restrict: 0, comment: "", tags: [] }; const url = "https://www.pixiv.net/ajax/illusts/bookmarks/add"; const setCsrfToken = await getCsrfToken(); const response = await fetch(url, { method: "post", headers: { "Accept": "application/json", "Content-Type": "application/json; charset=utf-8", "x-csrf-token": setCsrfToken, }, body: JSON.stringify(addBookmarkBody), credentials: "same-origin" }); const json = await response.json(); // ストレージにBookmarkIDを保存 sessionStorage.illustId = json.body.last_bookmark_id; if (!response.ok) { throw new Error(); } buttonElement.removeAttribute("disabled"); svgElement.style.color = "rgb(255, 64, 96)"; svgElement.style.fill = "currentcolor"; svgElement.classList.remove("dxYRhf"); svgElement.classList.add("bXjFLc"); } catch (error) { console.error(error); buttonElement.removeAttribute("disabled"); } })(); } else if (svgElement.classList.contains("bXjFLc")) { // ブックマーク削除 (async () => { try { buttonElement.setAttribute("disabled", true); // BookmarkIDを取得する const getStorageItem = sessionStorage.getItem(illustId); sessionStorage.removeItem(illustId); let bookmarkId; if (getStorageItem) { bookmarkId = getStorageItem; } else { // https://www.pixiv.net/ajax/user/*/illusts?ids[]=* const illustInfoUrl = `https://www.pixiv.net/ajax/user/${userId}/illusts?ids[]=${illustId}`; const illustInfoResponse = await fetch(illustInfoUrl); const illustInfoJson = await illustInfoResponse.json(); bookmarkId = illustInfoJson.body[illustId].bookmarkData.id; } const deleteBookmarkBody = new URLSearchParams({ bookmark_id: bookmarkId }); const url = "https://www.pixiv.net/ajax/illusts/bookmarks/delete" const setCsrfToken = await getCsrfToken(); const response = await fetch(url, { method: "post", headers: { "Accept": "application/json", "x-csrf-token": setCsrfToken, }, body: deleteBookmarkBody, credentials: "same-origin" }); if (!response.ok) { throw new Error(); } buttonElement.removeAttribute("disabled"); svgElement.removeAttribute("style"); svgElement.classList.remove("bXjFLc"); svgElement.classList.add("dxYRhf"); } catch (error) { console.error(error); buttonElement.removeAttribute("disabled"); } })(); } }) } } // フォロー・フォロー解除 function followAndUnfollow(setFollowLanguage) { const buttonElements = document.querySelectorAll(".follow"); for (let i = 0; i < buttonElements.length; i++) { const buttonElement = buttonElements[i]; const userId = buttonElement.getAttribute("data-gtm-user-id"); buttonElement.classList.remove("follow"); buttonElement.addEventListener("click", () => { if (buttonElement.classList.contains("fOWAlD")) { // フォローする (async () => { try { buttonElement.setAttribute("disabled", true); const followBody = new URLSearchParams({ mode: "add", type: "user", user_id: userId, tag: "", restrict: 0, format: "json" }); const url = "https://www.pixiv.net/bookmark_add.php"; const setCsrfToken = await getCsrfToken(); const response = await fetch(url, { method: "post", headers: { "Accept": "application/json", "x-csrf-token": setCsrfToken, }, body: followBody, credentials: "same-origin" }); if (!response.ok) { throw new Error(); } buttonElement.removeAttribute("disabled"); buttonElement.classList.remove("fOWAlD"); buttonElement.classList.add("cnpwVx"); buttonElement.style.backgroundColor = "var(--charcoal-surface3)"; buttonElement.style.color = "var(--charcoal-text2)"; buttonElement.style.fontWeight = "bold"; buttonElement.style.padding = "0 24px"; buttonElement.style.borderRadius = "999999px"; buttonElement.style.height = "40px"; buttonElement.textContent = setFollowLanguage[0]; } catch (error) { console.error(error); buttonElement.removeAttribute("disabled"); } })(); } else if (buttonElement.classList.contains("cnpwVx")) { // フォロー解除 (async () => { try { buttonElement.setAttribute("disabled", true); const unfollowBody = new URLSearchParams({ mode: "del", type: "bookuser", id: userId }); const url = "https://www.pixiv.net/rpc_group_setting.php" const setCsrfToken = await getCsrfToken(); const response = await fetch(url, { method: "post", headers: { "Accept": "application/json", "x-csrf-token": setCsrfToken, }, body: unfollowBody, credentials: "same-origin" }); if (!response.ok) { throw new Error(); } buttonElement.removeAttribute("disabled"); buttonElement.classList.remove("cnpwVx"); buttonElement.classList.add("fOWAlD"); buttonElement.removeAttribute("style"); buttonElement.textContent = setFollowLanguage[1]; } catch (error) { console.error(error); buttonElement.removeAttribute("disabled"); } })(); } }) } } // ----------------------------------------------------------------------------------------- let isProcessed = false; let saveScrollPageCount; let isValid = true; let currentUrl; let saveUrl; let scrollPageCount = 0; let observerCount = 0; let artworkIllustId = ""; const followingRegex = /https:\/\/www\.pixiv\.net(?:\/en)?\/users\/(\d+)\/following(?:\/([^?]+))?(?:\?p=(\d+))?(?:(?:&|\?)(rest=hide))?(?:\&p=(\d+))?/; const bookmarkRegex = /https:\/\/www\.pixiv\.net(?:\/en)?\/users\/(\d+)\/bookmarks\/artworks(?:\/([^?]+))?(?:\?p=(\d+))?/; const followUserWorkRegex = /https:\/\/www\.pixiv\.net\/bookmark_new_illust(_r18)?\.php(?:\?p=(\d+))?(?:(?:&|\?)(tag=[^&]*))?(?:\&p=(\d+))?/; const tagRegex = /https:\/\/www\.pixiv\.net(?:\/en)?\/tags\/(.+)\/(artworks|illustrations|manga)(?:\?order=([^&]+))?(?:(?:&|\?)(mode=(?:r18|safe)))?(?:(?:&|\?)(scd=\d{4}\-\d{2}-\d{2}))?(?:(?:&|\?)(ecd=\d{4}\-\d{2}-\d{2}))?(?:(?:&|\?)p=(\d+))?(?:(?:&|\?)(s_mode=(?:s_tag|s_tc)))?(?:(?:&|\?)type=([^&]+))?(?:(?:&|\?)([^p]+))?(?:(?:&|\?)p=(\d+))?/; const artworkRegex = /https:\/\/www\.pixiv\.net(?:\/en)?\/users\/(\d+)\/(illustrations|manga|artworks)(?:\/(.[^?p=]+))?(?:\?p=(\d+))?/; const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (currentUrl && saveUrl) { // ページ数が含まれていないURLを作成 // そのまま取得すると、?p=2の数字が変わった時に実行されてしまう function replaceURL(url) { if (url.includes("?p=") && url.includes("&")) { return url.replace(/p=\d+&/, ""); } else if (url.includes("?p=") && !url.includes("&")) { return url.replace(/\?p=\d+/, ""); } else if (url.includes("&p=")) { return url.replace(/&p=\d+/, ""); } else { return url } } // 現在のURLと初めに保存したURLが一致しない場合実行 if (replaceURL(saveUrl) != replaceURL(location.href)) { isProcessed = false; scrollPageCount = 0; observerCount = 0; artworkIllustId = ""; saveUrl = ""; isValid = true; // タグページ・プロフィールページで条件を変更した際に、追加した要素を削除する if (tagRegex.test(location.href) || artworkRegex.test(location.href) || followUserWorkRegex.test(location.href)) { const removeElements = document.querySelectorAll(".addElement-parents"); for (const removeElement of removeElements) { removeElement.remove(); } } } } if (followingRegex.test(location.href)) { addStyle(); // フォロー currentUrl = location.href; // 初回のみURLを保存 observerCount++; observerCount == 1 ? saveUrl = location.href : ""; const intersectionTarget = document.querySelector(".sc-1y4z60g-4"); if (intersectionTarget && !isProcessed) { isProcessed = true; const scrollObserver = new IntersectionObserver(entries => { entries.forEach((entry) => { if (entry.isIntersecting) { following_process(); isProcessed = false; scrollObserver.unobserve(entry.target); } }) }); const observerElement = document.querySelector(".sc-1y4z60g-5:last-child").previousElementSibling; if (observerElement) { if (scrollPageCount == 0 || scrollPageCount == 1) { setTimeout(() => { scrollObserver.observe(observerElement); }, 400); } else { scrollObserver.observe(observerElement); } } } } else if (bookmarkRegex.test(location.href) || followUserWorkRegex.test(location.href) || tagRegex.test(location.href) || artworkRegex.test(location.href)) { addStyle(); // ブックマーク・フォローユーザーの作品・タグ検索・プロフィールページ currentUrl = location.href; // 初回のみURLを保存 observerCount++; observerCount == 1 ? saveUrl = location.href : ""; let checkType; let matches; if (bookmarkRegex.test(currentUrl)) { checkType = "bookmark"; matches = currentUrl.match(bookmarkRegex); } else if (followUserWorkRegex.test(currentUrl)) { checkType = "follow"; matches = currentUrl.match(followUserWorkRegex); } else if (tagRegex.test(currentUrl)) { checkType = "tag"; matches = currentUrl.match(tagRegex); } else if (artworkRegex.test(currentUrl)) { checkType = "artwork"; matches = currentUrl.match(artworkRegex); } let intersectionTarget; if (bookmarkRegex.test(currentUrl) || followUserWorkRegex.test(currentUrl) || artworkRegex.test(currentUrl)) { intersectionTarget = document.querySelector(".sc-9y4be5-1"); } else { intersectionTarget = document.querySelector(".sc-l7cibp-1 img"); } if (intersectionTarget && !isProcessed) { isProcessed = true; const options = { rootMargin: "0px 0px 300px 0px" }; const scrollObserver = new IntersectionObserver(entries => { entries.forEach((entry) => { if (entry.isIntersecting) { bookmarkAndTag_process(checkType, matches); isProcessed = false; scrollObserver.unobserve(entry.target); } }) }, options); if (bookmarkRegex.test(currentUrl) || followUserWorkRegex.test(currentUrl) || artworkRegex.test(currentUrl)) { // ブックマーク・フォローユーザーの作品・プロフィールページ // ページ読み込み後すぐスクロールするとページが飛ばされることがあるので、遅延して読み込む if (scrollPageCount == 0 || scrollPageCount == 1) { setTimeout(() => { scrollObserver.observe(document.querySelector(".sc-9y4be5-1:last-child > .sc-9y4be5-2:last-child")); }, 400); } else { scrollObserver.observe(document.querySelector(".sc-9y4be5-1:last-child > .sc-9y4be5-2:last-child")); } } else { // タグ検索 // 2ページ目と3ページ目が同時に読み込まれてしまうので、2ページ目もsetTimeoutを使用 if (scrollPageCount == 0 || scrollPageCount == 1) { setTimeout(() => { scrollObserver.observe(document.querySelector(".sc-l7cibp-1:last-child > .sc-l7cibp-2:last-child")); }, 400); } else { scrollObserver.observe(document.querySelector(".sc-l7cibp-1:last-child > .sc-l7cibp-2:last-child")); } } } } }) }); const config = { childList: true, subtree: true }; observer.observe(document.querySelector("body"), config); // 区切り線の表示設定 const browserLang = navigator.language; let menuLang; switch (browserLang) { case "ja": menuLang = "区切り線を表示:"; break; default: menuLang = "Show dividing lines: "; } let menuText = showDividingLine ? `${menuLang}ON✔️` : `${menuLang}OFF❌`; GM_registerMenuCommand(menuText, () => { if (showDividingLine) { GM_setValue("dividingLine", false); } else { GM_setValue("dividingLine", true); } });