Greasy Fork

来自缓存

Greasy Fork is available in English.

bilibili 本地评论存储插件

存储用户于视频/专栏/动态下的评论到本地。其用途是保存评论区令人感动的瞬间,以方便日后查看,研究和学习。本脚本魔改自bilibili 枝网查重 API 版,并遵循其AGPL-3.0 License。在这里对其作者表示敬意,salute😎

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         bilibili 本地评论存储插件
// @namespace    https://github.com/FakeServerBot/userscript
// @supportURL   https://github.com/FakeServerBot/userscript/issues
// @version      0.4
// @description  存储用户于视频/专栏/动态下的评论到本地。其用途是保存评论区令人感动的瞬间,以方便日后查看,研究和学习。本脚本魔改自bilibili 枝网查重 API 版,并遵循其AGPL-3.0 License。在这里对其作者表示敬意,salute😎
// @author       Sparanoid,FakeServerBot
// @license      AGPL
// @compatible   chrome 80 or later
// @match        https://*.bilibili.com/*
// @icon         https://emoji.beeimg.com/🎯/mozilla
// @require http://greasyfork.icu/scripts/420061-super-gm-setvalue-and-gm-getvalue-greasyfork-mirror-js/code/Super_GM_setValue_and_GM_getValue_greasyfork_mirrorjs.js?version=890160
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @run-at       document-start
// ==/UserScript==

