shikiCommentsGraphLib

Lib для графа комментариев Shikimori

Version vom 16.10.2025. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/552715/1678737/shikiCommentsGraphLib.js

// ==UserScript==
// @name         shikiCommentsGraphLib
// @namespace    https://shikimori.one/
// @version      3.0
// @description  Lib для графа комментариев Shikimori
// @license      MIT
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// ==/UserScript==

; (function () {
  'use strict';

  // === Базовая математика (оставляем из оригинала) ===
  class ShikiMath {
    static is_above(x, y, x1, y1, x2, y2) {
      const dx = x2 - x1;
      const dy = y2 - y1;
      return ((((dy * x) - (dx * y)) + (dx * y1)) - (dy * x1)) <= 0;
    }
    static sector(x1, y1, x2, y2, rx, ry) {
      const lb_to_rt = this.is_above(x2, y2, x1 - rx, y1 - ry, x1, y1);
      const lt_to_rb = this.is_above(x2, y2, x1 - rx, y1 + ry, x1, y1);
      if (lb_to_rt && lt_to_rb) return 'top';
      if (!lb_to_rt && lt_to_rb) return 'right';
      if (!lb_to_rt && !lt_to_rb) return 'bottom';
      return 'left';
    }
    static square_cutted_line(x1, y1, x2, y2, rx1, ry1, rx2, ry2) {
      let f_x1, f_x2, f_y1, f_y2;
      const dx = x2 - x1, dy = y2 - y1;
      const y = x => (((dy * x) + (dx * y1)) - (dy * x1)) / dx;
      const x = y => (((dx * y) - (dx * y1)) + (dy * x1)) / dy;
      const target = this.sector(x1, y1, x2, y2, rx1, ry1);
      if (target === 'right') { f_x1 = x1 + rx1; f_y1 = y(f_x1); f_x2 = x2 - rx2; f_y2 = y(f_x2); }
      if (target === 'left') { f_x1 = x1 - rx1; f_y1 = y(f_x1); f_x2 = x2 + rx2; f_y2 = y(f_x2); }
      if (target === 'top') { f_y1 = y1 + ry1; f_x1 = x(f_y1); f_y2 = y2 - ry2; f_x2 = x(f_y2); }
      if (target === 'bottom') { f_y1 = y1 - ry1; f_x1 = x(f_y1); f_y2 = y2 + ry2; f_x2 = x(f_y2); }
      return { x1: f_x1, y1: f_y1, x2: f_x2, y2: f_y2 };
    }
  }

  // === Класс CommentNode ===
  const SELECT_SCALE = 1.5;
  const BORDER_OFFSET = 3;

  class CommentNode {
    constructor(data, size = 64) {
      Object.assign(this, data);
      this.width = this.height = size;
      this.rx = this.ry = size / 2;
      this.selected = false;
    }

    get d3Node() { return this._d3Node ||= d3.select(`.node#${this.id}`); }
    get d3Image() { return this._d3Image ||= this.d3Node.select('image'); }

    deselect(boundX, boundY, tick) {
      this.selected = false;
      this._animate(this.width, this.height, boundX, boundY, tick);
      this._hideTooltip();
    }

    select(boundX, boundY, tick) {
      this.selected = true;
      this._animate(this.width * SELECT_SCALE, this.height * SELECT_SCALE, boundX, boundY, tick);
      this._loadTooltip();
    }

    _animate(newW, newH, boundX, boundY, tick) {
      const iw = d3.interpolate(this.width, newW);
      const ih = d3.interpolate(this.height, newH);
      this.d3Node.transition().duration(300).tween('resize', () => t => {
        this.width = iw(t); this.height = ih(t);
        this.rx = this.width / 2; this.ry = this.height / 2;
        this.d3Node.attr('transform', `translate(${boundX(this) - this.rx}, ${boundY(this) - this.ry})`);
        this.d3Image.attr({ width: this.width, height: this.height });
        tick();
      });
    }

    async _loadTooltip() {
      try {
        const tooltip = document.querySelector('.sticky-tooltip');
        tooltip.style.display = 'block';
        tooltip.querySelector('.inner').innerHTML = '<i>Загрузка...</i>';

        const { data } = await axios.get(`https://shikimori.one/api/comments/${this.id}`);
        const html = data.html_body || '(нет данных)';
        tooltip.querySelector('.inner').innerHTML = html;
      } catch (e) {
        console.error('Tooltip error:', e);
      }
    }

    _hideTooltip() {
      const tooltip = document.querySelector('.sticky-tooltip');
      if (tooltip) tooltip.style.display = 'none';
    }
  }

  // === Граф комментариев ===
  class CommentsGraph extends window.FranchiseGraph {
    constructor(data) {
      super(data);
      this.image_w = this.image_h = 64;
      this.nodes_data = data.nodes.map(node => new CommentNode(node, 64));
    }

    _append_nodes() {
      this.d3_node = this.d3_svg.append('g').selectAll('.node')
        .data(this.nodes_data)
        .enter().append('g')
        .attr({ class: 'node', id: d => d.id })
        .call(this.d3_force.drag())
        .on('click', d => {
          if (d3.event?.defaultPrevented) return;
          this._node_selected(d);
        });

      this.d3_node.append('path').attr({ class: 'border_outer', d: '' });

      this.d3_image_container = this.d3_node.append('g').attr({ class: 'image-container' });
      this.d3_image_container.append('image')
        .attr({
          width: d => d.width,
          height: d => d.height,
          'xlink:href': d => d.avatar
        });
      this.d3_image_container.append('path')
        .attr({ class: 'border_inner', d: d => `M 0,0 ${d.width},0 ${d.width},${d.height} 0,${d.height} 0,0` });
    }
  }

  // === Экспорт ===
  window.ShikiMath = ShikiMath;
  window.CommentNode = CommentNode;
  window.CommentsGraph = CommentsGraph;
})();