orax / dim-mod

// ==UserScript==
// @name         dim-mod
// @namespace    dim-mod
// @version      1.3
// @description  -
// @author       orax
// @match        http*://app.destinyitemmanager.com/*/d2/inventory
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_getResourceText
// @require      https://code.jquery.com/jquery-3.4.1.min.js
// @require      https://openuserjs.org/src/libs/sizzle/GM_config.js
// @license      MIT
// ==/UserScript==

/*
https://github.com/mturco/context-menu
*/

(function () {
  'use strict';

  const ID_HEADER_DIV = '_dim-mod';

  let mode_filtreAjouter = false;
  let mode_filtreSupprimer = false;
  let headerDiv;
  let headerRight;
  let filterInput;
  let toggleButton;
  let cfg;

  GM_config.init({
    id: 'dim-mod-config', // The id used for this instance of GM_config
    // Fields object
    fields: {
      // This is the id of the field
      barreHaut: {
        label: 'Barre en haut', // Appears next to field
        type: 'textarea', // Makes this setting a text field
        cols: 140,
        rows: 25,
        // Default value if user doesn't change it
        default: `# CONFIGURATION PAR DÉFAUT

# perso actuel
  action = TR_PERSO_1

# filtre*
  action = EFFACE_FILTRE

# filtre+
  action = MODE_FILTRE_AJOUTER

# filtre−
  action = MODE_FILTRE_SUPPRIMER

# +classe
  action = AJOUTE_CLASSE_ACTIVE

# pve
  action = FILTRE wishlistnotes:pve8 or wishlistnotes:pve9 or wishlistnotes:pve10

# corrompus
  action = FILTRE holdsmod:outlaw

# déchus
  action = FILTRE holdsmod:forge

# ruche
  action = FILTRE modslot:opulent

# vex
  action = FILTRE holdsmod:undying

# armures PM
  action = FILTRE is:armor masterwork:>=9

# pas équipé
  action = FILTRE is:incurrentchar not:equipped is:haspower not:invault

# armes
  action = FILTRE is:weapon not:locked not:tagged or tag:junk not:maxpower not:exotic not:masterwork

# armures < 60
  action = FILTRE is:armor not:classitem not:locked not:tagged or tag:junk not:maxpower not:exotic not:white basestat:total:<60

# équip. classe
  action = FILTRE is:armor is:classitem not:locked not:tagged or tag:junk not:maxpower not:exotic not:masterwork energycapacity:<5

# bleus
  action = FILTRE is:blue is:haspower not:maxpower

# poubelle
  action = FILTRE is:armor not:tagged or tag:junk not:maxpower not:exotic not:masterwork modslot:none

# tag:junk
  action = FILTRE (tag:junk or is:blue) not:maxpower is:haspower

#nv
 action = FILTRE is:new not:maxpower is:haspower not:exotic not:locked

#nv armes
 action = FILTRE is:new is:weapon not:maxpower is:haspower not:exotic not:locked

# COFFRE
  action = TR_COFFRE

# PVE 7
 action = FILTRE wishlistnotes:pve7

# PVE 8
 action = FILTRE wishlistnotes:pve8

# PVE 9
 action = FILTRE wishlistnotes:pve9

# PVE 10
 action = FILTRE wishlistnotes:pve10

# PVE ≥7
 action = FILTRE wishlistnotes:pve10 or wishlistnotes:pve9 or wishlistnotes:pve8 or wishlistnotes:pve7`,
      },
    },
    // https://github.com/sizzlemctwizzle/GM_config/wiki/Event-Callbacks
    events: {
      close: start,
    },
  });

  // attend que DIM soit chargé et exécute l'initialisation
  start();

  function x(xpath) {
    return document.evaluate(
      xpath,
      document.body,
      null,
      XPathResult.FIRST_ORDERED_NODE_TYPE,
      null
    ).singleNodeValue;
  }

  async function waitXPathElementLoaded(xpath) {
    while (x(xpath) === null) {
      await new Promise((resolve) => requestAnimationFrame(resolve));
    }

    return x(xpath);
  }

  async function waitElementLoaded(selector) {
    while (document.querySelector(selector) === null) {
      await new Promise((resolve) => requestAnimationFrame(resolve));
    }

    return document.querySelector(selector);
  }

  function dim_clickOnCharacter(charNumber) {
    switch (charNumber) {
      case 1:
        $('div.character.current.destiny2').click();
        break;
      case 2:
        break;
      case 3:
        break;
    }
  }

  function dim_clickOnCharacterMenuItem(itemText) {
    dim_clickOnCharacter(1);
    $('span[title="' + itemText + '"]').click();
  }

  function dim_getCurrentCharacterClass() {
    return dim_normalizeClassName(
      $('div.character.current.destiny2 div.class').text()
    );
  }

  function dim_getCharacterClass(index) {
    let charClass = document.querySelectorAll(
      'div.character.destiny2 div.class'
    )[index].textContent;

    return dim_normalizeClassName(charClass);
  }

  function dim_addCharacterClassToFilter(characterClass) {
    let currClass = characterClass.toLowerCase();

    dim_addFilter('is:' + currClass);
  }

  function dim_clickOnRightMenu(item) {
    $('button#downshift-1-toggle-button').click(); // clique sur les trois points verticaux

    if (typeof item === 'number') {
      $('div#downshift-1-item-' + item).click(); // le premier index est 0
    }
    else if (typeof item === 'string') {
      $('div#downshift-1-menu div:contains(' + item + ')').click();
    }
  }

  function dim_activateFarming(charNumber) {
    dim_clickOnCharacter(charNumber);
    waitXPathElementLoaded('//span[text() = "Mode farming"]').then((e) => {
      e.click();
    });
  }

  function dim_setFilter(filter) {
    filterInput.value = filter + ' \n';
    filterInput.dispatchEvent(
      new Event('change', {
        bubbles: true,
      })
    );
    toggleButton.click();
  }

  function dim_addFilter(filter) {
    let newFilter = dim_getFilter();

    if (newFilter.length) {
      newFilter += ' ';
    }

    newFilter += filter;
    dim_setFilter(newFilter);
  }

  function dim_removeFilter(filter) {
    let newFilter = dim_getFilter().replace(filter, '');

    if (newFilter.match(/ +/g)) {
      dim_clearFilterInputbox();
    }
    else {
      dim_setFilter(newFilter);
    }
  }

  function dim_clearFilterInputbox() {
    let clearSearchFilterButton = document.querySelectorAll(
      'div.search-filter._2IHK9 button.filter-bar-button'
    )[1];

    if (clearSearchFilterButton !== undefined) {
      clearSearchFilterButton.click();
    }
  }

  function dim_getFilter() {
    return filterInput.value.trim();
  }

  function dim_normalizeClassName(className) {
    return className
      .replace('Arcaniste', 'Warlock')
      .replace('Chasseur', 'Hunter');
  }

  function normalizeNewline(str) {
    return str.replace(/\r\n|\r/g, '\n');
  }

  // réexécute le script s'il a été déchargé lors d'un changement de page par exemple
  function checkAndReloadMod() {
    // si le mod n'est plus chargé
    if (
      !document.getElementById(ID_HEADER_DIV) &&
      document.location.href.match('/inventory')
    ) {
      console.log('DIM mod : le script va être rechargé');
      start();
    }
    else {
      setTimeout(checkAndReloadMod, 5000);
    }
  }

  // supprime ce qui a été ajouté par ce script
  function clean() {
    let el = document.getElementById(ID_HEADER_DIV);

    if (el !== null) {
      el.remove();
    }
  }

  function setup_addInventoryAnchors(offset) {
    /*
    Commis des postes
    Armes
    Armures
    ...
    */
    $('div.store-row.inventory-title').each(function (index) {
      $(this).before($('<a id="_dimMod_inventory' + index + '"></a>'));
    });

    addStyle('html { scroll-padding-top:' + offset + 'px; }');
  }

  function setup_add_barreGauche() {
    let header = $('header#header');

    // https://codepen.io/chriscoyier/pen/NJJERg
    let offset =
      header.outerHeight() +
      $('div.store-row.store-header').outerHeight();

    let elems = $(`
            <nav id="_dimMod_barreGauche">
                <ul>
                    <li><a href="#"></a></li>
                    <li><a href="#_dimMod_inventory1"></a></li>
                    <li><a href="#_dimMod_inventory2"></a></li>
                    <li><a href="#_dimMod_inventory3"></a></li>
                    <li><a href="#_dimMod_inventory4"></a></li>
                </ul>
            </nav>`);

    addStyle(`
            nav#_dimMod_barreGauche {
                position: fixed;
                top: ${offset}px;
            }
            
            nav#_dimMod_barreGauche ul {
                width: 10px;
                list-style: none;
                margin: 0;
                padding: 0;
                height: 100px; 
            }
            
            nav#_dimMod_barreGauche li {
                height: 100%;
            }

            nav#_dimMod_barreGauche li a {
                display: block;
                height: 100%;
            }
            
            nav#_dimMod_barreGauche li:nth-child(odd) a {
                background-color: rgba(185, 255, 0, 0.2);
            }
            
            nav#_dimMod_barreGauche li:nth-child(even) a {
                background-color: rgba(40, 255, 0, 0.1);
            }
        `);

    header.append(elems);

    setup_addInventoryAnchors(offset);
  }

  function addStyle(css) {
    let doc = document;
    let elem = doc.createElement('style');
    let target =
      doc.getElementsByTagName('head')[0] ||
      doc.body ||
      doc.documentElement;
    elem.textContent = css;
    target.appendChild(elem);
  }

  function start() {
    console.log('Tentative de chargement de « DIM mod »');

    let el = document.querySelector('div.equipped-item');

    if (el) {
      clean();
      init();
    }
    else {
      setTimeout(start, 2500); // try again in ... milliseconds
    }
  }

  function setup() {
    headerDiv = $(
      '<div id="' +
      ID_HEADER_DIV +
      '" style="display: flex;flex-wrap: wrap;" class="BcBfoaH1"></div>'
    );
    headerRight = $('header span.search-button');
    filterInput = document.querySelector('input.filter-input'); // <input> recherche
    toggleButton = document.querySelector(
      'button#downshift-0-toggle-button'
    );
    // récupère la configuration créé par l'utilisateur
    cfg = {
      barreHaut: normalizeNewline(GM_config.get('barreHaut'))
        .replace(/\B\/\/.*/g, '') // supprime commentaires
        .replace(/^ +| +$/gm, ''),
    };
  }

  function init() {
    setup();

    console.log('DIM mod : init');

    $('header').append(headerDiv);

    // bouton « cfg »
    if (!document.getElementById('dim-mod-cfg')) {
      console.log('app');
      // debugger;
      headerRight.after(
        $('<button id="dim-mod-cfg">cfg</button>').click(function () {
          GM_config.open();
        })
      );
      // debugger;
    }

    // barre en haut
    // analyse la configuration et ajoute les boutons
    let sections = cfg.barreHaut.matchAll(/^#.+(?:\n(?:.+\n)+)/gm);

    for (const section of sections) {
      let btnName = section[0].match(/# *(.+)/)[1];
      let matches = section[0].matchAll(/(\w+) *= *(.*)/g);
      let btn = $('<button>' + btnName + '</button>');

      // m[1] : avant le « = »
      // m[2] : après le « = »
      for (const m of matches) {
        let key = m[1];

        if (key.match(/action/i)) {
          let params = m[2].match(/(\w+)(?: +(.+))?/);
          switch (params[1]) {
            case 'FILTRE':
              // évènement "input" pour notifier DIM du changement de la valeur de l'input
              btn.on('click', function () {
                console.log('FILTRE');

                if (mode_filtreAjouter) {
                  dim_addFilter(params[2]);
                }
                else if (mode_filtreSupprimer) {
                  dim_removeFilter(params[2]);
                }
                else {
                  dim_setFilter(params[2]);
                }
              });
              break;
            case 'CLIC_MENU': {
              btn.on('click', function () {
                console.log('CLIC_MENU : "' + params[2] + '"');
                dim_clickOnCharacterMenuItem(params[2]);
              });
              break;
            }
            case 'EFFACE_FILTRE': {
              btn.on('click', function () {
                console.log('EFFACE_FILTRE');
                dim_clearFilterInputbox();
              });
              break;
            }
            case 'AJOUTE_CLASSE_ACTIVE': {
              btn.on('click', function () {
                console.log('AJOUTE_CLASSE');
                dim_addCharacterClassToFilter(
                  dim_getCurrentCharacterClass()
                );
              });
              break;
            }
            case 'AJOUTE_CLASSE': {
              btn.on('click', function () {
                dim_addCharacterClassToFilter(
                  dim_getCharacterClass(params[2] - 1)
                );
              });
              break;
            }
            case 'MODE_FILTRE_AJOUTER': {
              btn.on('click', function () {
                console.log('MODE_FILTRE_AJOUTER');
                mode_filtreAjouter = !mode_filtreAjouter;

                if (mode_filtreAjouter) {
                  this.style = 'color: red';
                }
                else {
                  this.style = '';
                }

                console.log(mode_filtreAjouter);
              });
              break;
            }
            case 'MODE_FILTRE_SUPPRIMER': {
              btn.on('click', function () {
                console.log('MODE_FILTRE_SUPPRIMER');
                mode_filtreSupprimer = !mode_filtreSupprimer;

                if (mode_filtreSupprimer) {
                  this.style = 'color: red';
                }
                else {
                  this.style = '';
                }

                console.log(mode_filtreAjouter);
              });
              break;
            }
            case 'TR_COFFRE':
              btn.on('click', function () {
                console.log('TR_COFFRE');
                dim_clickOnRightMenu('Envoyer au Coffre');
              });
              break;
            case 'CLIC_MENU_DROITE':
              btn.on('click', function () {
                console.log('CLIC_MENU_DROITE ' + params[2]);
                dim_clickOnRightMenu(params[2]);
              });
              break;
            case 'FARM_PERSO_1': {
              btn.on('click', function () {
                dim_activateFarming(1);
              });
              break;
            }
            case 'TR_PERSO_1':
              btn.on('click', function () {
                console.log('TR_PERSO_1');
                dim_clickOnRightMenu(0);
              });
              break;
            case 'AFFICHE_CONFIG':
              btn.on('click', function () {
                console.log('AFFICHE_CONFIG');
                GM_config.open();
              });
              break;
          }
        }
        if (key.match(/css/i)) {
          btn[0].style.cssText = m[2];
        }
        headerDiv.append(btn);
      }
    }

    // barre à gauche
    setup_add_barreGauche();

    dim_activateFarming(1);
    setTimeout(checkAndReloadMod, 5000);
  }

  /*
      // attend que DIM soit chargé et exécute l'initialisation
      let observer = new MutationObserver(function (mutations, observer) {
          for (let i = 0; i < mutations.length; i++) {
              for (let j = 0; j < mutations[i].addedNodes.length; j++) {
                  if (mutations[i].addedNodes[j].matches('div#content')) {
                    //div#content div.inventory-content
                    //item-drag-container item-type-Kinetic
                      console.write("DIM mod");
                      observer.disconnect();
                      init();
                  }
              }
          }
      });
      observer.observe(document.documentElement, {
          childList: true,
          subtree: true
      });
  */
})();

/*
# perso actuel
  action = TR_PERSO_1

# filtre*
  action = EFFACE_FILTRE

# filtre+
  action = MODE_FILTRE_AJOUTER

# filtre−
  action = MODE_FILTRE_SUPPRIMER

# +classe
  action = AJOUTE_CLASSE_ACTIVE

# pve
  action = FILTRE wishlistnotes:pve8 or wishlistnotes:pve9 or wishlistnotes:pve10

# corrompus
  action = FILTRE holdsmod:outlaw

# déchus
  action = FILTRE holdsmod:forge

# ruche
  action = FILTRE modslot:opulent

# vex
  action = FILTRE holdsmod:undying

# armures PM
  action = FILTRE is:armor masterwork:>=9

# pas équipé
  action = FILTRE is:incurrentchar not:equipped is:haspower not:invault

# armes
  action = FILTRE is:weapon not:locked not:tagged or tag:junk not:maxpower not:exotic not:masterwork

# armures < 60
  action = FILTRE is:armor not:classitem not:locked not:tagged or tag:junk not:maxpower not:exotic not:white basestat:total:<60

# équip. classe
  action = FILTRE is:armor is:classitem not:locked not:tagged or tag:junk not:maxpower not:exotic not:masterwork energycapacity:<5

# bleus
  action = FILTRE is:blue is:haspower not:maxpower

# poubelle
  action = FILTRE is:armor not:tagged or tag:junk not:maxpower not:exotic not:masterwork modslot:none

# tag:junk
  action = FILTRE (tag:junk or is:blue) not:maxpower is:haspower

#nv
 action = FILTRE is:new not:maxpower is:haspower not:exotic not:locked

#nv armes
 action = FILTRE is:new is:weapon not:maxpower is:haspower not:exotic not:locked

# COFFRE
  action = TR_COFFRE

# PVE 7
 action = FILTRE wishlistnotes:pve7

# PVE 8
 action = FILTRE wishlistnotes:pve8

# PVE 9
 action = FILTRE wishlistnotes:pve9

# PVE 10
 action = FILTRE wishlistnotes:pve10

# PVE ≥7
 action = FILTRE wishlistnotes:pve10 or wishlistnotes:pve9 or wishlistnotes:pve8 or wishlistnotes:pve7

*/