window.addEventListener('load', () => {
  let front_label_list = ['可爱型', '见多识广型', '真情实感型', '科普型', '技术型', '狗头型', '小作文型'];
  let end_label_list = ['B站用户', '歪嘴龙王', '话唠', '大佬', '萌新', '宅男', '宅女', '现充', '狗头', '酷盖', 'DD'];
  const DEBUG = true;
  const NAMESPACE = 'bilibili-local-marker';
  var show_details_dict = {}; // Use this to decide when to hide past comments
  console.log(`${NAMESPACE} loaded`);
  // remove_all();
  function debug(description = '', msg = '', force = false) {
    if (DEBUG || force) {
      console.log(`${NAMESPACE}: ${description}`, msg)
    }
  }

  function formatDate(timestamp) {
    let date = timestamp.toString().length === 10 ? new Date(+timestamp * 1000) : new Date(+timestamp);
    return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
  }

  function rateColor(percent) {
    return `hsl(${226 + parseInt(percent/100* (360 - 226))}, 100%, 50%)`;
  }

  function set_value(uid, string, timestamp, comment_link) {
    // first, see if you can get this value
    var uid_values = get_value(uid);
    // then, append new string to this dict...
    uid_values.push([timestamp, string, comment_link]);
    // Set the new values...
    GM_SuperValue.set(uid, uid_values);
  }

  function is_marked(uid){
      let result = get_value(uid);
      if (result.length === 0){
          return false;
      }else{
          return true;
      }
  }

  function get_value(uid){
    return GM_SuperValue.get (uid, []);
  }

  function downloadTextFile(text, name) {
      const a = document.createElement('a');
      const type = name.split(".").pop();
      a.href = URL.createObjectURL( new Blob([text], { type:`text/${type === "txt" ? "plain" : type}` }) );
      a.download = name;
      a.click();
  }

  function format_as_json(){
      // get all uid
      let arrayOfKeys = GM_listValues();
      // empty dict
      var res = {}
      for (let key of arrayOfKeys){
          let tmp_res = get_value(key);
          if (tmp_res.length !== 0){
              res[key] = tmp_res;
          }
      }
      let json_res = JSON.stringify(res);
      downloadTextFile(json_res, `marker_file.json`);
      return res;
  }

  function load_json(input){
      var count = 0;
      for (const [key, value] of Object.entries(input)) {
          if (get_value(key).length === 0){
            // console.log(value);
            GM_SuperValue.set(key, value);
            count += 1;
          }
          // console.log(key, value);
      }
      return count;
  }
    function remove_all(){
      // get all uid
      let arrayOfKeys = GM_listValues();
      for (let key of arrayOfKeys){
          remove_all_values_of_target_uid(key);
      }
    }

  function remove_all_values_of_target_uid(uid){
      GM_SuperValue.set(uid, []);
  }

  function change_to_mark_status(LocalMarkerEl, uid){
      var count = get_value(uid).length;
      if (count >= 10) {
          count = 10;
          LocalMarkerEl.innerHTML = `10+ 已标记👀`;
      } else {
          LocalMarkerEl.innerHTML = `${count} 已标记👀`;
      }
      LocalMarkerEl.style.color = rateColor(count * 10);
  }

  function cancel_mark_status(LocalMarkerEl){
      LocalMarkerEl.innerHTML = '标记👀';
      LocalMarkerEl.style.color = '#99A2AA';
  }

  function get_show_details_dict_value(id){
      if (show_details_dict.hasOwnProperty(id)){
          return show_details_dict[id];
      } else {
          show_details_dict[id] = true;
          return show_details_dict[id];
      }
  }
  function remove_https_and_split(url){
      return url.replace(/^https?\:\/\//i, "").split('/');
  }
  function handle_target_link(comment_id, uid_card){
      // first, determine where are we..
      let url = window.location.href;
      let split_list = remove_https_and_split(url);
      if (split_list[0] === 'www.bilibili.com' && split_list[1] === 'video'){
          // at video
          var split_val = split_list[2];
          // remove ?
          split_val = split_val.split('?')[0];
          // remove %
          split_val = split_val.split('%')[0];
          return `https://www.bilibili.com/video/${split_val}/#reply${comment_id}`;
      } else if (split_list[0] === 'www.bilibili.com' && split_list[1] === 'read'){
          // at read
          var split_val_0 = split_list[2];
          // remove ?
          split_val_0 = split_val_0.split('?')[0];
          // remove %
          split_val_0 = split_val_0.split('%')[0];
          return `https://www.bilibili.com/read/${split_val_0}/#reply${comment_id}`;
      } else if (split_list[0] === 'space.bilibili.com'){
          // at dynamic
          let dynamic_id = uid_card.closest(".card").getAttribute('data-did');
          return `https://t.bilibili.com/${dynamic_id}/#reply${comment_id}`;
      } else if (split_list[0] === 't.bilibili.com'){
          // at t.bilibili...
          var split_val_2 = split_list[1];
          // remove ?
          split_val_2 = split_val_2.split('?')[0];
          // remove %
          split_val_2 = split_val_2.split('%')[0];
          return `https://t.bilibili.com/${split_val_2}/#reply${comment_id}`;
      } else {
          // unknown position...
          return '不支持提供链接😨';
      }
  }

  function attachEl(item) {
    let injectWrap = item.querySelector('.con .info');

    // .text - comment content
    // .text-con - reply content
    let content = item.querySelector('.con .text') || item.querySelector('.reply-con .text-con');
    let id = item.dataset.id;
    // save user uid
    let uid_card = item.querySelector('.con .name') || item.querySelector('.reply-con .name');
    // debug('current_page', remove_https_and_split(window.location.href));
    let uid = uid_card.getAttribute('data-usercard-mid')
    let comment_link = handle_target_link(id, uid_card);
    // console.log(uid);
    // Simple way to attach element on replies initially loaded with comment
    // which wouldn't trigger mutation inside observeComments
    let replies = item.querySelectorAll('.con .reply-box .reply-item');
    if (replies.length > 0) {
      [...replies].map(reply => {
        attachEl(reply);
      });
    }

    if (injectWrap.querySelector('.LocalMarker')) {
      debug('already loaded for this comment');
    } else {
        // Insert LocalMarker check button
        let LocalMarkerEl = document.createElement('span');
        LocalMarkerEl.style.userSelect = 'none';
        LocalMarkerEl.classList.add('LocalMarker', 'btn-hover', 'btn-highlight');
        if (is_marked(uid) === true){
            change_to_mark_status(LocalMarkerEl, uid);
        } else {
            cancel_mark_status(LocalMarkerEl);
        }
        LocalMarkerEl.addEventListener('click', e => {
          let contentPrepared = '';
          // Copy meme icons alt text
          for (let node of content.childNodes.values()) {
              if (node.nodeType === 3) {
                  contentPrepared += node.textContent;
              } else if (node.nodeName === 'IMG' && node.nodeType === 1) {
                  contentPrepared += node.alt;
              } else if (node.nodeName === 'BR' && node.nodeType === 1) {
                  contentPrepared += '\n';
              } else if (node.nodeName === 'A' && node.nodeType === 1 && node.classList.contains('comment-jump-url')) {
                  contentPrepared += node.href.replace(/https?:\/\/www\.bilibili\.com\/video\//, '');
              } else {
                  contentPrepared += node.innerText;
              }
            }
          // Need regex to stripe `回复 @username  :`
          let contentProcessed = contentPrepared.replace(/回复 @.*:/, '');
          debug('content processed', contentProcessed);
          // debug('dynamic_id', dynamic_id);
          // remove_all_values_of_target_uid(uid);
          set_value(uid, contentProcessed, Date.now(), comment_link);
          change_to_mark_status(LocalMarkerEl, uid);
          if (injectWrap.querySelector('.LocalMarker-result')) {
               injectWrap.querySelector('.LocalMarker-result').remove();
           }
           if (!injectWrap.querySelector('.LocalMarker-marker-label')) {
             add_chunk(uid, injectWrap);
           }
           show_details_dict[id] = true;
      }, false);

      injectWrap.append(LocalMarkerEl);

      let show_message_button = document.createElement('span');
      show_message_button.classList.add('LocalMarker', 'btn-hover', 'btn-highlight');
      show_message_button.innerHTML = '历史评论';
      show_message_button.style.userSelect = 'none';
      show_message_button.addEventListener('click', e => {
          if (get_show_details_dict_value(id)){
                show_details_dict[id] = false;
                let message_list = get_value(uid);
                // debug('get value:', message_list);
                let resultContent = ``;
                if (message_list.length === 0){
                    resultContent = `无标记历史!`;
                }
                for (const [index, sig_meg] of message_list.entries()){
                  // debug('sig_meg', sig_meg);
                    //${formatDate(sig_meg[0])}
                  resultContent += `<p><b style="color: #222222">[${index + 1}]</b> <span style="color: #222222">${sig_meg[1]}</span></p><p><a href="${sig_meg[2]}" target="_blank">原评论链接:${sig_meg[2]}</a></p><p>-- 标记于: <span style="color: #FB7299">${formatDate(sig_meg[0])}</span></p>`;
                  if (index < message_list.length-1){
                  resultContent += `<p class="" style="margin: 6px;"></p>`;
                  }
                }
                // Insert result
                let resultWrap = document.createElement('div');

                resultWrap.style.position = 'relative';
                resultWrap.style.padding = '.5rem';
                resultWrap.style.margin = '.5rem 0';
                resultWrap.style.background = 'hsla(0, 0%, 50%, .1)';
                resultWrap.style.borderRadius = '4px';
                // resultWrap.style.lineHeight = '10px';
                // resultWrap.style.whiteSpace = 'pre';
                resultWrap.style.wordBreak = 'break-word';
                resultWrap.style.width = '90%';
                resultWrap.classList.add('LocalMarker-result');
                resultWrap.innerHTML = resultContent;

                let download_button = document.createElement('span');
              download_button.classList.add('LocalMarker', 'btn-hover', 'btn-highlight');
              download_button.innerHTML = '下载记录';
              download_button.style.userSelect = 'none';
              download_button.setAttribute('title', '导出所有人的历史记录到本地');
              download_button.addEventListener('click', e => {
                  format_as_json();
              }, false);
              // resultWrap.append(download_button);

              let upload_input = document.createElement('input');
              upload_input.setAttribute('type', 'file');
              upload_input.style.color = 'transparent';
              upload_input.setAttribute('title', '从本地导入历史记录json文件');
              upload_input.addEventListener('change', function() {
                  var GetFile = new FileReader();
                  GetFile.onload=function(){
                      const json_obj = JSON.parse(GetFile.result);
                      // console.log(json_obj);
                      let load_counts = load_json(json_obj);
                      alert(`${load_counts}条额外的标记信息已成功加载`);
                  }
                  GetFile.readAsText(this.files[0]);
              });
              // resultWrap.append(upload_input);
                // Remove previous result if exists
                if (injectWrap.querySelector('.LocalMarker-result')) {
                  injectWrap.querySelector('.LocalMarker-result').remove();
                }
                injectWrap.append(resultWrap);
              } else {
                show_details_dict[id] = true;
                if (injectWrap.querySelector('.LocalMarker-result')) {
                    injectWrap.querySelector('.LocalMarker-result').remove();
                }
            }
      }, false);
      injectWrap.append(show_message_button);

      let remove_button = document.createElement('span');

      remove_button.classList.add('LocalMarker', 'btn-hover', 'btn-highlight');
      remove_button.innerHTML = '不再标记';
      remove_button.style.userSelect = 'none';
      remove_button.addEventListener('click', e => {
         remove_all_values_of_target_uid(uid);
          if (injectWrap.querySelector('.LocalMarker-result')) {
             injectWrap.querySelector('.LocalMarker-result').remove();
         }
         if (injectWrap.querySelector('.LocalMarker-marker-label')) {
             injectWrap.querySelector('.LocalMarker-marker-label').remove();
         }
         show_details_dict[id] = true;
         cancel_mark_status(LocalMarkerEl);
      }, false);
      injectWrap.append(remove_button);
      if (is_marked(uid) === true){
          add_chunk(uid, injectWrap);
      }
    }
  }
  function add_chunk(uid, injectWrap){
      let out_div = document.createElement('span');
      out_div.classList.add('LocalMarker-marker-label');
      out_div.style.userSelect = 'none';
      out_div.style.disabled = true;
      var select_front_list = add_default_list(uid + '_front', front_label_list);
      out_div.append(select_front_list);
      var select_end_list = add_default_list(uid + '_end', end_label_list);
      out_div.append(select_end_list);
      injectWrap.append(out_div);
  }
  function add_default_list(list_id, array){
      //Create and append select list
      var selectList = document.createElement("select");
      selectList.id = "mySelect";
      // Set the new values...
      let default_value = GM_SuperValue.get(list_id, array[0]);
      //Create and append the options
      for (var i = 0; i < array.length; i++) {
          var option = document.createElement("option");
          option.value = array[i];
          option.text = array[i];
          if (default_value === option.text){
              option.selected = "selected";
          }
          selectList.appendChild(option);
      }
      selectList.style.userSelect = 'none';
      selectList.addEventListener("change", function() {
          GM_SuperValue.set(list_id, selectList.value);
      });
      return selectList;
  }
  function add_selective_list(list_name, injectWrap) {
      if (get_value(list_name).length === 0){
          GM_SuperValue.set(list_name, [[], Date.now(), "this is the empty front list"]);
      }
      let array = get_value(list_name)[0];
      var input_chart = document.createElement("input");
      input_chart.setAttribute('type', 'text');
      input_chart.setAttribute('list', 'mySelect');
      //Create and append select list
      var selectList = document.createElement("datalist");
      selectList.id = "mySelect";
      //Create and append the options
      for (var i = 0; i < array.length; i++) {
          var option = document.createElement("option");
          option.value = array[i];
          option.text = array[i];
          if (0 === i){
              option.selected = "selected";
          }
          selectList.appendChild(option);
      }
      input_chart.addEventListener("change", function() {
          input_chart.style.width = `${1.3 * Math.max(input_chart.value.length, 4)}em`;
          array = get_value(list_name)[0];
          array.push(input_chart.value);
          // console.log(get_value(list_name));
          GM_SuperValue.set(list_name, [array, Date.now(), "this is front list"]);
          console.log(get_value(list_name));
      });
      injectWrap.append(input_chart);
      injectWrap.append(selectList);
      return selectList;
  }
  function observeComments(wrapper) {
    // .comment-list - general list for video, zhuanlan, and dongtai
    // .reply-box - replies attached to specific comment
    let commentLists = wrapper ? wrapper.querySelectorAll('.comment-list, .reply-box') : document.querySelectorAll('.comment-list, .reply-box');

    if (commentLists) {

      [...commentLists].map(commentList => {

        // Directly attach elements for pure static server side rendered comments
        // and replies list. Used by zhuanlan posts with reply hash in URL.
        // TODO: need a better solution
        [...commentList.querySelectorAll('.list-item, .reply-item')].map(item => {
          attachEl(item);
        });

        const observer = new MutationObserver((mutationsList, observer) => {

          for (const mutation of mutationsList) {

            if (mutation.type === 'childList') {

              // debug('observed mutations', [...mutation.addedNodes].length);

              [...mutation.addedNodes].map(item => {
                attachEl(item);

                // Check if the comment has replies
                // I check replies here to make sure I can disable subtree option for
                // MutationObserver to get better performance.
                let replies = item.querySelectorAll('.con .reply-box .reply-item');

                if (replies.length > 0) {
                  observeComments(item)
                  // debug(item.dataset.id + ' has rendered reply(ies)', replies.length);
                }
              })
            }
          }
        });
        observer.observe(commentList, { attributes: false, childList: true, subtree: false });
      });
    }
  }

  // .bb-comment loads directly for zhuanlan post. So load it directly
  observeComments();

  // .bb-comment loads dynamcially for dontai and videos. So observe it first
  const wrapperObserver = new MutationObserver((mutationsList, observer) => {

    for (const mutation of mutationsList) {

      if (mutation.type === 'childList') {

        [...mutation.addedNodes].map(item => {
          // debug('mutation wrapper added', item);

          if (item.classList?.contains('bb-comment')) {
            debug('mutation wrapper added (found target)', item);

            observeComments(item);

            // Stop observing
            // TODO: when observer stops it won't work for dynamic homepage ie. https://space.bilibili.com/703007996/dynamic
            // so disable it here. This may have some performance impact on low-end machines.
            // wrapperObserver.disconnect();
          }
        })
      }
    }
  });
  wrapperObserver.observe(document.body, { attributes: false, childList: true, subtree: true });

}, false);