fujimasa / girls battle strength scouter

// ==UserScript==
// @name         girls battle strength scouter
// @namespace    https://openuserjs.org/users/fujimasa
// @version      0.6.6
// @description  show girls' popularity
// @author       fujimasa
// @match        https://www.cityheaven.net/*
// @run-at       document-end
// @grant        unsafeWindow
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_openInTab
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_getResourceURL

// require      https://cdnjs.cloudflare.com/ajax/libs/cash/8.1.0/cash.min.js
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
// @require      https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
// @resource     jqueryui https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css
// @resource     spinner4 https://raw.githubusercontent.com/fujima3/4650a76e-fa4d-449a-9b9a-cc3b1eb0aac8/develop/style/spinner4.css?ver=9

// @copyright    2021, fujimasa (https://openuserjs.org/users/fujimasa)
// @license      MIT
// @updateURL    https://openuserjs.org/meta/fujimasa/girls_battle_strength_scouter.meta.js
// @downloadURL  https://openuserjs.org/install/fujimasa/girls_battle_strength_scouter.user.js
// ==/UserScript==

const $ = window.jQuery;

$(function () {
  'use strict'
  // Your code here...

  function addHeaderResource(resource) {
    $("<link>", {
        href: GM_getResourceURL(resource),
        rel: 'stylesheet',
        type: 'text/css'
      })
      .appendTo(document.head)
  }

  function initializeDialog() {
    $("<div/>", {
        id: "dialog",
        style: "text-align:left",
        title: "May I open girls tab ?"
      })
      .appendTo("body")
  }

  // enable jquery-ui
  addHeaderResource("jqueryui")
  addHeaderResource("orbitron")
  addHeaderResource("spinner4")

  initializeDialog()

  const PageType = {
    LIST: 1,
    GIRL: 2,
    NULL: -99
  }
  const UNDEFINED = -1
  const NULL = -2

  // commonData = {baseUrl, pageType, shopId}
  const commonData = extractCommonData()

  function cleanByShopId() {
    GM_listValues().forEach(k => {
      if (k.startsWith(commonData.shopId)) GM_deleteValue(k)
    })
    GM_deleteValue("undefined")
    console.debug("cleaned.")
  }

  /**
   *
   * @return object {baseUrl:String, pageType:PageType, shopId:String}
   */
  function extractCommonData() {
    const data = {
      baseUrl: undefined,
      pageType: PageType.NULL,
      shopId: undefined
    }

    const [all, domain, region, group, detail, shop, page] = window.location.href.match(/(https:\/\/[^\/]+)\/([^\/]+)\/([^\/]+)\/([^\/]+)\/([^\/]+)\/([^\/]*)/);
    console.debug([all, domain, region, group, detail, shop, page])

    data.baseUrl = domain + "/" + region + "/" + group + "/" + detail + "/" + shop

    switch (true) {
      case /girllist/.test(page):
        data.pageType = PageType.LIST
        break
      case /girlid-.+/.test(page):
        data.pageType = PageType.GIRL
        break
      default:
        // nop
        console.dir({
          "page": page
        })
    }

    data.shopId = unsafeWindow.dataLayer.filter(el => el.shop_id !== undefined)[0].shop_id
    return data
  }

  switch (commonData.pageType) {
    case PageType.LIST:
      commonCommand()
      listCommand()
      break
    case PageType.GIRL:
      commonCommand()
      // girlCommand()
      break
    case PageType.NULL:
      commonCommand()
      break
  }

  /**
   * save , restore girls' fav count
   * @constructor
   */
  function FavRepository() {

    this.prototype = {

      /**
       * save fav count by constructor parameters.
       * @param shopId
       * @param girlId
       * @param name
       * @param fav
       */
      save: function (shopId, girlId, name, fav) {
        const key = [shopId, girlId, name].join(".")

        console.dir(key)
        console.debug("key: " + key + " fav: " + fav)
        GM_setValue(key, fav)
      },

      /**
       * returns fav count by constructor parameters.
       * @returns fav count
       */
      findFav: function (shopId, girlId, name) {
        const key = [shopId, girlId, name].join(".")

        let v = GM_getValue(key)
        if (v === undefined) return UNDEFINED
        return v
      },

      /**
       * list by shopId .<br/>
       * name = shopId.girlId.girlName
       * @returns [{name, fav}]
       */
      listByShopId: function (shopId) {
        const names = GM_listValues()
        const dict = []
        names.forEach(name => {
          if (name.startsWith(shopId)) {
            dict.push({
              "name": name,
              "fav": Number(GM_getValue(name))
            })
          }
        })
        return dict
      },

      /**
       * list by shopId those sort fav ascending
       * @returns [{name, fav}]
       */
      listByShopIdAsc: function (shopId) {
        const d = this.listByShopId(shopId)

        d.sort(function (a, b) {
          if (a.fav < b.fav) return 1
          if (a.fav > b.fav) return -1
          return 0
        })
        return d
      },

      /**
       * list by shopId those sort fav descending
       * @returns [{name, fav}]
       */
      listByShopIdDesc: function (shopId) {
        const d = this.listByShopId(shopId)

        d.sort((a, b) => {
          if (a.fav < b.fav) return -1
          if (a.fav > b.fav) return 1
          return 0
        })
        return d
      }
    }
  }

  function commonCommand() {
    GM_registerMenuCommand("parse girls", () => {
      window.location.href = commonData.baseUrl + "/girllist/"
      console.debug("moved.")
    }, "")
  }

  function listCommand() {
    registerMenu()
    const refs = extractDomElements()
    getGirlsFav(refs)

    function registerMenu() {
      GM_registerMenuCommand("clean shop data", cleanByShopId, "")

      // open strongest members
      GM_registerMenuCommand("open strongest", () => {
        $('#dialog p').remove()
        const range = [1, 2, 3, 4, 5]
        const dict = new FavRepository().prototype.listByShopIdAsc(commonData.shopId)
        const tops = []

        Array.from(range, (v, k) => {
          const id = dict[k].name.split(".")[1]
          const name = dict[k].name.split(".")[2]
          tops.unshift(id)
          $("#dialog").append($("<p/>").text(id + ":" + name + " = " + dict[k].fav))
        })

        $("#dialog").dialog({
          height: "auto",
          modal: false,
          position: {
            my: "center",
            at: "center",
            of: document
          },
          buttons: {
            "OK": () => {
              tops.forEach(_gid => GM_openInTab(commonData.baseUrl + "/girlid-" + _gid + "/", "insert"))
              $(this).dialog("close");
            }
          }
        });
      }, "")

      // open weakest members
      GM_registerMenuCommand("open weakest", () => {
        $('#dialog p').remove()
        const range = [1, 2, 3, 4, 5]
        const dict = new FavRepository().prototype.listByShopIdDesc(commonData.shopId)
        const bottoms = []

        Array.from(range, (v, k) => {
          const id = dict[k].name.split(".")[1]
          const name = dict[k].name.split(".")[2]

          bottoms.unshift(id)
          $("#dialog").append($("<p/>").text(id + ":" + name + " = " + dict[k].fav))
        })

        $("#dialog").dialog({
          height: "auto",
          modal: false,
          position: {
            my: "center",
            at: "center",
            of: document
          },
          buttons: {
            "OK": () => {
              bottoms.forEach(_gid => GM_openInTab(commonData.baseUrl + "/girlid-" + _gid + "/", "insert"))
              $(this).dialog("close")
            }
          }
        })

      }, "")
    }

    /**
     *
     * @returns {{outs: [], hrefs: []}}
     */
    function extractDomElements() {
      const parseTarget = []
      parseTarget.push({
        "link": "li > div.girllistimg a:nth-child(1)",
        "output": "li > div.girllistimg"
      })
      parseTarget.push({
        "link": "li > div.girl_img > a:nth-child(1)",
        "output": "li > div.girl_img"
      })

      const refs = {
        "hrefs": [],
        "outs": []
      }

      parseTarget.forEach(elm => {
        if (refs.hrefs.length === 0) {
          refs.hrefs = $(elm.link)
          refs.outs = $(elm.output)
        }
      })
      return refs
    }

    function initGirlsFav(outputElement) {
      outputElement.prepend($('<div/>', {
        class: "loader4",
        text: ""
      }))
    }

    function putGirlsFav(outputElement, cnt) {
      $(".loader4").remove()
      outputElement.prepend(cnt)
    }

    function getGirlsFav(ref) {
      ref.hrefs.each((idx, linkUrl) => {
        const girlData = linkUrl.href.match(/girlid-([0-9]+)/)
        if (girlData === undefined) {
          console.warn("[" + idx + "] undefined girls' data: " + linkUrl)
          return
        }

        const shopId = commonData.shopId
        const girlId = girlData[1]
        const girlName = linkUrl.children[0].alt // TODO: It's not always exist img under anchor.

        initGirlsFav($(refs.outs[idx]))

        // sync
        let cnt = new FavRepository().prototype.findFav(shopId, girlId, girlName)
        if (cnt === UNDEFINED) {

          $.ajax({
            type: "GET",
            url: "https://www.cityheaven.net/api/myheaven/v1/getgirlfavcnt/",
            cache: false,
            async: true,
            data: {
              "commu_id": shopId,
              "girl_id": girlId
            }
          }).then(
            response => {
              // async
              const c = Number(response.cnt)
              if (c !== undefined && c > 0) new FavRepository().prototype.save(shopId, girlId, girlName, c)
              putGirlsFav($(refs.outs[idx]), c)
            },
            error => {
              console.error(error)
            }
          )

        }
        else {
          putGirlsFav($(refs.outs[idx]), cnt)
        }

      })
    }
  }

  // --debug
  //console.dir(dict)
})