// ==UserScript== // @name Web漫画アンテナお気に入り管理 // @description d:作者名を読み込む a:作者をお気に入りに追加/削除 e:検索ワード入力 Shift+E:全編集 // @match *://webcomics.jp/* // @version 0.1.4 // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @namespace https://greasyfork.org/users/181558 // @require https://code.jquery.com/jquery-3.4.1.min.js // @downloadURL none // ==/UserScript== (function() { var keyFunc = []; var INTERVAL = function() { return 5000; } const isBusy = function() { return Number(pref("busy") || 0) > Date.now() } const setBusy = function(delay = INTERVAL()) { if (Date.now() + delay > Number(pref("busy") || 0)) pref("busy", Date.now() + delay) } var scrollForGet = 0; const V = 0; // 1-3:verbose var db = {}; db.manga = pref("db.manga") || [] db.favo = pref('db.favo') || []; var latestget = Date.now() - INTERVAL() var busy = 0; var GF = {} document.querySelector(`head`).insertAdjacentHTML('beforeend', ``) String.prototype.autrep = function() { return this.replace(/\([^)]*\)|([^)]*)|原作|作画|漫画|キャラクター|ネーム|原案|著者|作者|シナリオ|[作|画][\::]|\:|:|・|\,|、|,|\/|/|\+|+|\&|&/gmi, " ").replace(/ +|\s+/gmi, " ").trim() } // gフラグ不可 String.prototype.match0 = function(re) { let tmp = this.match(re); if (!tmp) { return null } else if (tmp.length > 1) { return tmp[1] } else return tmp[0] } // gフラグ不可 String.prototype.sanit = function() { return this.replace(//g, ">").replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/`/g, '`') } function adja(place = document.body, pos, html) { return place ? (place.insertAdjacentHTML(pos, html), place) : null; } var JS = (v) => { return JSON.stringify(v) } var JP = (v) => { return JSON.parse(v) } var mousex = 0; var mousey = 0; var hovertimer document.addEventListener("mousemove", e => ((mousex = e.clientX), (mousey = e.clientY), (hovertimer = 0), undefined), false) var keyListen = function(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.getAttribute('contenteditable') === 'true') return; var key = (e.shiftKey ? "Shift+" : "") + (e.altKey ? "Alt+" : "") + (e.ctrlKey ? "Ctrl+" : "") + e.key; var ele = document.elementFromPoint(mousex, mousey); var sel = (window.getSelection) ? window.getSelection().toString().trim() : "" if (pushkey(key, ele, sel)) { e.preventDefault(); return false } } document.addEventListener('keydown', keyListen, false) document.addEventListener("mousedown", function(e) { // クリック var ele = document.elementFromPoint(mousex, mousey); if (e.button == 0 && ele.dataset.key) { if (pushkey(ele.dataset.key, ele)) return false } }) document.addEventListener("contextmenu", function(e) { // クリック var ele = document.elementFromPoint(mousex, mousey); if (ele.dataset.keyr) { if (pushkey(ele.dataset.keyr, ele)) { e.preventDefault(); return false } } }) function storemanga(tit, aut, ele) { db.manga = pref("db.manga") || [] db.manga = db.manga.filter(v => v.t != tit) db.manga.push({ t: tit, a: aut }) db.manga = (Array.from(new Set(db.manga.map(v => JSON.stringify(v))))).map(v => JSON.parse(v)) // uniq:オブジェクトの配列→JSON文字列配列→uniq→オブジェクトの配列 pref("db.manga", db.manga) V >= 3 && console.table(db.manga) run(ele) } function addaut(aut, ele = document) { if (!aut || aut == "-") return aut = aut.autrep() // 加工後の作者名で記憶する db.favo = pref("db.favo") || [] if (!db.favo.includes(aut)) { db.favo.push(aut) } else { db.favo = db.favo.filter(v => v !== aut) } db.favo = (Array.from(new Set(db.favo.map(v => JSON.stringify(v))))).map(v => JSON.parse(v)) // uniq:オブジェクトの配列→JSON文字列配列→uniq→オブジェクトの配列 pref("db.favo", db.favo) V >= 3 && console.table(db.favo) run(ele) } var que = { q: [], //{ele,key} add: function(ele, key) { this.q.push({ ele: ele?.closest(".entry"), key: key }) }, do: function() { V >= 2 && this.q.length && console.table(this.q) this.q.forEach(v => { var box = v.ele var key = v.key if (!box) { v.stop = 1; return 0 } var tit = eleget0('div.entry-title>a:first-child', box)?.textContent?.trim() var desc = eleget0('//span[@class="entry-detail"]/a[1]', box) var aut = eleget0('.aut', box)?.dataset?.author; if (aut == "-") { v.stop = 1; return 0 } if (aut) aut = decodeURI(aut) var descurl = desc?.href if (aut) { storemanga(tit, aut, box.parentNode) key == "a" && addaut(aut, box.parentNode) autsearch() v.stop = 1; return 0 } var q = eleget0('.autq:not(.waiting)', box); if (q) { q.classList.add("waiting"); } box.dataset.que = 1 if (descurl && !aut && !box.dataset.wait && Date.now() - latestget > INTERVAL() && !busy && !isBusy()) { busy = 1; setBusy(); box.dataset.wait = 1 v.stop = 1 var quee = eleget0('.autq', box) quee.style.color = "#f0f" V >= 1 && notify(`${"get:" + tit}\n${(Date.now() - latestget)/1000} sec.`, document.title) latestget = Date.now() if (scrollForGet) box.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }) $.get(descurl).done(got => { setBusy(INTERVAL() / 2) busy = 0 delete box.dataset.que; aut = $('div.comic-info-right div.comic-author', got)?.text()?.replace("作者: ", "").trim() || "-" V > 1 && notify(`${"done:" + tit}\n${aut}`, document.title) if (aut) { storemanga(tit, aut, box.parentNode) if (key == "a") addaut(aut, box.parentNode) } }).fail(err => { alert(`通信エラー\n${descurl}`) this.q = [] delete box.dataset.que; location.reload(); }) autsearch() } }) this.q = this.q.filter(v => !v.stop) }, } setInterval(() => { que.do() }, 1000) function pushkey(key, ele = null, sel = "") { keyFunc.forEach(v => { if (v.key === key) { v.func(ele) } }) if (/^open:/.test(key)) { window.open(key.replace(/^open:/, "")) return 1 } if (key === "e") { // e:: db.manga = pref("db.manga") || [] db.favo = pref('db.favo') || []; var favo = [...db.favo] GF.sorttype = ((GF.sorttype || 0) % 3 + 1) var [order, finstrfunc] = [ ["登録順", a => a.join(" ")], ["abc順", a => a.sort(new Intl.Collator("ja", { numeric: true, sensitivity: 'base' }).compare).join(" ")], ["長さ→abc順", a => a.sort((a, b) => a.length == b.length ? (new Intl.Collator("ja", { numeric: true, sensitivity: 'base' }).compare)(a, b) : a.length > b.length ? 1 : -1).join(" ")] ][GF.sorttype - 1] var target = (window.getSelection() && window.getSelection().toString().trim()) || (prompt(`お気に入りに登録するキーワードを入力してください\nすでに登録されている文字列を入力するとそれを削除します\n\n現在登録済み(${favo.length}): (${["登録順","abc順","長さ→abc順"][GF.sorttype-1]})\n${finstrfunc([...favo])}\n\n`) || "")?.trim(); target = target?.trim() if (!target) return; if (db.favo.includes(target)) { if (confirm(`『${target}』は既に存在します\n削除しますか?\n`)) { V && alert(`『${target}』をメモから削除しました`) db.favo = db.favo.filter(v => v != target) } } else { db.favo.push(target) } pref("db.favo", db.favo) pref("db.manga", db.manga) run() } if (key === "d" || key == "a") { // d:: a:: let descele = eleget0(".comic-info .aut", ele?.closest("#main")) || ele; if (key == "a" && descele.dataset.author) { //alert("!"); var aut = decodeURI(descele.dataset.author) addaut(aut) autsearch() return 1 } que.add(ele, key); que.do() return 1 } if (key === "Shift+E") { // E:: var tmp = prompt(`作品情報(${db.manga.length}) / お気に入り作者(${db.favo.length})\n全設定値をJSON形式で編集してください\n空欄を入力すれば全削除できます\n先頭の{の前に+を付けると現在のデータに追加(マージ)します\n\n` + JS(db), JS(db)) if (tmp !== null) { // ESCで抜けたのでなければ try { if (tmp?.trim()?.match(/^\+|^+/)) { tmp = tmp?.trim()?.replace(/^\+|^+/, "")?.trim() db.manga = (pref("db.manga") || []).concat(JSON.parse(tmp || "").manga) db.favo = (pref('db.favo') || []).concat(JSON.parse(tmp || "").favo) tmp = JSON.stringify(db) } var dbtmp = JP(tmp || '{"favo":[],"manga":[]}') dbtmp.manga = (Array.from(new Set(dbtmp.manga.map(v => JSON.stringify(v))))).map(v => JSON.parse(v)) // uniq:オブジェクトの配列→JSON文字列配列→uniq→オブジェクトの配列 dbtmp.favo = [...new Set(dbtmp.favo)]; // uniq db = dbtmp pref("db.favo", db.favo || []) pref("db.manga", db.manga || []) run(); } catch (e) { alert(e + "\n入力された文字列がうまくparseできなかったので設定を変更しません\n正しいJSON書式になっているか確認してください"); return false } } return 1 } } run() document.body.addEventListener('AutoPagerize_DOMNodeInserted', function(evt) { run(eleget0('.list.top', evt.target) || evt.target?.closest(".list.top") || document); }, false); // タブにフォーカスが戻ったら再実行 window.addEventListener("focus", () => { db.manga = pref("db.manga") || [] db.favo = pref('db.favo') || []; run() }) // 詳細画面 var aut = $('div.comic-info-right div.comic-author')?.text()?.replace("作者: ", "")?.trim() || "-" var tit = eleget0('//div/div/div/div[@class="comic-title"]/h2/a[1]')?.textContent?.trim() if (aut && tit) { storemanga(tit, aut, document) } function autsearch() { elegeta(".autsearchele").forEach(e => e.remove()); let tmp = pref('db.favo') || []; if (tmp.length) { var aut = tmp[0] var u = `https://webcomics.jp/search?q=${encodeURI(aut.autrep())}` var u2 = `https://webcomics.jp/search?q=${(aut.autrep())}` var l = aut != "-" ? `data-keyr="open:${u}"` : "" var e = adja(eleget0('//div[@id="side"]'), "afterbegin", `
${aut.autrep().sanit()} を検索 🗊
`)?.childNodes[0] elegeta('#auta,#changeaut').forEach(e => { e.addEventListener("click", v => { db.favo = pref('db.favo') || []; if (!db.favo.length) return db.favo.push(db.favo.shift()) pref('db.favo', db.favo) autsearch() }) e.addEventListener("mouseup", v => { if (v.button == 0) return setTimeout(() => { db.favo = pref('db.favo') || []; if (!db.favo.length) return db.favo.push(db.favo.shift()) pref('db.favo', db.favo) autsearch() }, 17) if (v.button != 1) { v.preventDefault(); return false; } }) e.addEventListener("contextmenu", v => { v.preventDefault(); return false }) }) } else { var e = adja(eleget0('//div[@id="side"]'), "afterbegin", `
🗊
`)?.childNodes[0] } } function run(node = document) { // run:: autsearch() elegeta('.autele', node).forEach(v => v.remove()) // 一覧画面 elegeta('.entry', node).forEach(v => { var title = eleget0('.entry-title>a:first-child', v)?.textContent?.trim() var aut = db.manga.find(v => v.t === title)?.a if (aut == "-") { adja(eleget0('.entry-date', v), "beforeend", `${aut.sanit()}`) } else if (aut) { var memo = db.favo.includes(aut.autrep()) // 加工後の作者名で記憶する var u = `https://webcomics.jp/search?q=${encodeURI(aut.autrep())}` var l = aut != "-" ? `data-keyr="open:${u}"` : "" adja(eleget0('.entry-date', v), "beforeend", `${memo?"●":"○"}${aut.autrep().sanit()}`) } else { adja(eleget0('.entry-date', v), "beforeend", ``) } }) // 詳細画面 var aut = $('div.comic-info-right div.comic-author')?.text()?.replace("作者: ", "")?.trim() || "-" var tit = eleget0('//div/div/div/div[@class="comic-title"]/h2/a[1]')?.textContent?.trim() if (aut && tit) { if (aut == "-") { adja(eleget0('.comic-info'), "afterbegin", `${aut.sanit()}`) } else if (aut) { var memo = db.favo.includes(aut.autrep()) // 加工後の作者名で記憶する var u = `https://webcomics.jp/search?q=${encodeURI(aut.autrep())}` var l = aut != "-" ? `data-keyr="open:${u}"` : "" V && notify(aut, memo) adja(eleget0('.comic-info'), "afterbegin", `${memo?"●":"○"}${aut.autrep().sanit()}`) } } } function elegeta(xpath, node = document) { if (!xpath || !node) return []; let flag if (!/^\.?\//.test(xpath)) return /:inscreen$/.test(xpath) ? [...node.querySelectorAll(xpath.replace(/:inscreen$/, ""))].filter(e => { var eler = e.getBoundingClientRect(); return (eler.top > 0 && eler.left > 0 && eler.left < document.documentElement.clientWidth && eler.top < document.documentElement.clientHeight) }) : /:visible$/.test(xpath) ? [...node.querySelectorAll(xpath.replace(/:visible$/, ""))].filter(e => e.offsetHeight) : [...node.querySelectorAll(xpath)] try { var array = []; var ele = document.evaluate("." + xpath.replace(/:visible$/, ""), node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); let l = ele.snapshotLength; for (var i = 0; i < l; i++) array[i] = ele.snapshotItem(i); return /:visible$/.test(xpath) ? array.filter(e => e.offsetHeight) : array; } catch (e) { alert(e + "\n" + xpath + "\n" + JSON.stringify(node)); return []; } } function eleget0(xpath, node = document) { if (!xpath || !node) return null; if (!/^\.?\//.test(xpath)) return /:inscreen$/.test(xpath) ? [...node.querySelectorAll(xpath.replace(/:inscreen$/, ""))].filter(e => { var eler = e.getBoundingClientRect(); return (eler.top > 0 && eler.left > 0 && eler.left < document.documentElement.clientWidth && eler.top < document.documentElement.clientHeight) })[0] ?? null : /:visible$/.test(xpath) ? [...node.querySelectorAll(xpath.replace(/:visible$/, ""))].filter(e => e.offsetHeight)[0] ?? null : node.querySelector(xpath.replace(/:visible$/, "")); try { var ele = document.evaluate("." + xpath.replace(/:visible$/, ""), node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); return ele.snapshotLength > 0 ? ele.snapshotItem(0) : null; } catch (e) { alert(e + "\n" + xpath + "\n" + JSON.stringify(node)); return null; } } function pref(name, store = null) { // prefs(name,data)で書き込み(数値でも文字列でも配列でもオブジェクトでも可)、prefs(name)で読み出し if (store === null) { // 読み出し let data = GM_getValue(name) || GM_getValue(name); if (data == undefined) return null; // 値がない if (data.substring(0, 1) === "[" && data.substring(data.length - 1) === "]") { // 配列なのでJSONで返す try { return JSON.parse(data || '[]'); } catch (e) { alert("データベースがバグってるのでクリアします\n" + e); pref(name, []); return; } } else return data; } if (store === "" || store === []) { // 書き込み、削除 GM_deleteValue(name); return; } else if (typeof store === "string") { // 書き込み、文字列 GM_setValue(name, store); return store; } else { // 書き込み、配列 try { GM_setValue(name, JSON.stringify(store)); } catch (e) { alert("データベースがバグってるのでクリアします\n" + e); pref(name, ""); } return store; } } function notify(body, title = "") { if (!("Notification" in window)) return; else if (Notification.permission == "granted") new Notification(title, { body: body }); else if (Notification.permission !== "denied") Notification.requestPermission().then(function(permission) { if (permission === "granted") new Notification(title, { body: body }); }); } })()