История изменений тайтла

Просмотр истории действий с отдельным тайтлом.

// ==UserScript==
// @name         История изменений тайтла
// @namespace    http://tampermonkey.net/
// @version      1.10
// @description  Просмотр истории действий с отдельным тайтлом.
// @author       grin3671
// @license      MIT
// @match        https://shikimori.one/*
// @match        https://shikimori.org/*
// @match        https://shikimori.me/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=shikimori.me
// @grant        none
// ==/UserScript==

(function() {
  "use strict";

  function checkPage () {
    let currentURL = location.pathname.substring(1).split("/");
    let isInPage = (node) => (node === document.body) ? false : document.body.contains(node);

    switch (currentURL[0]) {
      case "animes":
      case "mangas":
      case "ranobe":
        // remove old
        if (document.getElementById("btn-title-history")) document.getElementById("btn-title-history").remove();
        if (document.getElementById("modal-title-history")) document.getElementById("modal-title-history").remove();

        if (isInPage(document.querySelector(".b-db_entry .b-user_rate form")) && isTitleInUserList()) insertButton();
        break;
    }
  }

  // Adds Button to Title's Page
  function insertButton () {
    let parent = document.querySelector(".b-db_entry .b-user_rate").parentElement;
    let el = createElement("div", { "id": "btn-title-history", "class": "b-link_button mt-2 mb-2", "text": "История изменений" }, (e) => { e.onclick = () => { openModal() } });
    parent.append(el);
  }

  /**
   * Gets User Data
   * @property param (string) requested Property
   * @return (string) Property from User Data
   * @return (boolean) false if Property not found
   */
  const getUserData = (param) => param ? JSON.parse(document.body.getAttribute("data-user"))[param] || false : false;

  /**
   * Gets Title's ID
   * @return (string) Title ID
   * @return (boolean) false if Title ID can't be found
   */
  function getTitleID () {
    let el = document.querySelector(".b-db_entry .b-user_rate");
    if (!el) return false; // for checkPage function
    // data-entry="{"id":3649}"
    let data = JSON.parse(el.getAttribute("data-entry"));
    return data.id || false;
  }

  /**
   * Gets Title's type
   * @return (string) Title type for API (Anime || Manga)
   * @return (boolean) false if Title type is undefined
   */
  function getTitleType () {
    let currentURL = location.pathname.substring(1).split("/");
    return (currentURL[0] === "animes") ? "Anime" : (currentURL[0] === "mangas" || currentURL[0] === "ranobe") ? "Manga" : false;
  }

  function isTitleInUserList () {
    let e = document.querySelector(".b-db_entry .b-user_rate form[action*='/api/v2/user_rates']");
    e = e ? e.getAttribute("data-method") : false;
    return e == "PATCH" ? true : false;
  }

  /**
   * Utility. Creates Element with custom properties
   * @property type (string) element type
   * @property data (object) custom properties (id, class, text, style)
   * @property callback (function) callback with element itself as func prop
   * @return element
   */
  function createElement (type, data, callback) {
    let e = document.createElement(type || "div");

    if (data && data.id) e.id = data.id;
    if (data && data.class) e.classList = data.class;
    if (data && data.text) e.textContent = data.text;
    if (data && data.style) e.setAttribute("style", data.style);

    if (typeof callback === "function") callback(e);

    return e;
  }

  /**
   * Creates Modal for Title's History
   * @return element
   */
  function createModal () {
    let modal = createElement("div", { "id": "modal-title-history", "class": "b-modal hidden" });
    let modal_inner = createElement("div", { "class": "inner", "style": "top: 0" });
    let modal_title = createElement("div", { "class": "subheadline m5", "text": "История изменений" });
    let modal_block = createElement("div", { "id": "title-history-block", "class": "use-scroll pt-2 pb-2 mb-4", "style": "min-height: 150px; max-height: 500px; overflow: hidden auto; resize: vertical;" });
    let modal_close = createElement("div", { "class": "b-button", "text": "Закрыть" }, (e) => { e.onclick = () => { closeModal() } });
    let modal_shade = createElement("div", { "class": "b-shade", "style": "display: block; z-index: 1000;" }, (e) => { e.onclick = () => { closeModal() } });
    modal_inner.append(modal_title, modal_block, modal_close);
    modal.append(modal_shade, modal_inner);

    insertHistory(modal_block);

    return modal;
  }

  function openModal () {
    let modal = document.getElementById("modal-title-history") || document.body.appendChild(createModal());
    modal.classList.remove("hidden");
  }

  function closeModal () {
    let modal = document.getElementById("modal-title-history") || document.body.appendChild(createModal());
    modal.classList.add("hidden");
  }

  /**
   * Fills block with History Data
   * @property block (element) Node where the History will be filled
   */
  function insertHistory (block) {
    // reset old data // TODO: check if old data even possible
    block.innerHTML = "";

    // get user & title data
    let user_id = getUserData("id");
    let title_id = getTitleID();
    let title_type = getTitleType();

    // if smth goes wrong insert error message
    if (!user_id || !title_id || !title_type) {
      block.append(createElement("div", { "class": "b-nothing_here d-flex justify-content-center p-4", "text": "Не удалось собрать данные. Попробуйте позже." }));
      return false;
    }

    // main part
    block.classList.add("b-ajax");
    getHistoryData(user_id, title_id, title_type) // test: getHistoryData(324961, 21, "Anime")
      .then((data) => {
        block.classList.remove("b-ajax");
        if (data.length == 0) {
          block.append(createElement("div", { "class": "b-nothing_here d-flex justify-content-center p-4", "text": "История изменений тайтла отсутствует." }));
        } else {
          let historyTbody;
          block.append(createElement("table", { "class": "b-table b-editable_grid block2" }, e => { historyTbody = e.appendChild(createElement("tbody")); }));
          fillHistoryBlocks(data).forEach(entry => historyTbody.append(entry));
        }
      })
  }

  /**
   * Gets JSON History in Promise
   * @property user_id (integer) User ID
   * @property title_id (integer) Title ID
   * @property title_type (string) Title Type ["Anime" || "Manga"]
   * @return historyData (array) JSON History data
   */
  async function getHistoryData (user_id, title_id, title_type) {
    const RPS = 5;
    let requestIndex = 0; // works as a "page" param in request
    let historyLength = 100;
    let historyData = [];

    while (historyLength >= 100) {
      if (requestIndex >= RPS) break; // too many requests
      requestIndex++;
      let response = await requestHistory(user_id, title_id, title_type, requestIndex);
      // TODO: check response status
      let data = await response.json();
      historyLength = data.length || 0;
      historyData = historyData.concat(data || []);
    }

    return historyData;
  }

  async function requestHistory (user_id, title_id, title_type, page) {
    return await fetch(location.origin + "/api/users/" + user_id + "/history?target_id=" + title_id + "&target_type=" + title_type + "&limit=100&page=" + page);
  }

  /**
   * Fills Table Row from API History
   * @property history (array) API formatted data
   * @return historyBlocks (array) Array with Nodes
   */
  function fillHistoryBlocks (history) {
    let historyBlocks = [];

    history.forEach((entry, index) => {
      let row = createElement("tr", { "class": "b_history-row" }, (e) => {
        e.append(
          createElement("td", { "text": (index + 1).toString() }, e => { e.style.width = "5%" }),
          createElement("td", null, e => { e.innerHTML = entry.description }),
          createElement("td", { "text": new Intl.DateTimeFormat([], { dateStyle: "long", timeStyle: "medium"}).format(Date.parse(entry.created_at)) }, e => { e.style.width = "35%" }),
          createElement("td", null, e => {
            e.style.width = "10%";
            e.append(
              createElement("a", null, e => {
                e.textContent = "Удалить";
                e.href = getUserData("url") + "/history/" + entry.id;
                e.dataset.confirm = "Это действие необратимо. Точно?";
                e.dataset.method = "delete";
                e.dataset.remote = true;
                window.$(e).on("ajax:success", (e) => e.currentTarget.parentNode.parentNode.remove() );
              })
            )
          })
        )
      });
      historyBlocks.push(row);
    });

    return historyBlocks;
  }

  checkPage();

  document.addEventListener("page:load", checkPage);
  document.addEventListener("turbolinks:load", checkPage);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址