// ==UserScript== // @name Pixiv tag bookmark // @namespace http://tampermonkey.net/ // @version 0.2 // @description Pixivの作品ページにタグありのブックマーク機能を追加します // @author y_kahou // @match https://www.pixiv.net/* // @grant GM_setValue // @grant GM_getValue // @noframes // @require https://greasyfork.org/scripts/419955-y-method/code/y_method.js?version=890440 // @downloadURL none // ==/UserScript== const __CSS__ = ` :root { --bookmarked-color: rgb(255, 64, 96); --checked-color: #5596e6; } .selectable:hover { background-color: rgb(50, 73, 90); color: white; cursor: pointer; } .selectable.selected { background-color: rgb(85, 150, 230); color: white; } /* 自分用タグ */ #mytag ul { list-style: none; padding: 0; margin: 0 0 5px 0; max-width: 600px; overflow: hidden; transition: 300ms ease-in-out; } #mytag li { display: inline-block; margin-right: 5px; line-height: 1.5; } #mytag span { padding: 2px; user-select: none; } .o0, .o10 { color: rgb(103, 164, 209); } .o30, .o50, .o100 { color: rgb(132, 162, 183); font-weight: bold; } .o30 { font-size: 16px; } .o50 { font-size: 18px; } .o100 { font-size: 22px; } /* ブクマボタン */ #tb-submit { display: block; background: none; outline: none; border: solid 1.5px var(--checked-color); border-radius: 4px; cursor: pointer; transition: 500ms; } #tb-submit::before { content: '選択したタグでお気に入り'; display: inline-block; position: relative; transform: translateY(40%); color: var(--checked-color); } #tb-submit.already { border-color: var(--bookmarked-color); } #tb-submit.already::before { content: 'お気に入りタグを編集'; color: var(--bookmarked-color); } #tb-submit.already path { fill: #dfdfdf; } #tb-submit.already path { fill: var(--bookmarked-color); } /* タグテキスト */ #tb-text { width: 92%; height: 1.5em; text-indent: 0.5em; margin-bottom: 5px; background-color: white; border: solid 1px gray; } #tb-cnt { margin-left: 5px; } #tb-cnt:after { content: '/10'; } `; (function() { 'use strict'; addStyle('tagbookmark', __CSS__); let canonical = document.querySelector('link[rel="canonical"]') if (canonical) { // ページロード時に作品ページだった場合 // link[rel="canonical"]は削除されず使い回されるので // 上記を対象に監視する new MutationObserver(addMytag) .observe(canonical, { attributes: true }) } else { // 作品ページ以外から始まった場合 // link[rel="canonical"]は毎回削除追加されるので // headを監視しlink[rel="canonical"]の追加を検知する new MutationObserver(async (records) => { for (let record of records) { for (let node of record.addedNodes) { if (node.tagName == 'LINK' && node.getAttribute('rel') == 'canonical') { addMytag() return } } } }) .observe(document.head, { childList: true }) } })(); async function addMytag() { // イラスト 作品ページ以外は終了 if (!location.pathname.match(/artworks\/\d+$/)) { // 小説 && !location.pathname.match(/\/novel\/show\.php$/) return } // footer取得 let footer = (await repeatGetElements('figcaption footer'))[0] // ブクマ済みならタグとか取得 let already = document.querySelector('a[href^="/bookmark_add.php"]') let detail = !already ? null : await request.getArtworkData(already.href) let existTag = function(tag) { if (detail) return detail.tags.find(e => e == tag) != null return false } // 既存タグに機能追加 for (let tag of footer.querySelectorAll('li a')) { tag.classList.add('selectable') tag.classList.toggle('selected', existTag(tag.textContent)) tag.addEventListener('click', listener.tag) } // wrap作成 let wrap = document.querySelector('#tb-wrap') if (wrap) wrap.outerHTML = '' wrap = document.createElement('div') wrap.id = 'tb-wrap' // 自分用タグ let mytag = document.createElement('div') mytag.id = 'mytag' mytag.innerHTML = `` let ul = document.createElement('ul') let tags = await request.getMyTags() for (let tag in tags) { let c = 'o0' if (tags[tag] >= 10) c = 'o10' if (tags[tag] >= 30) c = 'o30' if (tags[tag] >= 50) c = 'o50' if (tags[tag] >= 100) c = 'o100' c += existTag(tag) ? ' selected' : '' let li = document.createElement('li') li.innerHTML = `${tag}` li.querySelector('span').addEventListener('click', listener.tag) li.dataset.cnt = tags[tag] ul.appendChild(li) } if (tags == null || tags.length == 0) { ul.innerHTML = 'ブックマークタグがありません' } mytag.appendChild(ul) // 高さ取得してスタイル化 let style = document.querySelector('#tagbookmark-ul') if (style) style.outerHTML = '' document.body.appendChild(mytag) const mytagHeight = mytag.clientHeight document.body.removeChild(mytag) addStyle('tagbookmark-ul', `#mytag ul { height: 0; } #mytag ul.show { height: ${mytagHeight}px }`) wrap.appendChild(mytag) // 登録用text let tagText = document.createElement('input') tagText.id = 'tb-text' tagText.placeholder = '登録タグ' tagText.value = detail ? detail.tags.join(' ') : '' tagText.addEventListener('keyup', listener.text) wrap.appendChild(tagText) // タグ数 let tagCnt = document.createElement('span') tagCnt.id = 'tb-cnt' tagCnt.textContent = '1' wrap.appendChild(tagCnt) // ブックマークボタン作成 let tbm = document.createElement('button') tbm.id = 'tb-submit' if (already) { tbm.className = 'already' tbm.appendChild(already.querySelector('svg').cloneNode(true)) } else { tbm.appendChild(document.querySelector('.gtm-main-bookmark svg').cloneNode(true)) } tbm.addEventListener('click', listener.bookmark) wrap.appendChild(tbm) // footer下に追加 footer.parentNode.insertBefore(wrap, footer.nextElementSibling) setTagcnt() } function setTagcnt() { let cnt = 0 let text = document.querySelector('#tb-text').value if (text) cnt = text.replace(' ', ' ').split(' ').filter(t => t != '').length let cntText = document.querySelector('#tb-cnt') cntText.textContent = cnt cntText.style.color = cnt > 10 ? 'red' : '' } const listener = { tag: function(e) { if (e.ctrlKey) return e.preventDefault() let tag = e.target.textContent let text = document.querySelector('#tb-text'); // 自分AND同じ名前のタグをtoggle [...document.querySelectorAll('.selectable')] .filter(e => e.textContent == tag) .forEach(e => e.classList.toggle('selected')) // テキストへの変換(toggle) if (e.target.classList.contains('selected')) { text.value += (text.value ? ' ' : '') + e.target.textContent } else { text.value = text.value.replace(' ', ' ').split(' ').filter(t => t != e.target.textContent).join(' ') } setTagcnt() }, text: function(e) { // テキスト変更でタグの選択も変更 for (let tag of document.querySelectorAll('.selectable')) { let match = false for (let text of e.target.value.replace(' ', ' ').split(' ')) { if (text == tag.textContent) { match = true break } } tag.classList.toggle('selected', match) } setTagcnt() }, bookmark: async function(e) { let url = document.querySelector('link[rel="canonical"]').getAttribute('href') let work_id = url.match(/artworks\/(\d+)$/)[1] let tags = document.querySelector('#tb-text').value.replace(' ', ' ').split(' ') let comment = '' let hide = 0 let token = await request.getToken(url) request.addBookmark('illusts', work_id, comment, tags, hide, token) .then(data => { // 成功 let btn = document.querySelector('#tb-submit') if (!btn.classList.contains('already')) { btn.classList.add('already') } else { // 何度も連続でクリックされないように alert('タグを編集しました') } }) .catch(error => { // 失敗 console.error(error) alert('ブックマークに失敗しました') }) } } const request = { getToken: async function(url) { return new Promise((resolve, reject) => { fetch(url) .then(response => response.text()) .then(data => { let result = data.match(/token":"(\w+)"/) if (!result) reject(null) else resolve(result[1]) }) }) }, getMyTags: async function(marge = true) { // pixiv側の変数 dataLayer[0].user_id console.log('pixiv側の変数 user_id: ' + dataLayer[0].user_id); return new Promise((resolve, reject) => { fetch(`https://www.pixiv.net/ajax/user/${dataLayer[0].user_id}/illusts/bookmark/tags`) .then(response => response.text()) .then(data => { let json = JSON.parse(data) let tags = json.body let ret = {} for (let tag of tags.public) { ret[tag.tag] = tag.cnt } if (marge) for (let tag of tags.private) { if (tag.tag in ret) ret[tag.tag] += tag.cnt; else ret[tag.tag] = tag.cnt; } resolve(ret) }) }) }, addBookmark: async function(type, work_id, comment, tags, hide, token) { let body = { comment: comment, tags: tags, restrict: (hide ? 1 : 0), } body[type == 'illusts' ? 'illust_id' : 'novel_id'] = work_id return new Promise((resolve, reject) => { fetch(`https://www.pixiv.net/ajax/${type}/bookmarks/add`, { method: 'POST', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json; charset=utf-8', 'x-csrf-token': token, }, body: JSON.stringify(body) }) .then(response => response.json()) .then(data => { if (data.error) reject(data) else resolve(data) }) }) }, getArtworkData: async function(url) { return new Promise((resolve, reject) => { fetch(url) .then(response => response.text()) .then(text => { let html = new DOMParser().parseFromString(text, "text/html") let detail = html.querySelector('.bookmark-detail-unit form') resolve({ comment: detail.comment.value, tags: detail.tag.value.split(' '), hide: detail.restrict.value }) }) }) } }