Danbooru Tags Select to Sort and Export

Select specified tags and copy to clipboard, for Stable Diffusion WebUI or NovelAI to use. Tags can be sorted by tag order in NovelAI method.

// ==UserScript==
// @name         Danbooru Tags Select to Sort and Export
// @name:zh-TW   Danbooru 標籤 選擇排序和匯出器
// @name:zh-HK   Danbooru 標籤 選擇排序和匯出器
// @name:zh-CN   Danbooru 标签 选择排序和导出器
// @name:ja      Danbooru Tags Select to Sort and Export
// @namespace    https://github.com/Takenoko3333/Danbooru-Tags-Sort-Exporter
// @supportURL   https://github.com/Takenoko3333/Danbooru-Tags-Sort-Exporter/issues
// @homepageURL  https://github.com/Takenoko3333/Danbooru-Tags-Sort-Exporter
// @require      https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @version      0.8.1
// @description  Select specified tags and copy to clipboard, for Stable Diffusion WebUI or NovelAI to use. Tags can be sorted by tag order in NovelAI method.
// @description:zh-TW  選擇指定標籤並複製到剪貼板,供Stable Diffusion WebUI或NovelAI等使用。標籤可根據 NovelAI 的標籤排序方法進行排序。
// @description:zh-HK  選擇指定標籤並複製到剪貼板,供Stable Diffusion WebUI或NovelAI等使用。標籤可根據 NovelAI 的標籤排序方法進行排序。
// @description:zh-CN  选择指定标签并复制到剪贴板,供Stable Diffusion WebUI或NovelAI等使用。标签可根据 NovelAI 的标签排序方法进行排序。
// @description:ja  指定したタグを選択し、クリップボードにコピーして、Stable Diffusion WebUIやNovelAIなどで使用することができます。タグをNovelAI方式のタグ順序で並べ替えることが可能です。
// @author       Takenoko3333
// @match        https://danbooru.donmai.us/posts/*
// @match        https://aibooru.online/posts/*
// @match        https://betabooru.donmai.us/posts/*
// @match        https://e621.net/posts/*
// @match        https://gelbooru.com/index.php?page=post&s=view*
// @match        https://rule34.xxx/index.php?page=post&s=view*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=donmai.us
// @grant        GM.setClipboard
// @grant        GM.notification
// @grant        GM.addStyle
// @license      AGPL-3.0
// ==/UserScript==

// Forked from FSpark/Danbooru-Tags-Exporter(https://github.com/FSpark/Danbooru-Tags-Exporter)

