CivitAi Utilities

A script that adds new tools to the civitai image generator

// ==UserScript==
// @name         CivitAi Utilities
// @namespace    http://tampermonkey.net/
// @version      0.11
// @description  A script that adds new tools to the civitai image generator
// @author       Pedro6159
// @match        https://civitai.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=civitai.com
// @run-at document-end
// @license      MIT
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_xmlhttpRequest
// ==/UserScript==

(function () {
    'use strict';
    // Editable variables for users
    var delay = 500 // Milliseconds to wait for the script to start injecting into the website.
    var enable_infolabels = true; // Creates labels that show positive and negative prompt size
    var enable_taglist = true; // Enable tag list box
    var tag_list_url = "https://drive.google.com/uc?id=1uR_hEn_6BQz3gP4gO0nhVfF9TY97JSNP"; // You must have a raw tag list url

    // code

    // Url
    var u = window.document.URL;
    var datalist = undefined;
    var datalist_bkp = undefined;


    function getElementByXpath(path) {
        return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    }
    // Ele filtra a string para retornar apenas a palavra, remove sintaxes do stable diffusion como "()", "[]", "{}", e os pesos ":x.x"
    String.prototype.sdfilter = function () {
        return this.toString().replace(/[()\[\]{}'"]|\W[:\d+(\.\d+)?]/g, "").trim().toLowerCase();
    };
    function addlabels(element) {
        // Prompt Label Length
        let _pll = document.createElement("span");
        _pll.id = "promptlength"; _pll.innerText = "Prompt Length : 0/1024";
        // Prompt Label Negativo
        let _negpll = document.createElement("span");
        _negpll.id = "negpromptlength"; _negpll.innerText = "Neg Prompt Length : 0/1024";
        // Adiciona os dois na ordem
        element.append(_pll);
        element.insertBefore(_pll, element.childNodes[0]);
        element.append(_negpll);
        element.insertBefore(_negpll, element.childNodes[1]);
    }
    function initialize_gui(gui) {
        const promptarea = getElementByXpath("(//textarea[contains(@class,'input')])[1]")
        const negpromptarea = getElementByXpath("(//textarea[contains(@class,'input')])[2]")
        promptarea.addEventListener('input', () => {
            updatelabel()
        });
        negpromptarea.addEventListener('input', () => {
            updatelabel()
        });
        if (enable_infolabels)
            addlabels(gui.childNodes[0]);
        if (enable_taglist)
            initialize_autocomplete(gui.childNodes[0]);
        console.log("Loaded UI");
    }

    // INICIA A CONSTRUÇÃO DO AUTO COMPLETE
    function initialize_autocomplete(element) {
        let infodiv = document.createElement("div");
        infodiv.id = "infodiv";
        element.append(infodiv);
        element.insertBefore(infodiv, element.childNodes[2]);
        // Infodiv pega o elemento criado pelo script com mesmo ID, então suporta ambas interfaces
        // Começa a adicionar o div na interface
        let ac_div = document.createElement("div");
        ac_div.style = "border: 3px solid rgb(52 52 52); overflow: auto;min-height:100px;height:250px;resize: vertical;";
        ac_div.id = "infoautocomplete";
        ac_div.beforeunload = function () { alert("deletado") };
        infodiv.append(ac_div);
        var order_type = 1;
        var btn_order = document.createElement("span");
        btn_order.style = "font-weight: bold; display: inline-block;border:thin solid;margin: 0 1px;padding:5px;cursor:pointer;"
        btn_order.innerText = "Sort (Normal)";
        btn_order.onclick = () => {
            if (order_type == 1) { // Mudar para ordem alfabetica (A-Z)
                datalist.sort();
                btn_order.innerText = "Sort (A-Z)";
                btn_order.style.color = "#8f9cff";
                order_type = 2;
            }
            else if (order_type == 2) {
                datalist.reverse();
                btn_order.innerText = "Sort (Z-A)";
                btn_order.style.color = "#90ff8f";
                order_type = 3;
            }
            else if (order_type == 3) {
                shuffle(datalist);
                btn_order.innerText = "Sort (Random)";
                btn_order.style.color = "rgb(255 55 219)";
                order_type = 4;
            }
            else if (order_type == 4) {
                datalist = Array.from(datalist_bkp);
                btn_order.innerText = "Sort (Normal)";
                btn_order.style.color = "White";
                order_type = 1;
            }
            autocompletelist.innerHTML = "";
            let results = getResults(selectedword[1].sdfilter());
            showtags(results);
        };
        ac_div.append(btn_order);
        var order_type_2 = 1;
        var btn_order2 = document.createElement("span");
        btn_order2.style = "font-weight: bold; display: inline-block;border:thin solid;margin: 0 1px;padding:5px;cursor:pointer;"
        btn_order2.innerText = "View (Inline)";
        btn_order2.onclick = () => {
            if (order_type_2 == 1) { // Mudar para ordem alfabetica (A-Z)
                btn_order2.innerText = "View (Vertical)";
                btn_order2.style.color = "#8f9cff";
                autocompletelist.style.display = "grid";
                order_type_2 = 2;
            }
            else if (order_type_2 == 2) {
                datalist.reverse();
                btn_order2.innerText = "View (Inline)";
                btn_order2.style.color = "White";
                autocompletelist.style.display = "inline";
                order_type_2 = 1;
            }
        };
        ac_div.append(btn_order2);
        ac_div.append(document.createElement("br"));

        if (datalist == undefined) {
            // Label dizendo que esta carregando a lista
            var labelloading = document.createElement("p");
            labelloading.id = "loadlabel";
            labelloading.style = "font-size:24px;font-weight: bold;color:Cyan"
            labelloading.innerText = "Loading TagList...";
            ac_div.append(labelloading);
        }
        // Elemento de lista de resultados
        var autocompletelist = document.createElement("ul");
        autocompletelist.style = "display:inline;padding-left: 0px;";
        autocompletelist.id = "results";
        ac_div.append(autocompletelist);



        if (datalist == undefined) {
            GM_xmlhttpRequest({
                method: "GET",
                // Url da lista de TAgs
                url: tag_list_url,
                onload: (ev) => {
                    // Foi carregado e atualiza o label
                    datalist = ev.responseText.match(/[^\r\n]+/g);
                    var versioninfo = datalist[0];
                    if (!versioninfo.startsWith("("))
                        versioninfo = "Customized";
                    labelloading.innerText = "Tag list loaded.\r\nCurrent version : " + versioninfo;
                    console.log("AutoComplete Tags Loaded | " + versioninfo);
                    labelloading.style.color = "#66FF99";
                    labelloading.style.cursor = "pointer";
                    datalist.shift();
                    datalist_bkp = Array.from(datalist);
                    // Avisar que foi carregado e fazer animação do label desaparecer lentamente
                    var o = 0;
                    var fade = setInterval(function () {
                        if (o < 100) {
                            labelloading.style.opacity = ((100 - o) + "%"); o = o + 0.11;
                        }
                        else {
                            clearInterval(fade); labelloading.remove();
                        }
                    }, 1);
                    labelloading.onclick = function () {
                        labelloading.remove();
                        clearInterval(fade);
                    }
                },
                onerror: () => {
                    console.error("List not loaded, invalid url.");
                }
            });
        }

        var promptarea = getElementByXpath("(//textarea[contains(@class,'input')])[1]");
        // Pega a caixa de texto do prompt dependendo da interface
        var selectedword;
        promptarea.oninput = function () {
            // Esse limpa tudo.
            autocompletelist.innerHTML = "";
            // Começa a progurar por tags
            selectedword = getWord(promptarea);
            let results = getResults(selectedword[1].sdfilter());
            showtags(results);
        };
        function showtags(results) {
            if (results.length > 0) {
                // Ele exibe a lista de tags em relação aos caractere que tu escreveu
                let i = 0;
                for (i; i < results.length; i++) {
                    const tags = document.createElement("li");
                    tags.style = "display: inline-block;padding: 0.5rem;border-color:white;border:solid;border-width:thin";
                    tags.className = "tags";
                    if (results[i][1] == 1) {
                        tags.style["background-color"] = "#302D00";
                        tags.innerText = results[i][0];
                        autocompletelist.append(tags);
                        autocompletelist.insertBefore(tags, autocompletelist.childNodes[0]);
                    }
                    else {
                        tags.innerText = results[i][0];
                        autocompletelist.append(tags);
                    }

                }
                // Caso chegue até o final, não vai adicionar o botão de "Mostrar Mais"
                if (i > 40) {
                    autocompletelist.innerHTML += "<li id='showmore' style='display: inline-block;padding: 0.5rem;border-color:white;border:solid;border-width:thin;background-color:darkblue'>Show More</li>";
                    // Função de mostrar mais
                    document.getElementById("showmore").onclick = () => {
                        // O texto da ultima tag
                        const lasttagarea = autocompletelist.childNodes[autocompletelist.childNodes.length - 2].innerText;
                        // Começa a procurar mais tags
                        let results = getResults(selectedword[1].sdfilter(), lasttagarea);
                        // Remove o "Show More"
                        autocompletelist.childNodes[autocompletelist.childNodes.length - 1].remove();
                        showtags(results);
                    };
                }
            }
        }
        // Obtem o array a partir da palavra que você escreveu para auto completar, que tem relação a lista de tags
        function getResults(input, last) {
            input = input.replace(/[_+-]+/g, " ");
            const results = [];
            let i = 0;
            // Caso você tiver clicado em "Mostrar Mais", ele pega a ultima tag da lista e continua a procurar por mais
            if (last) {
                i = searchStringInArray(last, datalist) + 1
            }

            // Pega palavras proximas
            const l = datalist.length;
            for (i; i < l; i++) {
                if (input === datalist[i].slice(0, input.length)) {
                    if (input == datalist[i])
                        results.push([datalist[i], 1]); // 1 a palavra é igual a tag
                    else
                        results.push([datalist[i], 0]); // 0 sugestoes de tag

                    if (results.length > 40)
                        break;
                }

            }
            return results;
        }
        // Quando você clicar em umas das tags para completar a palavra
        autocompletelist.onclick = function (event) {
            // Identificador tags, para não pensar que é aquele botão de 'mostrar mais'
            if (event.target.className != "tags")
                return;
            const setValue = event.target.innerText;
            // Separa o prompt em virgulas e remove espaços vazios
            let valuetext = promptarea.value.split(",").map(function (word) {
                return word.trim();
            });
            // Começa a procurar a palavra no array separado em virgula e depois substituir
            for (let i = 0; i < valuetext.length; i++) {
                if (valuetext[i].trim() == selectedword[1]) {
                    // Preserva os parenteses e a sintaxe de peso
                    const word = valuetext[i].sdfilter();
                    valuetext[i] = valuetext[i].replace(word, setValue);
                }
            }
            promptarea.value = valuetext.join(", ");
            this.innerHTML = "";
        };
    }

    // Procura a string no array retornando apenas o index
    function searchStringInArray(str, strArray) {
        const l = strArray.length;
        for (var j = 0; j < l; j++) {
            if (strArray[j].match(str)) return j;
        }
        return -1;
    };
    function getWord(textarea) {
        var cursorPos = textarea.selectionStart;
        var text = textarea.value;

        // Encontrar a palavra na posição do cursor
        var start = text.lastIndexOf(',', cursorPos - 1) + 1;
        var end = text.indexOf(',', cursorPos);
        end = end === -1 ? text.length : end;

        // Extrair a palavra
        var selectedWord = text.substring(start, end).trim();

        // Separar as palavras por vírgula
        var wordsArray = text.split(',').map(function (word) {
            return word.trim();
        });
        var selectedIndex = wordsArray.indexOf(selectedWord);
        //console.log("Posição do cursor:", cursorPos);
        //console.log("Palavra selecionada:", selectedWord);
        //console.log("Array de palavras:", wordsArray);
        return [selectedIndex, selectedWord.trim(), wordsArray];
    };
    // Aleatorizar Array
    function shuffle(array) {
        let currentIndex = array.length, randomIndex;

        // While there remain elements to shuffle.
        while (currentIndex > 0) {

            // Pick a remaining element.
            randomIndex = Math.floor(Math.random() * currentIndex);
            currentIndex--;

            // And swap it with the current element.
            [array[currentIndex], array[randomIndex]] = [
                array[randomIndex], array[currentIndex]];
        }

        return array;
    };
    // Atualiza as informações do tamanho do prompt positivo e negativo
    function updatelabel() {
        document.getElementById("promptlength").innerText = "Prompt Length : " + getElementByXpath("(//textarea[contains(@class,'input')])[1]").value.length;
        document.getElementById("negpromptlength").innerText = "Neg Prompt Length : " + getElementByXpath("(//textarea[contains(@class,'input')])[2]").value.length;
    };
    window.onload = function () {
        if (document.readyState == 'complete') {
            var gui;
            console.log("Trying to find the interface to inject the script...");
            var tempo = setInterval(function () {
                if (!document.getElementById("infodiv")) {
                    gui = getElementByXpath("//form/div[1]/div[1]/div[not(@style)]/div[3]");
                    if (gui) {
                        initialize_gui(gui);
                    }
                }
            }, delay);
            window.onload = null;
        }
    };
})();