Web漫画アンテナお気に入り管理

d:作者名を読み込む a:作者をお気に入りに追加/削除 e:検索ワード入力 Shift+E:全編集

Per 16-08-2024. Zie de nieuwste versie.

  1. // ==UserScript==
  2. // @name Web漫画アンテナお気に入り管理
  3. // @description d:作者名を読み込む a:作者をお気に入りに追加/削除 e:検索ワード入力 Shift+E:全編集
  4. // @match *://webcomics.jp/*
  5. // @version 0.1.5
  6. // @grant GM_setValue
  7. // @grant GM_getValue
  8. // @grant GM.openInTab
  9. // @grant GM_deleteValue
  10. // @namespace https://greasyfork.org/users/181558
  11. // @require https://code.jquery.com/jquery-3.4.1.min.js
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. var keyFunc = [];
  16. var INTERVAL = function() { return 5000; }
  17. const isBusy = function() { return Number(pref("busy") || 0) > Date.now() }
  18. const setBusy = function(delay = INTERVAL()) { if (Date.now() + delay > Number(pref("busy") || 0)) pref("busy", Date.now() + delay) }
  19.  
  20. var scrollForGet = 0;
  21. const V = 0; // 1-3:verbose
  22.  
  23. let addstyle = {
  24. added: [],
  25. add: function(str) {
  26. if (this.added.some(v => v[1] === str)) return;
  27. var S = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // var S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
  28. var d = Date.now()
  29. var uid = Array.from(Array(12)).map(() => S[Math.floor((d + Math.random() * S.length) % S.length)]).join('')
  30. document.head.insertAdjacentHTML("beforeend", `<style id="${uid}">${str}</style>`);
  31. this.added.push([uid, str]);
  32. return uid;
  33. },
  34. remove: function(str) { // str:登録したCSSでもaddでreturnしたuidでも良い
  35. let uid = this.added.find(v => v[1] === str || v[0] === str)?.[0]
  36. if (uid) {
  37. eleget0(`#${uid}`)?.remove()
  38. this.added = this.added.filter(v => v[0] !== uid)
  39. }
  40. }
  41. }
  42.  
  43. var db = {};
  44. db.manga = pref("db.manga") || [];
  45. db.favo = pref('db.favo') || [];
  46. db.favhis = pref('db.favhis') || [];
  47. // db.favhis= (Array.isArray(db.favhis)&&db.favhis.length) ? new Map(db.favhis): new Map();
  48. var latestget = Date.now() - INTERVAL()
  49. var busy = 0;
  50. var GF = {}
  51. document.querySelector(`head`).insertAdjacentHTML('beforeend', `<style>.waiting{ display: inline-block; vertical-align: middle; color: #666; line-height: 1; width: 1em; height: 1em; border: 0.12em solid currentColor; border-top-color: rgba(102, 102, 102, 0.3); border-radius: 50%; box-sizing: border-box; -webkit-animation: rotate 1s linear infinite; animation: rotate 1s linear infinite; } @-webkit-keyframes rotate { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } } @keyframes rotate { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } }</style>`)
  52.  
  53. String.prototype.autrep = function() { return this.replace(/\([^)]*\)|([^)]*)|原作|作画|漫画|キャラクター|キャタクターデザイン|ネーム|原案|著者|作者|シナリオ|[作|画][\::]|\:|:|・|\,|、|,|\/|/|\+|+|\&|&/gmi, " ").replace(/ +|\s+/gmi, " ").trim() } // gフラグ不可
  54. 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フラグ不可
  55. String.prototype.sanit = function() { return this.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/`/g, '&#x60;') }
  56. String.prototype.esc = function() { return this.replace(/[&<>"'`]/g, match => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '`': '&#96;' } [match])) }
  57.  
  58.  
  59. function adja(place = document.body, pos, html) {
  60. return place ? (place.insertAdjacentHTML(pos, html), place) : null;
  61. }
  62. var JS = (v) => { return JSON.stringify(v) }
  63. var JP = (v) => { return JSON.parse(v) }
  64.  
  65. var mousex = 0;
  66. var mousey = 0;
  67. var hovertimer
  68. document.addEventListener("mousemove", e => ((mousex = e.clientX), (mousey = e.clientY), (hovertimer = 0), undefined), false)
  69.  
  70. var keyListen = function(e) {
  71. if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.getAttribute('contenteditable') === 'true') return;
  72. var key = (e.shiftKey ? "Shift+" : "") + (e.altKey ? "Alt+" : "") + (e.ctrlKey ? "Ctrl+" : "") + e.key;
  73. var ele = document.elementFromPoint(mousex, mousey);
  74. var sel = (window.getSelection) ? window.getSelection().toString().trim() : ""
  75. if (pushkey(key, ele, sel)) { e.preventDefault(); return false }
  76. }
  77. document.addEventListener('keydown', keyListen, false)
  78.  
  79. document.addEventListener("mousedown", function(e) { // クリック
  80. var ele = document.elementFromPoint(mousex, mousey);
  81. if (e.button == 0 && ele.dataset.key) {
  82. if (pushkey(ele.dataset.key, ele)) return false
  83. }
  84. })
  85. document.addEventListener("contextmenu", function(e) { // クリック
  86. var ele = document.elementFromPoint(mousex, mousey);
  87. if (ele.dataset.keyr) {
  88. if (pushkey(ele.dataset.keyr, ele)) { e.preventDefault(); return false }
  89. }
  90. })
  91.  
  92. function storemanga(tit, aut, ele) {
  93. db.manga = pref("db.manga") || []
  94. db.manga = db.manga.filter(v => v.t != tit)
  95. db.manga.push({ t: tit, a: aut })
  96. db.manga = (Array.from(new Set(db.manga.map(v => JSON.stringify(v))))).map(v => JSON.parse(v)) // uniq:オブジェクトの配列→JSON文字列配列→uniq→オブジェクトの配列
  97. pref("db.manga", db.manga)
  98. V >= 3 && console.table(db.manga)
  99. run(ele)
  100. }
  101.  
  102. function addaut(aut, ele = document) {
  103. if (!aut || aut == "-") return
  104. aut = aut.autrep() // 加工後の作者名で記憶する
  105. db.favo = pref("db.favo") || []
  106. if (!db.favo.includes(aut)) { db.favo.push(aut) } else { db.favo = db.favo.filter(v => v !== aut) }
  107. db.favo = (Array.from(new Set(db.favo.map(v => JSON.stringify(v))))).map(v => JSON.parse(v)) // uniq:オブジェクトの配列→JSON文字列配列→uniq→オブジェクトの配列
  108. pref("db.favo", db.favo)
  109. V >= 3 && console.table(db.favo)
  110. run(ele)
  111. }
  112.  
  113. var que = {
  114. q: [], //{ele,key}
  115. add: function(ele, key) {
  116. this.q.push({ ele: ele?.closest(".entry"), key: key })
  117. },
  118. do: function() {
  119. V >= 2 && this.q.length && console.table(this.q)
  120. this.q.forEach(v => {
  121. var box = v.ele
  122. var key = v.key
  123. if (!box) { v.stop = 1; return 0 }
  124. var tit = eleget0('div.entry-title>a:first-child', box)?.textContent?.trim()
  125. var desc = eleget0('//span[@class="entry-detail"]/a[1]|.//a[contains(@class,"entry-detail")]', box)
  126. var aut = eleget0('.aut', box)?.dataset?.author;
  127.  
  128. if (aut == "-") { v.stop = 1; return 0 }
  129. if (aut) aut = decodeURI(aut)
  130. var descurl = desc?.href
  131. if (aut) {
  132. storemanga(tit, aut, box.parentNode)
  133. key == "a" && addaut(aut, box.parentNode)
  134. autsearch()
  135. v.stop = 1;
  136. return 0
  137. }
  138.  
  139. var q = eleget0('.autq:not(.waiting)', box);
  140. if (q) { q.classList.add("waiting"); }
  141. box.dataset.que = 1
  142.  
  143. if (descurl && !aut && !box.dataset.wait && Date.now() - latestget > INTERVAL() && !busy && !isBusy()) {
  144. busy = 1;
  145. setBusy();
  146. box.dataset.wait = 1
  147. v.stop = 1
  148.  
  149. var quee = eleget0('.autq', box)
  150. quee.style.color = "#f0f"
  151.  
  152. V >= 1 && notify(`${"get:" + tit}\n${(Date.now() - latestget)/1000} sec.`, document.title)
  153. latestget = Date.now()
  154.  
  155. if (scrollForGet) box.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" })
  156. $.get(descurl).done(got => {
  157. setBusy(INTERVAL() / 2)
  158. busy = 0
  159. //latestget = Date.now()-INTERVAL()/2
  160. delete box.dataset.que;
  161. aut = $('div.comic-info-right div.comic-author', got)?.text()?.replace("作者: ", "").trim() || "-"
  162. V > 1 && notify(`${"done:" + tit}\n${aut}`, document.title)
  163.  
  164. if (aut) {
  165. storemanga(tit, aut, box.parentNode)
  166. if (key == "a") addaut(aut, box.parentNode)
  167. }
  168. }).fail(err => {
  169. alert(`通信エラー\n${descurl}`)
  170. this.q = []
  171. delete box.dataset.que;
  172. location.reload();
  173. })
  174. autsearch()
  175. }
  176. })
  177. this.q = this.q.filter(v => !v.stop)
  178. },
  179. }
  180. setInterval(() => { que.do() }, 1000)
  181.  
  182. function pushkey(key, ele = null, sel = "") {
  183. keyFunc.forEach(v => { if (v.key === key) { v.func(ele) } })
  184. if (/^open:/.test(key)) {
  185. window.open(key.replace(/^open:/, ""))
  186. return 1
  187. }
  188. if (key === "e") { // e::
  189. db.manga = pref("db.manga") || []
  190. db.favo = pref('db.favo') || [];
  191. db.favhis = pref('db.favhis') || [];
  192. var favo = [...db.favo]
  193.  
  194. GF.sorttype = ((GF.sorttype || 0) % 3 + 1)
  195. var [order, finstrfunc] = [
  196. ["登録順", a => a.join(" ")],
  197. ["abc順", a => a.sort(new Intl.Collator("ja", { numeric: true, sensitivity: 'base' }).compare).join(" ")],
  198. ["長さ→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(" ")]
  199. ][GF.sorttype - 1]
  200. 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();
  201. target = target?.trim()
  202. if (!target) return;
  203. if (db.favo.includes(target)) {
  204. if (confirm(`『${target}』は既に存在します\n削除しますか?\n`)) {
  205. V && alert(`『${target}』をメモから削除しました`)
  206. db.favo = db.favo.filter(v => v != target)
  207. }
  208. } else {
  209. db.favo.push(target)
  210. }
  211. pref("db.favo", db.favo)
  212. pref("db.manga", db.manga)
  213. pref("db.favhis", db.favhis);
  214. run()
  215. }
  216. if (key === "d" || key == "a") { // d:: a::
  217. let descele = eleget0(".comic-info .aut", ele?.closest("#main")) || ele;
  218. if (key == "a" && descele.dataset.author) { //alert("!");
  219. var aut = decodeURI(descele.dataset.author)
  220. addaut(aut)
  221. autsearch()
  222. return 1
  223. }
  224. que.add(ele, key);
  225. que.do()
  226. return 1
  227. }
  228. if (key === "Shift+E") { // Shift+E::
  229. var tmp = prompt(`作品情報(${db.manga.length}) / お気に入り作者(${db.favo.length})\n全設定値をJSON形式で編集してください\n空欄を入力すれば全削除できます\n先頭の{の前に+を付けると現在のデータに追加(マージ)します\n\n` + JS(db), JS(db))
  230. if (tmp !== null) { // ESCで抜けたのでなければ
  231. try {
  232. if (tmp?.trim()?.match(/^\+|^+/)) {
  233. tmp = tmp?.trim()?.replace(/^\+|^+/, "")?.trim()
  234. db.manga = (pref("db.manga") || []).concat(JSON.parse(tmp || "").manga)
  235. db.favo = (pref('db.favo') || []).concat(JSON.parse(tmp || "").favo)
  236. db.favhis = (pref('db.favhis') || []).concat(JSON.parse(tmp || "").favhis)
  237. tmp = JSON.stringify(db)
  238. }
  239. var dbtmp = JP(tmp || '{"favo":[],"manga":[],"favhis":[]}')
  240. dbtmp.manga = (Array.from(new Set(dbtmp.manga.map(v => JSON.stringify(v))))).map(v => JSON.parse(v)) // uniq:オブジェクトの配列→JSON文字列配列→uniq→オブジェクトの配列
  241. dbtmp.favo = [...new Set(dbtmp.favo)]; // uniq
  242. dbtmp.favhis = (Array.from(new Set(dbtmp.favhis.map(v => JSON.stringify(v))))).map(v => JSON.parse(v)) // uniq:オブジェクトの配列→JSON文字列配列→uniq→オブジェクトの配列
  243. db = dbtmp
  244. pref("db.favo", db.favo || [])
  245. pref("db.manga", db.manga || [])
  246. pref("db.favhis", db.favhis || []);
  247. $("#favpanel").remove()
  248. run();
  249. } catch (e) {
  250. alert(e + "\n入力された文字列がうまくparseできなかったので設定を変更しません\n正しいJSON書式になっているか確認してください");
  251. return false
  252. }
  253. }
  254. return 1
  255. }
  256.  
  257.  
  258. //
  259. if (key == "0") { // 0::メモ一覧一括削除画面
  260. db.favhis = pref('db.favhis') || [];
  261. let coll = new Intl.Collator("ja", { numeric: true, sensitivity: 'base' })
  262. var newstr = db.favhis.sort((a, b) => coll.compare(a[1].t, b[1].t)).sort((a, b) => coll.compare(a[1].a, b[1].a)).sort((a, b) => b[1].p - a[1].p)
  263. //var newstr = db.favhis.sort((a, b) => b[1].p - a[1].p)
  264. var words = newstr
  265. $("#favpanel").remove()
  266. end(document.body, `<div id="favpanel" style="width:90%; max-width:95% !important; position:absolute; top:1em; left: 50%; border-radius:1em; transform: translate(-50%, 0%); box-shadow:0px 0px 99999em 9999em #000c; word-break:break-all; z-index:111; padding:2em; margin:auto; background-color:#fff; line-height:1.8em;"><span id="favpanelclose" style="cursor:pointer;float:right;">×(Esc)</span>0:○メモを付けた作品履歴(${words.length})<br><br><div><table id="favtable" style=""><tbody><tr><th>削除</th><th>No.</th><th class="sortableColumn">メモ長</th><th></th><th>著者</th><th>作品名</th><th>url</th><th>サイト</th><th title="検索用">詳細</th><th>記帳</th></tr></tbody></table><br><textarea id="favta"></textarea>`)
  267. addstyle.add(`#favtable{margin:0 auto; width:100%;}
  268. #favpanel a{ text-decoration:none; padding-right:0.5em;}
  269. #favpanel td{padding:0 0.5em; max-width:16vw; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  270. #favpanel tr:nth-child(even){background-color:#f0f0f8;}
  271. #favpanel th{background-color:#dde; padding:0.5em 0.5em;}
  272. #favpanel img{max-width:100%;}
  273. #favpanel span.yhmMyMemo{user-select: none !important; -webkit-user-select: none !important; -moz-user-select: none !important; -ms-user-select: none;}
  274. #favpanel .yhmMyMemo{font-size:90%;}
  275. #favpanel .favfiltericon{color: #aa7; font-size:99%; cursor:pointer; float: right; user-select: none !important; -webkit-user-select: none !important; -moz-user-select: none !important; -ms-user-select: none;}
  276. #favpanel .favhissum{ max-width:2vw !important; }
  277. `)
  278. let list = []
  279. words.forEach((w, i) => {
  280. list.push(`<tr class="favhisentry" title="${(w[1]?.sum||"")?.esc()}"><td data-delfav="${escape(w[1].t+w[1].a)}" style="cursor:pointer; text-align:center;" title="削除">🗑</td>
  281. <td style="text-align:center;">${i+1}</td>
  282. <td style="text-align:center;">${w[1].p}</td>
  283. <td style="width:3em;"><a href="${w[1].h}"><img class="favhisimg" loading="lazy" src="${w[1].i}"></a></td>
  284. <td><span class="favfiltericon" title="Solo" data-favfilter="${escape(w[1].a)}">▼</span><span><a href="https://webcomics.jp/search?q=${encodeURI(w[1].a)}" class="favhisaut">${w[1].a.esc()}</a></span></td>
  285. <td style="max-width:21vw;" class="favhistitletd"><span><a href="https://webcomics.jp/search?q=${encodeURI(w[1].t)}" class="favhistitle">${w[1].t.esc()}</a></span></td>
  286. <td><br style="display:block;"><a href="${w[1].h}" class="favhishref">${w[1].h}</a></td>
  287. <td><span><span class="favhissite">${w[1].s.esc()}</span></span></td>
  288. <td class="favhissum"><br style="display:block;">${w[1]?.sum?.esc()||""}</td>
  289. <td>${gettime("YYYY/MM/DD",(new Date(w[1].o)))}</td>
  290. </tr>`);
  291. });
  292. // <td style="max-width:21vw;" class="favhistitletd"><span><a href="${w[1].d}" class="favhistitle">${w[1].t.esc()}</a></span></td>
  293. list = list.join("") + "</table></div>";
  294. end(eleget0("#favtable"), list)
  295.  
  296. function dispTA() { // textareaにテキスト化
  297. let ta = eleget0('textarea#favta');
  298. if (ta) {
  299. ta.style.width = "80%";
  300. ta.style.height = "5em";
  301. ta.value = elegeta('.favhisentry:visible').map(n => `${eleget0('.favhistitle',n)?.textContent||""} ${eleget0('.favhisaut',n)?.textContent||""}\n${eleget0('.favhishref',n)?.href||""}\n`).join("");
  302. }
  303. }
  304. setDragCol()
  305. sortableTH("#favpanel th", dispTA)
  306. dispTA()
  307.  
  308. eleget0('#favpanel')?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
  309. $('#favpanelclose').one("click", e => $('#favpanel').remove())
  310.  
  311. GF.filteron = 0;
  312. cbOnce(() => document.addEventListener("click", e => {
  313. if (eleget0('#favpanel') && !e?.target?.closest(`#favpanel`)) {
  314. $('#favpanel').remove()
  315. e.stopPropagation()
  316. e.preventDefault()
  317. return false;
  318. }
  319. let filter = e.target?.dataset?.favfilter; // フィルタ
  320. if (filter) {
  321. e.stopPropagation()
  322. e.preventDefault()
  323. GF.filteron = 1 - GF.filteron;
  324. elegeta('.favhisentry').forEach(e => {
  325. e.style.display = "revert";
  326. //console.log(GF.filteron, e,unescape(filter))
  327. if (GF.filteron && e.textContent.indexOf(unescape(filter)) == -1) {
  328. $(e).fadeOut(222);
  329. setTimeout(() => { e.style.display = "none"; }, 222);
  330. }
  331. })
  332. setTimeout(() => {
  333. e?.target?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
  334. dispTA()
  335. }, GF.filteron ? 223 : 1)
  336. addstyle.add(`.attra{outline:4px solid #0f0;}`)
  337. e.target.closest("tr")?.classList.add("attra");
  338. setTimeout(() => $('.attra').removeClass("attra"), 1500)
  339. return false;
  340. }
  341. let f = e.target?.dataset?.delfav; // 削除
  342. if (!f) return;
  343. db.favhis = db.favhis.filter(v => v[0] != unescape(f))
  344. pref("db.favhis", db.favhis);
  345. e.target?.closest('tr')?.remove()
  346. }))
  347. cbOnce(() => document.addEventListener("keydown", e => {
  348. if (e.key == "Escape") $('#favpanel').remove()
  349. }))
  350. document.dispatchEvent(new CustomEvent("requestyhm")) //setTimeout(() => requestAnimationFrame(() => document.dispatchEvent(new CustomEvent("requestyhm"))), 0)
  351. }
  352. }
  353.  
  354. run()
  355. // document.body.addEventListener('AutoPagerize_DOMNodeInserted', function(evt) { run(evt.target); }, false);
  356. document.body.addEventListener('AutoPagerize_DOMNodeInserted', function(evt) { run(eleget0('.list.top', evt.target) || evt.target?.closest(".list.top") || document); }, false);
  357.  
  358. // タブにフォーカスが戻ったら再実行
  359. window.addEventListener("focus", () => {
  360. db.manga = pref("db.manga") || []
  361. db.favo = pref('db.favo') || [];
  362. db.favhis = pref('db.favhis') || [];
  363. run()
  364. })
  365.  
  366. // 詳細画面
  367. var aut = $('div.comic-info-right div.comic-author')?.text()?.replace("作者: ", "")?.trim() || "-"
  368. var tit = eleget0('//div/div/div/div[@class="comic-title"]/h2/a[1]')?.textContent?.trim()
  369.  
  370. if (aut && tit) {
  371. storemanga(tit, aut, document)
  372. }
  373.  
  374. function autsearch() {
  375. elegeta(".autsearchele").forEach(e => e.remove());
  376. let tmp = pref('db.favo') || [];
  377. if (tmp.length) {
  378. var aut = tmp[0]
  379. var u = `https://webcomics.jp/search?q=${encodeURI(aut.autrep())}`
  380. var u2 = `https://webcomics.jp/search?q=${(aut.autrep())}`
  381. var l = aut != "-" ? `data-keyr="open:${u}"` : ""
  382. var e = adja(eleget0('//div[@id="side"]'), "afterbegin", `<div class="autsearchele ignoreMe"><a id="auta" href="${u}" title='左クリック:このキーワードを検索\n右クリック:開かずにキーワードを変更' style="font-size:12px; ">${aut.autrep().sanit()}</a> を検索 <span title="左クリック/e:お気に入りワードを追加\n右クリック/Shift+E:全設定値を編集" style="cursor:pointer;" data-key="e" data-keyr="Shift+E">&#128458;</span></div>`)?.childNodes[0]
  383. elegeta('#auta,#changeaut').forEach(e => {
  384. e.addEventListener("click", v => {
  385. db.favo = pref('db.favo') || [];
  386. if (!db.favo.length) return
  387. db.favo.push(db.favo.shift())
  388. pref('db.favo', db.favo)
  389. autsearch()
  390. })
  391. e.addEventListener("mouseup", v => {
  392. if (v.button == 0) return
  393. setTimeout(() => {
  394. db.favo = pref('db.favo') || [];
  395. if (!db.favo.length) return
  396. db.favo.push(db.favo.shift())
  397. pref('db.favo', db.favo)
  398. autsearch()
  399. }, 17)
  400. if (v.button != 1) { v.preventDefault(); return false; }
  401. })
  402. e.addEventListener("contextmenu", v => { v.preventDefault(); return false })
  403. })
  404. } else {
  405. var e = adja(eleget0('//div[@id="side"]'), "afterbegin", `<div class="autsearchele ignoreMe"><span title="左クリック/e:お気に入りワードを新規作成\n右クリック/Shift+E:全設定値を編集" style="cursor:pointer;" data-key="e" data-keyr="Shift+E">&#128458;</span></div>`)?.childNodes[0]
  406. }
  407. }
  408.  
  409. function run(node = document) { // run::
  410. autsearch()
  411. elegeta('.autele', node).forEach(v => v.remove())
  412.  
  413. // 一覧画面
  414. elegeta('.entry', node).forEach(v => {
  415. var title = eleget0('.entry-title>a:first-child', v)?.textContent?.trim()
  416. var aut = db.manga.find(v => v.t === title)?.a
  417. if (aut == "-") {
  418. adja(eleget0('.entry-date', v), "beforeend", `<span data-author="${encodeURI(aut)}" class="autele aut" style="cursor:pointer;font-size:12px; margin:0 0 0 1em; color:#444;">${aut.sanit()}</span>`)
  419. } else if (aut) {
  420. var memo = db.favo.includes(aut.autrep()) // 加工後の作者名で記憶する
  421. var u = `https://webcomics.jp/search?q=${encodeURI(aut.autrep())}`
  422. var l = aut != "-" ? `data-keyr="open:${u}"` : ""
  423. adja(eleget0('.entry-date', v), "beforeend", `<span data-author="${encodeURI(aut)}" ${memo?'data-gakusai="1"':''} class="autele aut${memo?' fav':''}" title='${aut}\nクリック/a:作者をお気に入りに追加/解除' data-key="a" style=" cursor:pointer; ${memo?"color:#00f;":"color:#444;"}font-size:12px; margin:0 0 0 1em;">${memo?"●":"○"}</span><a href="${u}" data-author="${encodeURI(aut)}" class="autele aut autname" title='${aut.sanit()}\n左クリック:作者を検索\na:作者をお気に入りに追加/解除' style=" ${memo?"color:#00f;font-weight:bold;":"color:#444;"}font-size:12px; margin:0 0 0 0.25em;">${aut.autrep().sanit()}</a>`)
  424. } else {
  425. adja(eleget0('.entry-date', v), "beforeend", `<span data-key="d" title="d:作者を取得\na:作者をお気に入りに追加" class="autele autq${v.dataset.que?" waiting":""}" style="cursor:pointer;font-size:12px; margin:0 0 0 1em; ${memo?"color:#00f;font-weight:bold;":"color:#444;"}">?</span>`)
  426. }
  427. })
  428.  
  429. // 詳細画面
  430. var aut = $('div.comic-info-right div.comic-author')?.text()?.replace("作者: ", "")?.trim() || "-"
  431. var tit = eleget0('//div/div/div/div[@class="comic-title"]/h2/a[1]')?.textContent?.trim()
  432. if (aut && tit) {
  433. if (aut == "-") {
  434. adja(eleget0('.comic-info'), "afterbegin", `<span data-author="${encodeURI(aut)}" class="autele aut" style="float:right; font-size:12px; margin:0 0 0 1em; color:#444;">${aut.sanit()}</span>`)
  435. } else if (aut) {
  436. var memo = db.favo.includes(aut.autrep()) // 加工後の作者名で記憶する
  437. var u = `https://webcomics.jp/search?q=${encodeURI(aut.autrep())}`
  438. var l = aut != "-" ? `data-keyr="open:${u}"` : ""
  439. V && notify(aut, memo)
  440. adja(eleget0('.comic-info'), "afterbegin", `<span class="autele" style="float:right; "><span data-author="${encodeURI(aut)}" class="aut" title='${aut}\nクリック/a:作者をお気に入りに追加/解除' data-key="a" style=" cursor:pointer; ${memo?"color:#00f;":"color:#444;"}font-size:12px; margin:0 0 0 1em;">${memo?"●":"○"}</span><a href="${u}" data-author="${encodeURI(aut)}" class="autele aut autname" title='${aut}\n左クリック:作者を検索\na:作者をお気に入りに追加/解除' style=" ${memo?"color:#00f;font-weight:bold;":"color:#444;"}font-size:12px; margin:0 0 0 0.25em;">${aut.autrep().sanit()}</a></span>`)
  441. }
  442. }
  443.  
  444. // 好き履歴保存 0::
  445. moq3(ele => {
  446. if (ele.matches(`.yhmMyMemoO`) || ele.querySelector(`.yhmMyMemoO`)) {
  447. clearTimeout(GF?.favST)
  448. GF.favST = setTimeout(() => {
  449. db.favhis = pref('db.favhis') || [];
  450. const favhis = new Map(db.favhis)
  451. const lastfav = JSON.stringify(db.favhis)
  452. elegeta('div.entry').map(v => { return { box: v, score: elegeta('.entry .yhmMyMemoO', v).reduce((a, b) => a + b?.textContent?.length, 0) } }).filter(v => v.score).forEach(n => {
  453. let t = eleget0('div.entry-title > a:nth-of-type(1) , a.favhistitle', n.box);
  454. let title = t.textContent?.trim();
  455. let author = eleget0('a.autele.aut.autname , .favhisaut', n.box)?.textContent || "";
  456. let site = eleget0('div.entry-site > a , span.favhissite', n.box);
  457. let oldest = favhis.get(title + author)?.o || (new Date().setHours(0, 0, 0, 0));
  458. let i = eleget0('div.entry-thumb > a > img , .favhisimg', n.box)?.src || ""
  459. let detail = eleget0('span.entry-detail > a , a.favhishref', n.box)?.href || "";
  460. let summary = eleget0('.entry-summary', n.box)?.textContent?.trim() || eleget0('.favhissum', n.box)?.textContent?.trim() || favhis.get(title + author)?.sum || "";
  461. if (favhis.get(title)?.d == detail) favhis.delete(title) //タイトル同じで作者名未取得でdetailが同じものは削除=実質上書き
  462. favhis.set(title + author, { t: t.textContent?.trim(), h: t.href, a: author, s: site?.textContent?.trim() || "", i: i, p: n.score, o: oldest, d: detail, sum: summary })
  463. })
  464. if (lastfav != JSON.stringify([...favhis])) {
  465. db.favhis = [...favhis];
  466. pref("db.favhis", [...favhis]);
  467. }
  468. }, 555)
  469. }
  470. })
  471.  
  472. function moq3(cb, observeNode = document.body) {
  473. let mo = new MutationObserver((m) => {
  474. let eles = [...m.filter(v => v.addedNodes).map(v => [...v.addedNodes]).filter(v => v.length)].flat().filter(v => v.nodeType === 1).forEach(v => cb(v));
  475. })?.observe(observeNode || document.body, { attributes: false, childList: true, subtree: true });
  476. return () => {
  477. mo?.disconnect();
  478. mo = null;
  479. }
  480. }
  481.  
  482. }
  483.  
  484. function elegeta(xpath, node = document) {
  485. if (!xpath || !node) return [];
  486. let flag
  487. 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)]
  488. try {
  489. var array = [];
  490. var ele = document.evaluate("." + xpath.replace(/:visible$/, ""), node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  491. let l = ele.snapshotLength;
  492. for (var i = 0; i < l; i++) array[i] = ele.snapshotItem(i);
  493. return /:visible$/.test(xpath) ? array.filter(e => e.offsetHeight) : array;
  494. } catch (e) { alert(e + "\n" + xpath + "\n" + JSON.stringify(node)); return []; }
  495. }
  496.  
  497. function eleget0(xpath, node = document) {
  498. if (!xpath || !node) return null;
  499. 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$/, ""));
  500. try {
  501. var ele = document.evaluate("." + xpath.replace(/:visible$/, ""), node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  502. return ele.snapshotLength > 0 ? ele.snapshotItem(0) : null;
  503. } catch (e) { alert(e + "\n" + xpath + "\n" + JSON.stringify(node)); return null; }
  504. }
  505.  
  506. function pref(name, store = null) { // prefs(name,data)で書き込み(数値でも文字列でも配列でもオブジェクトでも可)、prefs(name)で読み出し
  507. if (store === null) { // 読み出し
  508. let data = GM_getValue(name) || GM_getValue(name);
  509. if (data == undefined) return null; // 値がない
  510. if (data.substring(0, 1) === "[" && data.substring(data.length - 1) === "]") { // 配列なのでJSONで返す
  511. try { return JSON.parse(data || '[]'); } catch (e) {
  512. alert("データベースがバグってるのでクリアします\n" + e);
  513. pref(name, []);
  514. return;
  515. }
  516. } else return data;
  517. }
  518. if (store === "" || store === []) { // 書き込み、削除
  519. GM_deleteValue(name);
  520. return;
  521. } else if (typeof store === "string") { // 書き込み、文字列
  522. GM_setValue(name, store);
  523. return store;
  524. } else { // 書き込み、配列
  525. try { GM_setValue(name, JSON.stringify(store)); } catch (e) {
  526. alert("データベースがバグってるのでクリアします\n" + e);
  527. pref(name, "");
  528. }
  529. return store;
  530. }
  531. }
  532.  
  533. function notify(body, title = "") {
  534. if (!("Notification" in window)) return;
  535. else if (Notification.permission == "granted") new Notification(title, { body: body });
  536. else if (Notification.permission !== "denied") Notification.requestPermission().then(function(permission) {
  537. if (permission === "granted") new Notification(title, { body: body });
  538. });
  539. }
  540.  
  541. function before(e, html) { e?.insertAdjacentHTML('beforebegin', html); return e?.previousElementSibling; }
  542.  
  543. function begin(e, html) { e?.insertAdjacentHTML('afterbegin', html); return e?.firstChild; }
  544.  
  545. function end(e, html) { e?.insertAdjacentHTML('beforeend', html); return e?.lastChild; }
  546.  
  547. function after(e, html) { e?.insertAdjacentHTML('afterend', html); return e?.nextElementSibling; }
  548.  
  549. function gettime(_fmt = 'YYYY/MM/DD hh:mm:ss.iii', _dt = new Date()) {
  550. return [
  551. ['YYYY', _dt.getFullYear()],
  552. ['MM', _dt.getMonth() + 1],
  553. ['DD', _dt.getDate()],
  554. ['hh', _dt.getHours()],
  555. ['mm', _dt.getMinutes()],
  556. ['ss', _dt.getSeconds()],
  557. ['iii', _dt.getMilliseconds()],
  558. ].reduce((s, a) => s.replace(a[0], `${a[1]}`.padStart(a[0].length, '0')), _fmt)
  559. }
  560.  
  561. function sortableTH(selector = "th", cb = () => {}) { // 重複して何度呼んでも大丈夫
  562. if (!sortableTH?.added?.size) { // 呼ばれたのは初めて
  563. $(document).on("mousedown", selector, function(e) { // ソート
  564. e.target.dataset.order = 1 - (e.target.dataset.order || 0)
  565. let c = e?.target?.cellIndex;
  566. let tablebottom = e.target.closest('tbody')
  567. var table = e.target.closest('table')
  568. let collator = new Intl.Collator("ja", { numeric: true, sensitivity: 'base' })
  569. var trs = Array.from(table.rows).slice(1).sort((a, b) => collator.compare(a.cells[c].textContent, b.cells[c].textContent))
  570. if (e.target.dataset.order == 0) trs.reverse()
  571. trs.forEach(tr => tablebottom.appendChild(tr))
  572. cb();
  573. })
  574. }
  575. if (!sortableTH?.added?.has(selector)) document.head.insertAdjacentHTML("beforeend", `<style>${selector} {cursor:pointer;}</style>`); // この引数は初めて
  576. sortableTH.added = (sortableTH.added || new Set()).add(selector) // 1度やったのを記憶
  577. }
  578.  
  579. // Tableの縦罫線をドラッグで動かす
  580. function setDragCol(dragTarget = "td,th", GRABBABLE_WIDTH_PX = 8) {
  581. if (setDragCol?.done) return;
  582. else setDragCol.done = 1; // 1度しかやらない
  583. document.addEventListener('mousedown', startResize, true);
  584. document.addEventListener('mousemove', e => {
  585. let pare = e?.target?.nodeType == 1 && e?.target?.closest(dragTarget)
  586. if (pare && (e.clientX - pare.getBoundingClientRect().right > -GRABBABLE_WIDTH_PX)) {
  587. if (document.body.style.cursor != 'col-resize') document.body.style.cursor = 'col-resize';
  588. } else {
  589. if (!setDragCol.ingrab && document.body.style.cursor != 'default') document.body.style.cursor = 'default'
  590. }
  591. });
  592.  
  593. function startResize(e) {
  594. let pare = e?.target?.nodeType == 1 && e?.target?.closest(dragTarget)
  595. if (!pare) return;
  596. if (pare?.getBoundingClientRect()?.right - e?.clientX > GRABBABLE_WIDTH_PX) return;
  597.  
  598. e.preventDefault();
  599. e.stopPropagation();
  600. let startX = e.clientX;
  601. let startWidth = parseFloat(getComputedStyle(pare).getPropertyValue('width'))
  602. setDragCol.ingrab = 1;
  603. document.addEventListener('mousemove', dragColumn);
  604. document.addEventListener('mouseup', endDrag);
  605. return false;
  606.  
  607. function dragColumn(em) {
  608. //let pare=e.target.closest(dragTarget(e))
  609. if (["TD", "TH"].includes(pare.tagName)) {
  610. [...pare.closest("table").querySelectorAll(`:is(td,th):nth-child(${pare.cellIndex + 1})`)].forEach(cell => {
  611. cell.style.width = `${startWidth + em.clientX - startX}px`;
  612. cell.style.overflowWrap = "anywhere";
  613. //cell.style.whiteSpace = "break-spaces";
  614. cell.style.whiteSpace = "initial";
  615. cell.style.wordWrap = "break-word";
  616. cell.style.minWidth = `0px`;
  617. cell.style.maxWidth = `${Number.MAX_SAFE_INTEGER}px`;
  618. })
  619. } else {
  620. pare.style.resize = "both";
  621. pare.style.overflow = "auto"
  622. pare.style.width = `${startWidth + em.clientX - startX}px`;
  623. pare.style.minWidth = `0px`;
  624. pare.style.maxWidth = `${Number.MAX_SAFE_INTEGER}px`;
  625. }
  626. }
  627.  
  628. function endDrag() {
  629. document.removeEventListener('mousemove', dragColumn);
  630. document.removeEventListener('mouseup', endDrag);
  631. setDragCol.ingrab = 0;
  632. };
  633. }
  634. }
  635.  
  636. function cbOnce(cb) { // callbackを初回に呼ばれたときの1回だけ実行し2度め以降はしない ※要ルートブロックに設置
  637. let cbstr = cb.toString();
  638. //console.log(cbOnce.done , cbstr , cbOnce?.done?.has(cbstr))
  639. if (cbOnce?.done?.has(cbstr)) return;
  640. cbOnce.done = (cbOnce.done || new Set()).add(cbstr)
  641. //console.log(cbOnce.done)
  642. cb()
  643. }
  644.  
  645. })()