(function () {
    'use strict';

    // Settings changed on the web page are saved to local storage and applied when revisiting.
    // ウェブページ上で変更した設定はローカルストレージに保存され、再訪時に適用されます。

    // ==Settings==
    const initialSettings = {
        sort: true, // Sorted by tag order in NovelAI method. (タグをNovelAI方式で並び替える)
        bracketEscape: false, // true: Stable Diffusion, false: NovelAI
        setWeight: false, // true: Show and Activate weight inputs. (ウェイト入力の表示と有効化)
        useBracket: 1, // 0: ( ) Stable Diffusion, 1: { } NovelAI
        selections: {artist: true, copyright: true, character: true, species: true, general: true} // Pre-checked state for each category. (カテゴリ毎の初期選択状態)
    };
    // ==/Settings==

    let settings = {};
    let localSettings = JSON.parse(localStorage.getItem("settings")) || {};
    for (let key in initialSettings) {
        if (localSettings[key] === undefined) {
            settings[key] = initialSettings[key];
        } else {
            settings[key] = localSettings[key];
        }
    }
    localStorage.setItem("settings", JSON.stringify(settings));
    // console.log(settings)

    let sortCheck = "";
    let bracketEscapeCheck = "";
    let setWeightCheck = "";
    let roundBracketsRadio = "";
    let curlyBracketsRadio = "";
    let selectArtistCheck = "";
    let selectCopyrightCheck = "";
    let selectCharacterCheck = "";
    let selectSpeciesCheck = "";
    let selectGeneralCheck = "";

    if(settings.sort) {
        sortCheck = "checked";
    }
    if(settings.bracketEscape) {
        bracketEscapeCheck = "checked";
    }
    if(settings.setWeight) {
        setWeightCheck = "checked";
    }
    if(settings.useBracket) {
        curlyBracketsRadio = "checked";
    } else {
        roundBracketsRadio = "checked";
    }
    if(settings.selections) {
        if(settings.selections.artist) {
            selectArtistCheck = "checked";
        }
        if(settings.selections.copyright) {
            selectCopyrightCheck = "checked";
        }
        if(settings.selections.character) {
            selectCharacterCheck = "checked";
        }
        if(settings.selections.species) {
            selectSpeciesCheck = "checked";
        }
        if(settings.selections.general) {
            selectGeneralCheck = "checked";
        }
    }

    let isRule34 = location.hostname == "rule34.xxx";
    let tagListSelector = isRule34 ? "#tag-sidebar" : "#tag-list";

    GM.addStyle(`#tags-exporter-setting button, ${tagListSelector} button {margin: 0.25rem 0 0.5rem 0; padding: 0.25em 0.55em;}
                 #tags-exporter-setting button#reset-settings {margin-top: .5em}
                 #tags-exporter-setting label {display: inline-block; padding: .1em .25em; line-height: 1.5em; font-weight: normal;}
                 #tags-exporter-setting .heading {margin-top: .25em; line-height: 1.5em;}
                 #tags-exporter-setting .inline-checkbox {display: inline-block;}
                 #tags-exporter-setting .use-bracket {margin-left: 1.3em;}
                 ${tagListSelector} input[type='checkbox'] {margin-right: .4em; vertical-align: text-bottom;}
                 .tag-weight {width: 3em; margin-right: .4em}
                `);

    if (location.hostname == "gelbooru.com") {
        GM.addStyle(`#tags-exporter-setting {margin: 0 10px 0 25px;}
                     #tags-exporter-setting h2 {font-size: 1.2em;}
                     #tags-exporter-setting .heading {font-weight: bold}
                     #tags-exporter-setting button,  ${tagListSelector} button {padding: 0.25em 0.4em;}
                     [id$="-tag-buttons"] {margin: 0 4px 0 15px;}
                     .tag-weight {width: 2.5em;}
                    `);
    }

    if (location.hostname == "rule34.xxx") {
        GM.addStyle(`#tags-exporter-setting {margin: 10px 0;}
                     #tags-exporter-setting h2 {font-size: 1.2em;}
                     #tags-exporter-setting .heading {font-weight: bold}
                     #tags-exporter-setting button,  ${tagListSelector} button {padding: 0.25em 0.4em;}
                     #tags-exporter-setting input, [class^="tag-type-"] input {margin: 0;}
                     input.tag-weight {width: 2.5em; padding: 0 0 0 .1em;}
                     ${tagListSelector} h6 {margin-top: 13px;}
                    `);

        for (let i = 0; i < document.styleSheets.length; i++) {
            let href = document.styleSheets[i].href;
            if (href) {
                let fileName = href.split('/').pop();
                if (fileName.startsWith('mobile.css')) {
                    GM.addStyle(`#tags-exporter-setting {text-align: center;}
                     #tags-exporter-setting input, ${tagListSelector} input[type='checkbox'] {height: 27px; vertical-align: middle; transform: scale(1.5);}
                     #tags-exporter-setting input {margin: 0 0.2em}
                     ${tagListSelector} input[type='checkbox'] {margin-right: 0.6em;}
                     #tags-exporter-setting .use-bracket {margin-left: 0;}
                    `);
                }
            }
        }
    }

    if (location.hostname != "e621.net") {
        GM.addStyle(`#tags-exporter-setting .show-e621 {display: none;}
                    `);
    }

    if (location.hostname == "e621.net") {
        GM.addStyle(`#tags-exporter-setting h2 {font-size: 1.16667em;}
                     #tags-exporter-setting .show-e621 {display: inline-block;}
                     .tag-list-header {margin-bottom: 2px;}
                     ul + .tag-list-header {margin-top: 8px;}
                     .tag-weight {width: 2.5em; padding: 1px;}
                    `);
    }

    let SettingPanel = document.createElement('section');
    SettingPanel.id = "tags-exporter-setting";
    SettingPanel.innerHTML = `
        <h2>Tags Export Settings</h2>
        <input type="checkbox" id="sort" ${sortCheck}/><label for="sort">Sort by NovelAI method</label><br>
        <input type="checkbox" id="bracket-escape" ${bracketEscapeCheck}/><label for="bracket-escape"><code>(</code> <code>)</code> -> <code>\\(</code> <code>\\)</code></label><br>
        <input type="checkbox" id="set-weight"  ${setWeightCheck}/><label for="set-weight">Setting weights</label><br>
        <div class="use-bracket">
        <input type="radio" name="use_bracket" id="round-brackets" value="0" ${roundBracketsRadio}/><label for="round-brackets">Using ( )</label>
        <input type="radio" name="use_bracket" id="curly-brackets" value="1" ${curlyBracketsRadio}/><label for="curly-brackets">Using { }</label>
        </div>
        <div>
        <div class="heading">Pre-checked</div>
        <span class="inline-checkbox"><input type="checkbox" id="select-artist" ${selectArtistCheck}/><label for="select-artist">Artist</label></span>
        <span class="inline-checkbox"><input type="checkbox" id="select-copyright" ${selectCopyrightCheck}/><label for="select-copyright">Copyright</label></span>
        <span class="inline-checkbox"><input type="checkbox" id="select-character" ${selectCharacterCheck}/><label for="select-character">Character</label></span>
        <span class="inline-checkbox show-e621"><input type="checkbox" id="select-species" ${selectGeneralCheck}/><label for="select-species">Species</label></span>
        <span class="inline-checkbox"><input type="checkbox" id="select-general" ${selectGeneralCheck}/><label for="select-general">General</label></span>
        </div>
        <button name="reset_settings" id="reset-settings">Settings Reset</button>
        `
	let Container = document.createElement('div');
    Container.id = "tags-exporter-container";
    Container.innerHTML = `
        <button name="select_all">All</button>
        <button name="select_none">None</button>
        <button name="invert_select">Invert</button>
        <button name="export">Export</button>
        `

    function insertBefore(newNode, referenceNode) {
        referenceNode.parentNode.insertBefore(newNode, referenceNode);
    }

    function insertAfter(newNode, referenceNode) {
        referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
    }

    if (location.hostname == "gelbooru.com" || location.hostname == "rule34.xxx") {
        insertBefore(SettingPanel, document.querySelector(tagListSelector));
    } else {
        insertAfter(SettingPanel, document.querySelector("#search-box"));
    }
    insertAfter(Container, document.querySelector("#tags-exporter-setting > h2"));

    function addBrackets(prompts,isRound,n){
        let l,r;
        if(n==0) {
            return prompts
        }
        else if(n>0){
            if(isRound){
                l='('
                r=')'}
            else{
                l='{'
                r='}'
            }
        }else{
            l='['
            r=']'
        }
        n=Math.abs(n)
        return l.repeat(n).concat(prompts,r.repeat(n))
    }
    function exportTags(target){
        let tags = []
        let reorderedTags = []
        let sort = document.getElementById("sort").checked
        let bracket_escape = document.getElementById("bracket-escape").checked
        let set_weight = document.getElementById("set-weight").checked
        let round_brackets = document.getElementById("round-brackets").checked
        if(!target) {
            if(sort) {
                ["[name=character-tags]:checked", "[name=copyright-tags]:checked", "[name=artist-tags]:checked", "[name=species-tags]:checked", "[name=general-tags]:checked"].forEach((t)=>createTags(t));
            } else {
                createTags(`${tagListSelector} input[type='checkbox']:checked`);
            }
        } else {
            createTags(target);
        }
        function createTags(target) {
            document.querySelectorAll(target).forEach((e) => {
                let prompts = e.value
                if(bracket_escape) {
                    prompts = prompts.replaceAll(`(`,`\\(`).replaceAll(`)`,`\\)`)
                }
                if(set_weight) {
                    prompts = addBrackets(prompts,round_brackets,e.nextSibling.value)
                }
                tags.push(prompts)
            })
        }

        if(sort) {
            const regexp = /[1-6]\+?(girl|boy)s?/;
            const girlsTags = tags.filter(tag => tag.includes('girl') && regexp.test(tag));
            const boysTags = tags.filter(tag => !tag.includes('girl') && regexp.test(tag));
            const otherTags = tags.filter(tag => !regexp.test(tag));
            reorderedTags = [...boysTags, ...girlsTags, ...otherTags];
        } else {
            reorderedTags = tags;
        }
        let res = reorderedTags.join(", ")

        GM.setClipboard(res)
        GM.notification(`${reorderedTags.length} tag(s) were copied.`, "Danbooru Tags Sort and Exporter")
    }

    function insertButtons(target){
        let head = document.querySelector(`h3.${target}-tag-list`)
        if (location.hostname == "gelbooru.com" || location.hostname == "rule34.xxx") {
            head = document.querySelector(`.tag-type-${target}`)
        }
        if (location.hostname == "e621.net") {
            head = document.querySelector(`.${target}-tag-list`)
        }
        if(!head) return;
        let buttonContainer = Container.cloneNode(true)
        buttonContainer.id = `${target}-tag-buttons`
        if (location.hostname == "gelbooru.com" || location.hostname == "rule34.xxx" || location.hostname == "e621.net") {
            insertBefore(buttonContainer, head)
        } else {
            insertAfter(buttonContainer, head)
        }

        let tagItem = `.${target}-tag-list>li`;
        if (location.hostname == "gelbooru.com" || location.hostname == "rule34.xxx") {
            tagItem = `.tag-type-${target}`;
        }

        document.querySelectorAll(tagItem).forEach((e) => {
            let chk = document.createElement('input');
            chk.type = "checkbox"
            chk.name = `${target}-tags`
            if (location.hostname == "gelbooru.com" || location.hostname == "rule34.xxx" || location.hostname == "e621.net") {
                let aTags = e.querySelectorAll('a');
                if (aTags.length > 0) {
                    let lastATag = aTags[aTags.length - 1];
                    chk.value = lastATag.textContent;
                }
            } else {
                chk.value = e.dataset.tagName.replaceAll("_", " ")
            }
            if(settings.selections[target.replace("-tag", "")]) {
                chk.checked = true
            }
            e.insertBefore(chk, e.firstChild)

            let nbr = document.createElement('input');
            nbr.type = "number"
            nbr.name = `${target}-tags-weight`
            nbr.className = "tag-weight"
            nbr.value = 0
            nbr.hidden = true
            insertAfter(nbr,chk)
        })

        buttonContainer.querySelector("[name='select_all']").onclick = function () {
            var items = document.getElementsByName(`${target}-tags`);
            for (var i = 0; i < items.length; i++) {
                items[i].checked = true;

            }
        };
        buttonContainer.querySelector("[name='select_none']").onclick = function () {
            var items = document.getElementsByName(`${target}-tags`);
            for (var i = 0; i < items.length; i++) {
                items[i].checked = false;

            }
        };
        buttonContainer.querySelector("[name='invert_select']").onclick = function () {
            var items = document.getElementsByName(`${target}-tags`);
            for (var i = 0; i < items.length; i++) {
                items[i].checked == true ? items[i].checked = false : items[i].checked = true;

            }
        };
        buttonContainer.querySelector("[name='export']").onclick = function () {
            exportTags(`[name=${target}-tags]:checked`)
        };
    }

    function setSettings() {
        let sort = document.getElementById("sort").checked;
        let bracketEscape = document.getElementById("bracket-escape").checked;
        let setWeight = document.getElementById("set-weight").checked;
        let roundBrackets = document.getElementById("round-brackets").checked;
        let useBracket = roundBrackets ? 0 : 1;
        let selections = {artist: document.getElementById("select-artist").checked,
                          copyright: document.getElementById("select-copyright").checked,
                          character: document.getElementById("select-character").checked,
                          species: document.getElementById("select-species").checked,
                          general: document.getElementById("select-general").checked
                         };
        localStorage.setItem("settings", JSON.stringify({sort: sort, bracketEscape: bracketEscape, setWeight: setWeight, useBracket: useBracket, selections: selections}));
        // console.log(JSON.parse(localStorage.getItem("settings")));
    }

    function resetSettings() {
        localStorage.removeItem("settings");
        document.getElementById("sort").checked = initialSettings.sort;
        document.getElementById("bracket-escape").checked = initialSettings.bracketEscape;
        document.getElementById("set-weight").checked = initialSettings.setWeight;
        if(initialSettings.useBracket) {
            document.getElementById("curly-brackets").checked = true;
        } else {
            document.getElementById("round-brackets").checked = true;
        }
        toggleWeightInputs();
        document.getElementById("select-artist").checked = initialSettings.selections.artist;
        document.getElementById("select-copyright").checked = initialSettings.selections.copyright;
        document.getElementById("select-character").checked = initialSettings.selections.character;
        document.getElementById("select-species").checked = initialSettings.selections.species;
        document.getElementById("select-general").checked = initialSettings.selections.general;
    }

    function toggleWeightInputs(event) {
        const target = event ? event.target : document.getElementById('set-weight');
        if (target && target.id === 'set-weight') {
            const isSetWeightChecked = target.checked;
            document.querySelectorAll(`${tagListSelector} input[type='number']`).forEach(e => { e.hidden = !isSetWeightChecked; });
        }
    }

    ["artist","character","copyright", "species", "general"].forEach((t)=>insertButtons(t))

    toggleWeightInputs();

    Container.querySelector("[name='select_all']").onclick = function () {
        var items = document.querySelectorAll(`${tagListSelector} input[type='checkbox']`)
        for (var i = 0; i < items.length; i++) {
            items[i].checked = true;
        }
    };
    Container.querySelector("[name='select_none']").onclick = function () {
        var items = document.querySelectorAll(`${tagListSelector} input[type='checkbox']`)
        for (var i = 0; i < items.length; i++) {
            items[i].checked = false;
        }
    };
    Container.querySelector("[name='invert_select']").onclick = function () {
        var items = document.querySelectorAll(`${tagListSelector} input[type='checkbox']`)
        for (var i = 0; i < items.length; i++) {
            items[i].checked == true ? items[i].checked = false : items[i].checked = true;
        }
    };
    Container.querySelector("[name='export']").onclick = function () {
        exportTags()
    };
    SettingPanel.querySelector("[name='reset_settings']").onclick = function () {
        resetSettings()
    };
    SettingPanel.querySelectorAll("input[type='radio'], input[type='checkbox']").forEach(e => {
        e.onchange = function (event) {
            setSettings();
            toggleWeightInputs(event);
        };
    });

})();