Greasy Fork

Greasy Fork is available in English.

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

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

当前为 2022-07-31 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Web漫画アンテナお気に入り管理
// @description d:作者名を読み込む a:作者をお気に入りに追加/削除 e:検索ワード入力 Shift+E:全編集
// @match       *://webcomics.jp/*
// @version     0.1
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @namespace   http://greasyfork.icu/users/181558
// @require     https://code.jquery.com/jquery-3.4.1.min.js
// ==/UserScript==

(function() {
  var keyFunc = [];
  var INTERVAL = function() { return 5000 };
  const V = 0; // 1:verbose
  var db = {};
  db.manga = pref("db.manga") || []
  db.favo = pref('db.favo') || [];
  var latestget = 0
  var busy = 0;

  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>`)

  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フラグ不可
  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 && 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 && 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 && 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[@class="entry-title"]/a[1]', box)?.textContent?.trim()
        var desc = eleget0('//div/a[@class="entry-comment" and text()="詳細・コメント"]', 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) {
          busy = 1;
          box.dataset.wait = 1
          v.stop = 1

          var quee = eleget0('.autq', box)
          quee.style.color = "#f0f"

          V && notify(Date.now() - latestget, "get:" + tit)
          latestget = Date.now()

          $.get(descurl).done(got => {
            busy = 0
            latestget = Date.now()
            delete box.dataset.que;
            aut = $('div.comic-info-right div.comic-author', got)?.text()?.replace("作者: ", "").trim() || "-"
            V && notify(aut, "done:" + tit)
            if (aut) {
              storemanga(tit, aut, box.parentNode)
              if (key == "a") addaut(aut, box.parentNode)
            }
          })
          autsearch()
        }
      })
      this.q = this.q.filter(v => !v.stop)
    },
  }
  setInterval(() => { que.do() }, 200)

  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]
      var target = (window.getSelection() && window.getSelection().toString().trim()) || (prompt(`お気に入りに登録するキーワードを入力してください\nすでに登録されている文字列を入力するとそれを削除します\n\n現在登録済み(${favo.length}):\n${((Math.random()>0.5?favo:favo.sort(new Intl.Collator("ja", {numeric: true, sensitivity: 'base'} ).compare ) ).join(" ") ) }\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.match(/^\+|^+/)) {
            tmp = tmp.replace(/^\+|^+/, "")
            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(evt.target); }, 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", `<div class="autsearchele ignoreMe"><a id="auta" href="${u}" title='左クリック:このキーワードを検索\n右クリック:開かずにキーワードを変更' style="font-size:12px; ">${aut.autrep()}</a> を検索 <span title="左クリック/e:お気に入りワードを追加\n右クリック/Shift+E:全設定値を編集" style="cursor:pointer;" data-key="e" data-keyr="Shift+E">&#128458;</span></div>`)?.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", `<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]
    }
  }

  function run(node = document) { // run::
    autsearch()
    elegeta('.autele', node).forEach(v => v.remove())

    // 一覧画面
    elegeta('.entry', node).forEach(v => {
      var title = eleget0('//div[@class="entry-title"]/a[1]', v)?.textContent?.trim()
      var aut = db.manga.find(v => v.t === title)?.a
      if (aut == "-") {
        adja(eleget0('//div[@class="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}</span>`)
      } 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('//div[@class="entry-date"]', v), "beforeend", `<span data-author="${encodeURI(aut)}" class="autele 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()}</a>`)
      } else {
        adja(eleget0('//div[@class="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>`)
      }
    })

    // 詳細画面
    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", `<span data-author="${encodeURI(aut)}" class="autele aut" style="float:right; font-size:12px; margin:0 0 0 1em; color:#444;">${aut}</span>`)
      } 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", `<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()}</a></span>`)
      }
    }

  }

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