dev_group_list2

Better Submit-to-group dialog

目前為 2022-09-19 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         dev_group_list2
// @namespace    http://www.deviantart.com/
// @version      5.4
// @description  Better Submit-to-group dialog
// @author       Dediggefedde
// @match        https://www.deviantart.com/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue

// ==/UserScript==
/* globals $*/
// @require      http://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js

//dgl2=dgl2 identifiert/classname
(function() {
    'use strict';
    let userName = "";
    let moduleID = 0;
    let grPerReq = 24;
    let groups = []; //{userId,useridUuid,username,usericon,type,isNewDeviant,latestDate}
    const inactiveDate = new Date();
    inactiveDate.setMonth(inactiveDate.getMonth() - 3); //inactive if latest submission before 3 months
    // isNewDeviant not needed.
    // type=oneof{group, super group}
    // usericon always starts with https://a.deviantart.net/avatars-big
    // useridUuid specific to the group, contains submission rights
    // userid of the group
    // latestDate newest publish date of thumb for a folder; filled later when folders requested
    let groupN = 0;
    let listedGroups = [];
    let devID;
    let collections = [{
        id: 0,
        name: "all",
        groups: [],
        showing: 1
    }];
    let collectionOrder = [];
    let colMode = 0; //0 show, 1 add, 2 remove, 3 delete
    let colModeTarget = 0;
    let macros = []; //{id, name, data:[{folderName, folderID, groupID, type}]}
    let macroOrder = [];
    let macroMode = 0; //0 idle, 1 record, 2 play, 3 remove
    let macroModeTarget = 0; //for recording
    let lastFilter = 0;
    var dragSrcEl = null;
    let colListMode = 0; //0 collection, 1 macro;
    let svgTurnArrow = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1.507856 1.2692268" style="height:0.5em;"> <defs id="defs2"> <marker id="Arrow2Send" style="overflow:visible"> <path id="path8454" style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1" d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" transform="matrix(-0.3,0,0,-0.3,0.69,0)" /> </marker> </defs> <g id="layer1" transform="translate(-1.2565625,-0.79875001)"> <path style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Send)" d="m 2.38125,0.93125 c -1.3229167,0 -1.3229166,0.79375 0,0.79375" id="path8419"/> </g> </svg> ';
    let fetchingGroups = false; //prevent refresh button requesting multiple times at once
    let entityMap = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
        '/': '&#x2F;',
        '`': '&#x60;',
        '=': '&#x3D;'
    };
    let loadedFolders = new Map();
    let lastGroupClickID;
    let notScanThisInstance = false;
    let targetName = ""; //target group or macro name

    const errtyps = Object.freeze({
        Connection_Error: "Connection Error",
        No_User_ID: "No User ID",
        Unknown_Error: "Site Error",
        Parse_Error: "Parse Error",
        Wrong_Setting: "Wrong Profile setting"
    });
    //error protocol convention: ErrType of errtyps, ErrDescr as text, ErrDetail with exception object
    //alert of type/descr, console log with detail

    function err(type, descr, detail) {
        return { ErrType: type, ErrDescr: descr, ErrDetail: detail };
    }

    function errorHndl(err) {
        myMsgBox("<strong>Type</strong>: " + err.ErrType + "<br /><strong>Description</strong>: " + err.ErrDescr + "<br /><br />See the log (F12 in Chrome/Firefox) for more details.<br />If the error persist, feel free to write me a <a style=\"text-decoration: underline;\" href=\"https://www.deviantart.com/dediggefedde/art/dev-group-list2-817465905#comments\">comment</a>.", "Error");
        console.log("dev_group_list2 error:", err);
    }

    //API calls getting data
    function fillGroups(offset) { //async+callback //load all groups

        let murl = "https://www.deviantart.com/_napi/da-user-profile/api/module/groups/members?username=" + userName + "&moduleid=" + moduleID + "&offset=" + offset + "&limit=" + grPerReq;
        return new Promise(function(resolve, reject) {
            GM.xmlHttpRequest({
                method: "GET",
                url: murl,
                onerror: function(response) {
                    reject(err(errtyps.Connection_Error, "Connection to " + murl + " failed", response));
                },
                onload: function(response) {
                    if (response.status == 500) {
                        resolve(oldFillGroups(offset));
                        return
                    }
                    let resp = JSON.parse(response.responseText);

                    if (offset == 0) groups = [];

                    let newnams = resp.results.map(x => x.userId);
                    if (groups.map(x => x.userId).filter(x => newnams.includes(x)).length > 0) {
                        reject(err(errtyps.Wrong_Setting, "Double group entries detected. Probably wrong sorting at profile/about's group-member section. Please choose asc or desc, not random.", groups));
                        $("#dgl2_refresh").css("cursor", "pointer");
                        return;
                    }

                    groups = groups.concat(resp.results);
                    groupN = resp.total;

                    let frac = 0;
                    if (groupN > 0) frac = groups.length / groupN * 100;
                    frac = Math.round(frac);

                    $("#dgl2_refresh rect").css("fill", "url(#dgl2_grad1)");
                    $("#dgl2_grad1_stop1").attr("offset", frac + "%");
                    $("#dgl2_grad1_stop2").attr("offset", (frac + 1) + "%");
                    $("#dgl2_refresh").attr("title", frac + " %");
                    $("span.dgl2_descr").text("Loading List of Groups... " + frac + " %");

                    if (resp.hasMore) {
                        resolve(fillGroups(resp.nextOffset));
                    } else {
                        GM.setValue("groups", JSON.stringify(groups));
                        insertGroups();
                        $("#dgl2_refresh rect").css("fill", "");
                        $("#dgl2_refresh").css("cursor", "pointer");

                        resolve(groups);
                    }
                }
            });
        });
    }

    let ngroups = [];

    function oldFillGroups(offset) { //only fills names and icons
        let murl = "https://www.deviantart.com/" + userName + "/modals/mygroups/?offset=" + offset + "&limit=100";
        return new Promise(function(resolve, reject) {
            GM.xmlHttpRequest({
                method: "GET",
                url: murl,
                onerror: function(response) {
                    reject(err(errtyps.Connection_Error, "Connection to " + murl + " failed", response));
                },
                onload: function(response) {

                    let tgroups = response.responseText.split("<div class=\"mygroup")
                    let rex = /<img.*?class="avatar".*?src="(.*?)".*?title="(.*?)"/i;
                    let rex2 = /data-gruser-type="(.*?)"/i;

                    tgroups = tgroups.map(str => { //parse websites to group data
                        let ret = {};
                        let mat = str.match(rex);
                        if (mat == null) return null;
                        ret.usericon = mat[1];
                        ret.username = mat[2];
                        ret.type = str.match(rex2)[1];
                        return ret;
                    }).filter(el => el != null);
                    ngroups = ngroups.concat(tgroups);

                    $("#dgl2_refresh rect").css("fill", "url(#dgl2_grad1)");
                    $("span.dgl2_descr").text(`Loading List of Groups... ${ngroups.length}/?`);

                    if (response.responseText.indexOf("class=\"next disabled") == -1) { //next not disabled = new page available
                        resolve(oldFillGroups(offset + 100));
                    } else {
                        ngroups.forEach(el => { //new groups not previously in list
                            if (el == null) return;
                            for (let gr of groups) {
                                if (gr.username == el.username) {
                                    Object.assign(el, gr);
                                    //el=gr;
                                    //copy old information to prevent id/uuid loss
                                }
                            }
                        });
                        groups = ngroups;
                        ngroups = [];

                        GM.setValue("groups", JSON.stringify(groups));
                        insertGroups();
                        $("#dgl2_refresh rect").css("fill", "");
                        $("#dgl2_refresh").css("cursor", "pointer");
                        resolve(groups);
                    }
                }
            });
        });
    }

    function grabIDfromPage(name) {
        return new Promise(function(resolve, reject) {
            GM.xmlHttpRequest({
                method: "GET",
                url: "https://www.deviantart.com/" + name,
                onerror: function(response) {
                    reject(err(errtyps.Connection_Error, "Connection to https://www.deviantart.com/" + name + " failed", response));
                },
                onload: async function(response) {

                    $("#dgl2_refresh rect").css("fill", "url(#dgl2_grad1)");
                    ngrpCnt += 1;
                    $("span.dgl2_descr").text(`Loading List of Groups IDs... ${ngrpCnt}/${ngrpleft}`);

                    let rex = /itemid":(.*?),"friendid":"(.*?)"/i;
                    let mat = response.responseText.match(rex);
                    if (mat == null) {
                        reject(err(errtyps.No_User_ID, "Request of " + name + "-id failed", response));
                        return;
                    }

                    groups.forEach(gr => {
                        if (gr.username == name) {
                            gr.userId = mat[1];
                            gr.useridUuid = mat[2];
                        }
                    });
                    GM.setValue("groups", JSON.stringify(groups));
                    resolve(mat[1]);
                }
            });
        });
    }

    function fillSubFolder(groupID, type, name) { //async+callback //type =[collection,gallery]

        if (groupID == "undefined") {
            $(".dgl2_groupdialog ").css("cursor", "wait");
            $(".dgl2_groupButton").css("cursor", "wait");
            return grabIDfromPage(name).then(id => {
                groupID = id;
                lastGroupClickID = id;
                return fillSubFolder(groupID, type, name);
            }).catch(erg => {
                errorHndl(erg);
                $(".dgl2_groupdialog ").css("cursor", "pointer");
                return null;
            });
        }

        return new Promise(function(resolve, reject) {
            let murl = "https://www.deviantart.com/_napi/shared_api/deviation/group_folders?groupid=" + groupID + "&type=" + type;
            GM.xmlHttpRequest({
                method: "GET",
                url: murl,
                onerror: function(response) {
                    reject(err(errtyps.Connection_Error, "Connection to " + murl + " failed", response));
                },
                onload: async function(response) {
                    let resp;
                    let errg;
                    try {
                        resp = JSON.parse(response.responseText);
                    } catch (ex) {
                        errg = err(errtyps.Connection_Error, "Page problems. Please try again later!", ex)
                        reject(errg)
                    }
                    if (typeof resp.results == "undefined") {
                        errg = err(errtyps.Parse_Error, "Error parsing website response. Private browser mode active?", response);
                        errorHndl(errg);
                    } else {
                        let latestDate = Math.max(...Object.values(resp.results).map(o => o.thumb ? new Date(o.thumb.publishedTime) : null));
                        let grInd = groups.findIndex(item => item.userId == groupID);
                        groups[grInd].latestDate = latestDate;
                        let but = document.querySelector(`button.dgl2_groupButton[groupid='${groupID}']`);
                        if (latestDate != null && but != null) {
                            but.setAttribute("title", escapeHtml(groups[grInd].username) + "\n Last submission: " + (new Date(latestDate)).toLocaleString());
                            but.setAttribute("activity", (inactiveDate < new Date(latestDate)) ? "active" : "inactive");
                        }
                        insertSubFolders(resp.results);
                        if (macroMode == 1) {
                            for (let gr of macros[macroModeTarget].data) {
                                if (gr.groupID == groupID) {
                                    $("button.dgl2_groupButton[folderID='" + gr.folderID + "']").addClass("folderInMacro");
                                    break;
                                }
                            }
                        }
                    }
                    $(".dgl2_groupdialog").css("cursor", "");
                    $(".dgl2_groupButton").css("cursor", "pointer");
                    $("div.groupPopup").focus();
                    resolve(resp.results);

                }
            });
        });
    }

    function fillModuleID() { //async+callback //get Module ID for group submission
        let murl = "https://www.deviantart.com/" + userName + "/about";
        return new Promise(function(resolve, reject) {
            GM.xmlHttpRequest({
                method: "GET",
                url: murl,
                onerror: function(response) {
                    reject(err(errtyps.Connection_Error, "Connection to " + murl + " failed", response));
                },
                onload: async function(response) {
                    try {
                        let resp = (response.responseText);
                        resp = resp.match(/{\\\"id\\\":(\d+),\\\"type\\\":\\\"group_list_members/i);
                        if (resp != null && resp.length > 1) {
                            moduleID = resp[1];
                            resolve(moduleID);
                        } else {
                            reject(err(errtyps.Wrong_Setting, "No group-member section in /about page", resp));
                        }
                    } catch (ex) {
                        reject(err(errtyps.Unknown_Error, "Something went wrong while accessing groups", ex));
                    }
                }
            });
        });
    }

    function fillListedGroups(devID) {
        let murl = "https://www.deviantart.com/deviation/" + devID + "/groups";
        return new Promise(function(resolve, reject) {
            GM.xmlHttpRequest({
                method: "GET",
                url: murl,
                onerror: function(response) {
                    reject(err(errtyps.Connection_Error, "Connection to " + murl + " failed", response));
                },
                onload: async function(response) {
                    let tex = response.responseText;
                    let res = tex.matchAll(/gmi-groupid="(\d+)"/gi);

                    for (let entr of res) {
                        listedGroups.push(parseInt(entr[1]));
                    }
                    resolve(listedGroups);
                }
            });
        });

    }

    function requestAddSubmission(token, devID, folderID, groupID, type) { //async+callback //type =[collection,gallery]
        let macroFchanged = false;
        if (macroMode == 1) {
            if (macros[macroModeTarget].data.some(e => e.groupID === groupID)) {
                macros[macroModeTarget].data.forEach(function(el) {
                    if (el.groupID === groupID) {
                        el.folderID = folderID;
                        el.folderName = loadedFolders.get(folderID);
                    }
                }); //change folder of present group
                macroFchanged = true;
            } else { //don't add if included already
                macros[macroModeTarget].data.push({
                    folderName: loadedFolders.get(folderID),
                    folderID: folderID,
                    groupID: groupID,
                    type: type
                });
            }
            GM.setValue("macros", JSON.stringify(macros));
        }

        return new Promise(function(resolve, reject) {
            let dat = {
                "groupid": parseInt(groupID),
                "type": type.toString(),
                "folderid": parseInt(folderID),
                "deviationid": parseInt(devID),
                "csrf_token": token.toString()
            };
            if (macroMode == 1) { //don't submit while adding to macros
                resolve({ success: true, gname: groupNameById(groupID), fname: loadedFolders.get(folderID), fchanged: macroFchanged });
            } else {
                GM.xmlHttpRequest({
                    method: "POST",
                    url: "https://www.deviantart.com/_napi/shared_api/deviation/group_add",
                    headers: {
                        "accept": 'application/json, text/plain, */*',
                        "content-type": 'application/json;charset=UTF-8'
                    },
                    dataType: 'json',
                    data: JSON.stringify(dat),
                    onerror: function(response) {
                        response.gname = groupNameById(groupID);
                        reject(err(errtyps.Connection_Error, "Connection to https://www.deviantart.com/_napi/shared_api/deviation/group_add failed", response));
                    },
                    onload: async function(response) {
                        let resp = JSON.parse(response.responseText);
                        resp.gname = groupNameById(groupID);
                        resolve(resp);
                    }
                });
            }
        });
    }

    function playMacro(index) {
        macroMode = 2;
        let promises = [];
        let token = $("input[name=validate_token]").val();
        for (let d of macros[index].data) {
            promises.push(requestAddSubmission(token, devID, d.folderID, d.groupID, d.type));
        }
        Promise.all(promises).catch(err => {
            alert(macros[index].name + " Error!<br/>" + JSON.stringify(macros[index].data) + " " + JSON.stringify(err), "Error");
        }).then(res => {
            myMsgBox(
                res.map(obj => {
                    let retval = "<strong>" + obj.gname + "</strong>: "
                    if (obj.success) {
                        retval += "Success! ";
                        if (obj.needsVote == true) retval += " Vote pending";
                    } else {
                        retval += "Error! ";
                        if (obj.errorDetails) retval += obj.errorDetails;
                    }
                    return retval;
                }).join("<br/>"), "Play Macro " + macros[macroModeTarget].name);
        })
        macroMode = 0;
    }
    //event handlers
    function Ev_groupClick(event) { //event propagation
        event.stopPropagation();

        let targetBut = $(event.target).closest(".dgl2_groupButton");
        let groupID = targetBut.attr("groupID");
        let groupNam = targetBut.attr("groupname");
        //targetName="";

        if (groupID == "undefined") {
            grabIDfromPage(groupNam).then(id => {
                $(event.target).closest(".dgl2_groupButton").attr("groupID", id);
                Ev_groupClick(event);
            });
            return;
        }
        let elInd;
        switch (colMode) {
            case 1: //add
                elInd = collections[colModeTarget].groups.indexOf(groupID);
                if (elInd == -1) {
                    collections[colModeTarget].groups.push(groupID);
                    GM.setValue("collections", JSON.stringify(collections));
                    targetBut.addClass("dgl2_inCollection");
                    targetName = targetBut.attr("groupName");
                }
                break;
            case 2: //remove
                targetName = collections[colModeTarget].name;
                switch (colListMode) {
                    case 0: //collection
                        elInd = collections[colModeTarget].groups.indexOf(groupID);
                        if (elInd > -1) {
                            collections[colModeTarget].groups.splice(elInd, 1);
                            GM.setValue("collections", JSON.stringify(collections));
                        }
                        insertFilteredGroups(colModeTarget);
                        break;
                    case 1: //macro
                        for (elInd = 0; elInd < macros[macroModeTarget].data.length; ++elInd) {
                            if (macros[macroModeTarget].data[elInd].groupID == groupID) break;
                        }

                        if (elInd < macros[macroModeTarget].data.length) {
                            macros[macroModeTarget].data.splice(elInd, 1);
                            GM.setValue("macros", JSON.stringify(macros));
                        }
                        insertMacroGroups(macroModeTarget);
                        break;
                }
                break;
            case 0:
            default:
                if (targetBut.attr("type") == "group") {
                    lastGroupClickID = targetBut.attr("groupID");
                    fillSubFolder(groupID, "gallery", groupNam);
                    if (macroMode != 1) {
                        targetName = targetBut.attr("groupName");
                    } else {

                    }
                    displayModeText();
                    //Add this deviation to a group folder
                } else if (targetBut.attr("type") == "folder") {
                    let token = $("input[name=validate_token]").val();
                    requestAddSubmission(token, devID, targetBut.attr("folderID"), targetBut.attr("groupID"), targetBut.attr("folderType")).then(function(arg) {
                        if (arg.success == true) {
                            if (macroMode == 1) {
                                if (arg.fchanged) {
                                    myMsgBox(arg.gname + " target folder changed to " + arg.fname, "Info");
                                } else {
                                    myMsgBox(arg.gname + "/" + arg.fname + " added to macro", "Info");
                                }
                                insertMacros(); //update titles
                                insertGroups(); //go back to groups view

                                $("div.dgl2_groupWrapper").addClass("dgl2_addGroup");
                                displayModeText();
                                //$("span.dgl2_descr").text("macro " + macros[macroModeTarget].name + " is recording.");
                                for (let el of macros[macroModeTarget].data) {
                                    $("button.dgl2_groupButton[groupID=" + el.groupID + "]").addClass("dgl2_inCollection");
                                }
                                macroMode = 1;
                                let lastgrBut = $("button[groupid=" + lastGroupClickID + "]")
                                lastgrBut[0].scrollIntoView();
                                lastgrBut.addClass("shadow-pulse");
                            } else {
                                let retfun = function() {
                                    $("span.dgl2_titleText").click();
                                    let lastgrBut = $("button[groupid=" + lastGroupClickID + "]")
                                    lastgrBut[0].scrollIntoView();
                                    lastgrBut.addClass("shadow-pulse");
                                };
                                if (arg.needsVote) {
                                    myMsgBox("Success! Submission pending group's vote", "Info").then(retfun);
                                } else {
                                    myMsgBox("Success! Submission added to group", "Info").then(retfun);
                                }
                            }

                        } else {
                            throw arg;
                        }
                        /*
                        deviationGroupCount: 1
                        needsVote: true
                        */
                    }).catch(function(arg) {
                        let tx = "deviation-ID: " + devID + "<br/>" +
                            "Group-Name: " + (arg.gname ? arg.gname : "Unknown") + "<br/>" +
                            (arg.errorDescription ? arg.errorDescription : "Unexpected error.") + "<br/>" +
                            (arg.errorDetails ? arg.errorDetails : JSON.stringify(arg))
                        let errg = err(errtyps.Unknown_Error, tx, arg);
                        errorHndl(errg);
                    });
                }
        }
        displayModeText();
    }

    function Ev_ContextSubmit(event) {
        event.stopPropagation();
        event.preventDefault();
        event.target = $("#dgl2_grContext select option:selected").get(0);
        Ev_groupClick(event);
        $("#dgl2_grContext").hide().find("select").empty();
    }

    function Ev_groupContext(event) {
        event.stopPropagation();
        event.preventDefault();
        let el = $("#dgl2_grContext");
        if (el.length == 0) {
            el = $("<div id='dgl2_grContext'><span class='desc'>Submit to a Folder</span><br /><select size=5></select><br/><button>Submit</button></div>").appendTo(document.body);
            el.find("button").click(Ev_ContextSubmit);
        }
        el.find("select").hide();
        el.finish().show().css({
            top: event.pageY + "px",
            left: event.pageX + "px"
        });
        let groupID = $(event.target).closest("button.dgl2_groupButton").attr("groupid")
        fillSubFolder(groupID, "gallery", $(event.target).closest("button.dgl2_groupButton").attr("groupname")).then(function() {
            el.find("select").show().focus().get(0).selectedIndex = 0;
        });

    }

    function switchColList() {
        switch (colListMode) {
            case 0:
                colListMode = 1;
                $("span.dgl2_CollTitle").html("Macros");
                insertMacros();
                break;
            case 1:
                $("span.dgl2_CollTitle").html("Collections");
                colListMode = 0;
                insertCollections();
        }
    }

    function highlightLetter(which) {

        $(".dgl2_letterfound").removeClass("dgl2_letterfound");
        $(".dgl2_groupdialog button.dgl2_groupButton[groupName^='" + which + "' i]").addClass("dgl2_letterfound").focus();
        $(".dgl2_groupdialog button.dgl2_groupButton[folderName^='" + which + "' i]").addClass("dgl2_letterfound").focus();
    }

    function Ev_colListClick(event) {
        event.stopPropagation();

        if ($(event.target).closest(".dgl2_CollTitleBut").length > 0) {
            switchColList();
        }
        let id = $(event.target).closest("li").first().attr("colID");
        if (typeof id == "undefined" && $(event.target).closest("button").hasClass("dgl2_topBut")) id = 0;
        else if (typeof id == "undefined") return;
        let clasNam = $(event.target).closest("button").attr("class");
        let index;
        if (colListMode == 0) index = colIndexById(id);
        else if (colListMode == 1) index = makIndexById(id);
        $("div.dgl2_groupWrapper").removeClass("dgl2_addGroup").removeClass("dgl2_remGroup");
        $("button.dgl2_inCollection").removeClass("dgl2_inCollection");
        let el;
        let obj, dat;
        let d = new Date();
        let con;
        let nam;
        targetName = "";

        if (clasNam) clasNam = clasNam.replace(" dgl2_topBut", "");
        switch (clasNam) {
            case "dgl2_export":
                obj = {
                    collections: collections,
                    collectionOrder: collectionOrder,
                    macros: macros,
                    macroOrder: macroOrder
                };
                dat = d.getFullYear() + ("0" + d.getMonth()).slice(-2) + ("0" + d.getDate()).slice(-2) + "-" + ("0" + d.getHours()).slice(-2) + ("0" + d.getMinutes()).slice(-2) + ("0" + d.getSeconds()).slice(-2);
                download(JSON.stringify(obj), "dev_group_list2_data_" + dat + ".txt");
                break;
            case "dgl2_import":
                upload().then(function(imp) {
                    try {
                        let i;
                        let obj = JSON.parse(imp);
                        if (obj.macros && obj.macroOrder) {
                            macros = obj.macros;
                            macroOrder = obj.macroOrder;
                        }
                        if (obj.collections && obj.collectionOrder) {
                            collections = obj.collections;
                            collectionOrder = obj.collectionOrder;
                        } else if (typeof obj[0] != "undefined" && (obj[0][0].indexOf("_collist") != -1 || obj[1][0].indexOf("_collist") != -1)) { //v1 compatibility mode
                            collections = [{
                                id: 0,
                                name: "all",
                                groups: [],
                                showing: 1
                            }];
                            let ind = (obj[0][0].indexOf("_collist") != -1) ? 0 : ((obj[1][0].indexOf("_collist") != -1) ? 1 : -1)
                            let oldList = obj[ind][1].split("\u0002");
                            let coll = oldList.map((list, ind) => {
                                let entries = list.split("\u0001");
                                let nam = entries.shift();
                                return {
                                    id: ind + 1,
                                    name: nam,
                                    groups: entries.map(el => {
                                        return $("button[groupname='" + el + "']").attr("groupid");
                                    }).filter(el => typeof el != "undefined"),
                                    showing: 1
                                }
                            });
                            collections = collections.concat(coll).sort((a, b) => a.id > b.id);
                            collectionOrder = collections.map(col => "dgl2item-" + col.id);
                        } else {
                            throw "No collections found!";
                        }
                        //clean up old groups not beeing a member of anymore
                        for (i in macros) {
                            macros[i].data = macros[i].data.filter(el => { return groupNameById(el.groupID) != "" });
                        }
                        for (i in collections) {
                            collections[i].groups = collections[i].groups.filter(el => { return groupNameById(el) != "" });
                        }
                    } catch (ex) {
                        errorHndl(err(errtyps.Parse_Error, "Not a valid dev_group_list2 file", ex));
                        return;
                    }

                    GM.setValue("collectionOrder", JSON.stringify(collectionOrder));
                    GM.setValue("collections", JSON.stringify(collections));
                    GM.setValue("macroOrder", JSON.stringify(macroOrder));
                    GM.setValue("macros", JSON.stringify(macros));
                    myMsgBox("Import successfull!", "Info");
                    insertGroups();
                    insertCollections();
                    if (colListMode != 0) switchColList();
                });
                break;
            case "dgl2_add":
                insertGroups();
                switch (colListMode) {
                    case 0: //collection

                        targetName = collections[index].name;
                        //$("span.dgl2_descr").text("Add groups to the collection " + collections[index].name);
                        colMode = 1;
                        colModeTarget = index;
                        $("div.dgl2_groupWrapper").addClass("dgl2_addGroup");
                        for (el of collections[index].groups) {
                            $("button.dgl2_groupButton[groupID=" + el + "]").addClass("dgl2_inCollection");
                        }
                        displayModeText();
                        break;
                    case 1:
                        colMode = 0;
                        insertGroups();
                        $("div.dgl2_groupWrapper").addClass("dgl2_addGroup");
                        targetName = macros[index].name;
                        //$("span.dgl2_descr").text("macro " + macros[index].name + " is recording.");
                        for (el of macros[index].data) {
                            $("button.dgl2_groupButton[groupID=" + el.groupID + "]").addClass("dgl2_inCollection");
                        }
                        macroMode = 1;
                        macroModeTarget = index;
                        displayModeText();
                        break;
                }
                break;
            case "dgl2_sub":
                switch (colListMode) {
                    case 0: //collection
                        insertFilteredGroups(index);
                        targetName = collections[index].name;
                        //$("span.dgl2_descr").text("Remove groups from the collection " + collections[index].name);
                        colMode = 2;
                        colModeTarget = index;
                        $("div.dgl2_groupWrapper").addClass("dgl2_remGroup");
                        displayModeText();
                        break;
                    case 1: //macro
                        insertMacroGroups(index);
                        targetName = macros[index].name;
                        // $("span.dgl2_descr").text("Remove groups from the macro " + macros[index].name);
                        colMode = 2;
                        macroMode = 3;
                        macroModeTarget = index;
                        $("div.dgl2_groupWrapper").addClass("dgl2_remGroup");
                        displayModeText();
                        break;
                }
                break;
            case "dgl2_del":
                switch (colListMode) {
                    case 0: //collection
                        myMsgBox("Delete Collection " + collections[index].name + " ?", "Collection", 1).then(con => {
                            if (!con) return;
                            collections.splice(index, 1);
                            collectionOrder.splice(collectionOrder.indexOf("dgl2item-" + id), 1);

                            GM.setValue("collectionOrder", JSON.stringify(collectionOrder));
                            GM.setValue("collections", JSON.stringify(collections));

                            insertGroups();
                            insertCollections();
                        });
                        break;
                    case 1: //macro
                        myMsgBox("Delete Macro " + macros[index].name + " ?", "Macro", 1).then(con => {
                            if (!con) return;
                            macros.splice(index, 1);
                            macroOrder.splice(macroOrder.indexOf("dgl2item-" + id), 1);

                            GM.setValue("macroOrder", JSON.stringify(macroOrder));
                            GM.setValue("macros", JSON.stringify(macros));

                            insertGroups();
                            insertMacros();
                        });
                        break;
                }
                break;
            case "dgl2_new":
                switch (colListMode) {
                    case 0: //collection
                        el = {
                            name: "New Collection",
                            groups: [],
                            showing: 1
                        };
                        el.id = getLowestFree(collections);
                        collections.push(el);
                        collectionOrder.push("dgl2item-" + el.id);

                        GM.setValue("collectionOrder", JSON.stringify(collectionOrder));
                        GM.setValue("collections", JSON.stringify(collections));

                        insertGroups();
                        insertCollections();
                        break;
                    case 1: //macros
                        el = {
                            name: "New Macro",
                            data: []
                        };
                        el.id = getLowestFree(macros);
                        macros.push(el);
                        macroOrder.push("dgl2item-" + el.id);

                        GM.setValue("macroOrder", JSON.stringify(macroOrder));
                        GM.setValue("macros", JSON.stringify(macros));

                        insertGroups();
                        insertMacros();
                        break;
                }
                break;
            case "dgl2_visible":

                switch (colListMode) {
                    case 0: //collection
                        if (!collections[index].hasOwnProperty("showing")) collections[index].showing = 0;
                        else collections[index].showing = 1 - collections[index].showing; //toggle 0 and 1
                        GM.setValue("collections", JSON.stringify(collections));

                        $(event.target).closest("li").attr("active", collections[index].showing);
                        insertGroups();
                        break;
                    case 1: //macro
                        //donothing
                        break;
                }
                break;
            case "dgl2_edit":
                switch (colListMode) {
                    case 0: //collection
                        myMsgBox("Please enter a new collection name!", "Change Collection Name", 2, collections[index].name).then(nam => {
                            if (!nam) return;
                            collections[index].name = nam;
                            GM.setValue("collections", JSON.stringify(collections));
                            insertCollections();
                        });
                        break;
                    case 1: //macro
                        myMsgBox("Please enter a new macro name!", "Change Macro Name", 2, macros[index].name).then(nam => {
                            if (!nam) return;
                            macros[index].name = nam;
                            GM.setValue("macros", JSON.stringify(macros));
                            insertMacros();
                        });
                        break;
                }
                break;
            case undefined:
            default:
                switch (colListMode) {
                    case 0: //collection
                        colMode = 0;
                        insertFilteredGroups(index);
                        break;

                    case 1: //macro
                        //$("span.dgl2_descr").text("Add this deviation to a group folder");
                        myMsgBox("Do you want to add this to the following groups?<br/>" + macros[index].data.map(obj => {
                            return groupNameById(obj.groupID);
                        }).join(", "), "Submit to Groups", 1).then(con => {
                            if (!con) {} else {
                                macroMode = 2;
                                playMacro(index);
                            }
                            displayModeText();
                        });
                        break;
                }

        }
        displayModeText();
    }

    function Ev_getGroupClick() {

        if (fetchingGroups) return;
        fetchingGroups = true;
        groups.forEach((el) => {
            if (el.userId == 0) {
                el.userId = "undefined"
                el.useridUuid = "undefined"
            }
        });

        $("span.dgl2_descr").text("Loading Module ID...");
        $("#dgl2_refresh").css("cursor", "pointer");
        fillModuleID().then(function() {
            $("span.dgl2_descr").text("Loading List of Groups...");
            $("#dgl2_refresh").css("cursor", "wait");
            return fillGroups(0);
        }).then(function() {
            // $("span.dgl2_descr").text("Add this deviation to one of your groups");
            displayModeText();
        }).catch(function(e) {
            if (e.ErrType != null) errorHndl(e);
            else errorHndl(err(errtyps.Unknown_Error, "fillGroups error", e));
        }).finally(function() {
            fetchingGroups = false;
        });
    }
    //templates
    function getGroupTemplate(name, img, id, latestDate = null) { //return HTML string
        //return "<button groupID="+id+" type='group' groupName="+name+" class='_3UhBt _3PBqc _3PBqc dgl2_groupButton'><div class='_1cFkE _11Rtb _3Lbo4 dgl2_groupButDiv'><div class='_3_Bqs _1lUlZ'><div class='pqF_I 3tZow'><span class='_1X7Yj _14i4i _23Ekg _3q2EJ'><img data-hook='user_avatar' alt='"+name+"'s avatar' class='_19ZLc dIDzJ' src='"+img+"'></span></div><div class='_3po1a _3_Nxk'><span class='_3g6BC _2PY8v _3YKkm _1sm3t _3HH04 _3y1P2 _3kEx1 _32s2-'><svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'><path d='M4.75,3.25 L7,3.25 L7,4.75 L4.75,4.75 L4.75,7 L3.25,7 L3.25,4.75 L1,4.75 L1,3.25 L3.25,3.25 L3.25,1 L4.75,1 L4.75,3.25 Z'></path></svg></span></div></div><div class='_3j_sQ _22Jp8'>"+name+"</div></div></button>";
        return `<button title='${escapeHtml(name)}${(latestDate!=null)?"\nLast submission: "+(new Date(latestDate)).toLocaleString():""}' class='dgl2_groupButton' groupID=${id} type='group' groupName='${escapeHtml(name)}' activity='${latestDate==null?"unknown":((inactiveDate<new Date(latestDate))?"active":"inactive")}'>
				<div class='dgl2_imgwrap'>
					<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' class='dgl2_hover'>"
						<path d='M4.75,3.25 L7,3.25 L7,4.75 L4.75,4.75 L4.75,7 L3.25,7 L3.25,4.75 L1,4.75 L1,3.25 L3.25,3.25 L3.25,1 L4.75,1 L4.75,3.25 Z'></path>
					</svg>
					<img class='dgl2_group_image' src='${img}'/>
				</div>
				<div class='dgl2_groupName'>${escapeHtml(name)}</div>
			</button>`;
    }

    function getSubFolderOptionTemplate(name, devCnt, grID, foID, foType, img) {
        //text option only needs name,IDs and type
        return "<option class='dgl2_groupButton' groupID=" + grID + " folderName='" + escapeHtml(name) + "' folderID=" + foID + " folderType='" + foType + "' type='folder'>" + escapeHtml(name) + "</option>";
    }

    function getSubFolderTemplate(name, devCnt, grID, foID, foType, img) { //return HTML string
        loadedFolders.set("" + foID, name);
        //return "<button class='_3UhBt _3PBqc _3PBqc dgl2_groupButton' groupID="+grID+" folderName="+name+" folderID="+foID+" folderType='"+foType+"' type='folder'><div class='_26MiR _2yz_F'><div class='_3Q4xp'><div class='_1PQmd'><div role='img' aria-label='Suggestions unwatch fav' class='_2i5F4 _2m25r' style='width: 112px; height: 48px; background-position: center center; background-size: cover; background-repeat: no-repeat; background-image: url(&quot;"+img+"&quot;);'><noscript></noscript></div></div><div class='_2ghXZ'><span class='_3g6BC _2PY8v _3YKkm _1AjcI'><svg width='0' height='0' viewBox='0 0 8 8' xmlns='http://www.w3.org/2000/svg'><path d='M1.237 6.187L0 4.95l1.237-1.238L2.475 4.95l3.712-3.713 1.238 1.238-4.95 4.95-1.238-1.238z' fill-rule='evenodd'></path></svg></span></div></div><div class='_3a6pU'><div class='_1lEBY'>"+name+"</div><span class='_3_DY3'>"+devCnt+" deviations</span></div></div></button>";
        let imgstring;
        if (img.textContent) { //journal
            imgstring = "<p class='dgl2_journalSubF'>" + img.textContent.excerpt + "</p>";
        } else {
            let i;
            let cstr = "";
            img = img.media;
            for (i of img.types) {
                if (typeof i.c != "undefined") {
                    cstr = i.c;
                    break;
                }
            }
            if (cstr == "") {
                for (i of img.types) {
                    if (typeof i.s != "undefined") {
                        cstr = i.s;
                        break;
                    }
                }
            }
            if (img.baseUri) imgstring = img.baseUri;
            if (img.prettyName) imgstring += cstr.replace("<prettyName>", img.prettyName);
            if (img.token) imgstring += "?token=" + img.token[0];
            imgstring = "<img class='dgl2_group_image' title='" + escapeHtml(name) + "' src='" + imgstring + "'/>";
        }

        return "<button class='dgl2_groupButton' groupID=" + grID + " folderName='" + escapeHtml(name) + "' folderID=" + foID + " folderType='" + foType + "' type='folder'>" +
            "  <div class='dgl2_imgwrap'>" +
            "    <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' class='dgl2_hover'>" +
            "      <path d='M4.75,3.25 L7,3.25 L7,4.75 L4.75,4.75 L4.75,7 L3.25,7 L3.25,4.75 L1,4.75 L1,3.25 L3.25,3.25 L3.25,1 L4.75,1 L4.75,3.25 Z'></path>" +
            "    </svg>" +
            imgstring +
            "  </div>" +
            "  <div class='dgl2_groupName'>" + escapeHtml(name) + "</div>" +
            "  <div class='dgl2_devCnt'>" + devCnt + "</div>" +
            "</button>";
    }

    function getSearchBarTemplate() {
        return "<input id='dgl2_searchbar' type='text' placeholder='Search'/>";
    }

    function getCollectionColTemplate() {
        return "<div id='dgl2_CollTab'><div class='dgl2_CollTitleBut'>" + svgTurnArrow + "<span class='dgl2_CollTitle'>" +
            "Collections</span></div><div class='buttons'></div><ul class='sortableList'></ul></div>";
    }

    function getAddButTemplate() {
        let sty = getComputedStyle(document.body);
        return "<button title='Add group to collection/macro' class='dgl2_add'><svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='24' height='24' viewBox='0 0 172 172'>" +
            "	<g style='stroke:" + sty.color + ";stroke-width:15;'>" +
            "		<rect x='00' y='00' style='opacity:0.1' rx='50' ry='50' width='172' height='172'></rect>" +
            "		<line x1='86' y1='30' x2='86' y2='142' />" +
            "		<line x1='30' y1='86' x2='142' y2='86' />" +
            "	</g>" +
            "</svg></button>";
    }

    function getRecButTemplate() {
        let sty = getComputedStyle(document.body);
        return "<button title='Add groups to macro' class='dgl2_add'><svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='24' height='24' viewBox='0 0 172 172'>" +
            "	<g style='stroke:" + sty.color + ";stroke-width:15;'>" +
            "		<rect x='00' y='00' style='opacity:0.1' rx='50' ry='50' width='172' height='172'></rect>" +
            "		<ellipse cx='86' cy='86' rx='40' ry='40'></ellipse>" +
            "	</g>" +
            "</svg></button>";
    }

    function getNewColTemplate() {
        let sty = getComputedStyle(document.body);
        return "<button title='New collection/macro' class='dgl2_new dgl2_topBut'><svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='24' height='24' viewBox='0 0 172 172'>" +
            "	<g style='stroke:" + sty.color + ";stroke-width:15;'>" +
            "		<rect x='00' y='00' style='opacity:0.1' rx='50' ry='50' width='172' height='172'></rect>" +
            "		<line x1='86' y1='30' x2='86' y2='142' />" +
            "		<line x1='30' y1='86' x2='142' y2='86' />" +
            "	</g>" +
            "</svg></button>";
    }

    function getSubButTemplate() {
        let sty = getComputedStyle(document.body);
        return "<button title='Remove groups from collection/macro' class='dgl2_sub'><svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='24' height='24' viewBox='0 0 172 172'>" +
            "	<g style='stroke:" + sty.color + ";stroke-width:15;'>" +
            "		<rect x='00' y='00' style='opacity:0.1' rx='50' ry='50' width='172' height='172'></rect>" +
            "		<line x1='30' y1='86' x2='142' y2='86' />" +
            "	</g>" +
            "</svg></button>";
    }

    function getRefreshButTemplate() {
        let sty = getComputedStyle(document.body);
        return "<button  title='refresh list of groups' id='dgl2_refresh'><svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='24' height='24' viewBox='0 0 172 172' style=' fill:#000000;'><g fill='none' fill-rule='nonzero' stroke='none' stroke-width='1' stroke-linecap='butt' stroke-linejoin='miter' stroke-miterlimit='10' stroke-dasharray='' stroke-dashoffset='0' font-family='none' font-weight='none' font-size='none' text-anchor='none' style='mix-blend-mode: normal'><path d='M0,172v-172h172v172z' fill='none'></path>" +
            " <linearGradient id='dgl2_grad1' x1='0%' y1='100%' x2='0%' y2='0%'><stop id='dgl2_grad1_stop1' offset='0%' style='stop-color:rgb(0,255,0);stop-opacity:1' /><stop id='dgl2_grad1_stop2' offset='100%' style='stop-color:rgb(255,0,0);stop-opacity:1' /></linearGradient>" +
            "<rect x='00' y='00' style='stroke:" + sty.color + ";stroke-width:5;opacity:0.1' rx='50' ry='50' width='172' height='172'></rect>" +
            "<g fill='" + sty.color + "'><path d='M62.00062,36.98l7.99979,10.89333h21.44625c18.10591,0 32.68,14.57409 32.68,32.68v21.78667h-16.34l21.78667,29.78646l21.78667,-29.78646h-16.34v-21.78667c0,-23.99937 -19.57396,-43.57333 -43.57333,-43.57333zM42.42667,39.87354l-21.78667,29.78646h16.34v21.78667c0,23.99938 19.57396,43.57333 43.57333,43.57333h29.44604l-7.99979,-10.89333h-21.44625c-18.10591,0 -32.68,-14.57409 -32.68,-32.68v-21.78667h16.34z'></path></g><path d='M43.86,172c-24.22321,0 -43.86,-19.63679 -43.86,-43.86v-84.28c0,-24.22321 19.63679,-43.86 43.86,-43.86h84.28c24.22321,0 43.86,19.63679 43.86,43.86v84.28c0,24.22321 -19.63679,43.86 -43.86,43.86z' fill='none'></path><path d='M47.3,168.56c-24.22321,0 -43.86,-19.63679 -43.86,-43.86v-77.4c0,-24.22321 19.63679,-43.86 43.86,-43.86h77.4c24.22321,0 43.86,19.63679 43.86,43.86v77.4c0,24.22321 -19.63679,43.86 -43.86,43.86z' fill='none'></path></g></svg></button";
    }

    function getDelButTemplate() {
        let sty = getComputedStyle(document.body);
        return "<button title='Delete collection/macro' class='dgl2_del'><svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='24' height='24' viewBox='0 0 172 172'>" +
            "<g style='stroke-width:5;stroke:" + sty.color + ";fill:none'>" +
            "	<rect x='00' y='00' style='opacity:0.1' rx='50' ry='50' width='172' height='172'></rect>" +
            "	<rect x='50' y='50' rx='5' ry='5' width='72' height='92'></rect>" +
            "	<rect x='65' y='35' rx='5' ry='5' width='42' height='15'></rect>" +
            "  <line x1='40' y1='50' x2='132' y2='50'/>" +
            "  <line x1='70' y1='132' x2='70' y2='60'/>" +
            "  <line x1='86' y1='132' x2='86' y2='60'  />" +
            "  <line x1='104' y1='132' x2='104' y2='60' />" +
            "  </g>" +
            "</svg></button>";
    }

    function getExportButTemplate() {
        return '<button title="Export collection/macro list to file" class="dgl2_export dgl2_topBut"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 5.2916665 5.2916668">' +
            '   <g transform="translate(0,-291.70832)">' +
            '      <path style="fill:#008000;"' +
            '          d="M 0.26458332,291.9729 H 5.0270831 v 4.7625 H 0.79345641 l -0.52887309,-0.51217 z" />' +
            '      <rect style="fill:#ffffff;" width="3.7041667" height="1.8520833" x="0.79374999" y="292.23749" />' +
            '      <rect style="fill:#ffffff;" width="2.6458333" height="1.3229259" x="1.3229166" y="295.41248" />' +
            '      <rect style="fill:#008000;" width="0.52916676" height="0.79375702" x="2.9104166" y="295.67706" />' +
            '   </g>' +
            '</svg></button>';
    }

    function getImportButTemplate() {
        return '<button class="dgl2_import dgl2_topBut" title="Import collection/macro list from file" ><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 5.2916665 5.2916668">' +
            '	<g transform="translate(0,-291.70832)">' +
            '		<rect style="fill:#806600;" width="3.96875" height="2.9104137" x="0.52916664" y="293.03125" />' +
            '		<path style="fill:#ffcc00;" d="m 0.52916666,295.94165 0.79375004,-2.11666 h 3.96875 l -0.7937501,2.11666 z" />' +
            '		<rect style="fill:#00DD00;" width="0.52916664" height="1.0583333" x="3.4395833" y="292.50208" />' +
            '		<path style="fill:#00DD00;" d="m 3.175,292.50207 0.5291667,-0.52917 0.5291667,0.52917 z" />' +
            '	</g>' +
            '</svg></button>';
    }

    function getTitleBarTemplate() {
        return '<span class="dgl2_titleText">Add to Group</span>' +
            '<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill-rule="evenodd" d="M8.84210526,13 L8.84210526,21.1578947 L2,21.1578947 L2,9.57894737 L12,3 L22,9.57894737 L22,21.1578947 L15.1578947,21.1578947 L15.1578947,13 L8.84210526,13 Z"></path></svg>' +
            '<span class="dgl2_descr">Add this deviation to one of your groups</span>'
    }

    function getEditButTemplate() {
        return '<button title="Change collection/macro name" class="dgl2_edit"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.389 6.4503l-3-3-1.46-1.45-1.41 1.42-11.52 11.58-1 .97v6.03h5.987l1.013-1 11.41-11.76 1.39-1.41-1.41-1.38zm-4.45-1.62l3 3-.88.87-3-3 .88-.87zm.74 5.33l-8.21 8.2-2.801-3.0118 8.0028-8.099 3.0083 2.9108zm-12.68 9.84v-3.17l3.0433 3.17H3.9991z"></path></svg></button>';
    }

    function getVisibleButTemplate() {
        return `<button title="Hide/show groups within collection" class="dgl2_visible">
<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 500 250" stroke-width="20">
<defs>
	<radialGradient id="c1" cx="0.5" cy="0.5" r="0.5">
			<stop offset="0" stop-color="#ffffff" />
			<stop offset=".5" stop-color="hsl(40, 60%, 60%)" />
			<stop offset="1" stop-color="#3dff3d" />
	</radialGradient>
</defs>
<ellipse role="sclera" cx="250" cy="125" rx="200" ry="100" fill="white"/>
<ellipse role="iris" cx="250" cy="125" rx="95" ry="95" stroke="black" fill="url(#c1)"/>
<ellipse role="pupil" cx="250" cy="125" rx="50" ry="50" stroke="none" fill="black"/>
<ellipse role="light" cx="200" cy="80" rx="50" ry="50" stroke="none" fill="#fffffaee"/>
<ellipse role="outline" cx="250" cy="125" rx="200" ry="100" stroke="black" fill="none"/>
</svg>
</button>`;
    }

    function addCss() {
        if ($("#dgl2_style").length > 0) return;
        let style = $("<style type='text/css' id='dgl2_style'></style>");

        //searchbar
        style.append("#dgl2_searchbar{background: var(--L3);box-shadow: inset 0 1px 4px 0 rgba(0,0,0,.25);padding: 5px;width: 50%;}");

        //right collection column
        style.append(`
			#dgl2_CollTab{color: #2d3a2d;padding-left:15px; overflow-y: auto;font-family: CalibreSemiBold,sans-serif;font-weight: 600;font-size: 20px;font-display: swap;line-height: 24px;letter-spacing: .3px;margin-bottom: 28px;grid-row: 2;}
			#dgl2_CollTab ul{overflow-wrap: anywhere;overflow: auto;list-style: none;padding-left: 10px;margin-top: 20px;}
			#dgl2_CollTab ul li{cursor:pointer;padding:2px;display:grid;grid-template: auto/7px auto 16px 16px 16px 16px 16px;}
			#dgl2_CollTab ul li:hover{background:linear-gradient(to right, rgba(255,0,0,0.1), rgba(255,0,0,0));}
			#dgl2_CollTab button{cursor:pointer;border-width: 0;padding: 0;margin: 0;background-color: transparent;}
			#dgl2_CollTab button:hover rect{fill:red;user-select: none; }
			#dgl2_refresh{margin-left:auto;border-width:0px;background:transparent;cursor:pointer}
			#dgl2_CollTab div.buttons{display: inline-block;vertical-align: middle;margin: 0 5px;}
			div.dgl2_groupCol{overflow-y:auto;grid-row: 2;}
			div.dgl2_groupdialog{display: grid;position: fixed;top: 50%;left: 50%;transform: translate(-50%,-50%);height: 80%;background-color:#afcaa9;grid-template-columns: auto 300px;grid-template-rows: 50px auto;width: 80%;border: 2px solid #2a5d00;border-radius: 10px;box-shadow: 1px 1px 2px ;}
			div.dgl2_titlebar{cursor:move;display:flex;justify-content:space-between;grid-row: 1;grid-column: 1/3;background: linear-gradient(#619c32,#378201);color: white;align-items: center;}
div.dgl2_titlebar > * {margin: 7px;}
			#dgl2_refresh:hover rect{fill:red;}
div.dgl2_closeDiag{border-radius: 50px;padding: 7px;cursor: pointer;}
			#dgl2_refresh rect{fill:rgba(255,0,0,0.1);}
			#dgl2_CollTab ul li span.handle{vertical-align:middle; display:inline-block; width:5px; height:100%; cursor:move; text-align:center;background-color:#363; background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABZJREFUeNpi2r9//38gYGAEESAAEGAAasgJOgzOKCoAAAAASUVORK5CYII=);}
			button.dgl2_groupButton{border-radius:15px; background-color:rgba(255,255,255,0.5);margin:5px; padding:5px; width:120px; border-width:0px; overflow:hidden; position:relative; cursor:pointer; }
			div.dgl2_imgwrap{ position: relative;}
			svg.dgl2_hover{ position: absolute; left: 50%; width:50%; height:50%; transform: translate(-50%,50%); opacity:0; transition: ease 0.25s; }
			img.dgl2_group_image{ opacity:1; width:100px; height:50px; transition: ease 0.25s; border-radius:2px; }
			div.dgl2_groupName{ font-family: CalibreSemiBold; font-size: 15px; line-height: 15px; font-weight: 600; letter-spacing: 0.3px; word-break: break-word; }
			button.dgl2_groupButton:hover{background-color:rgba(255,255,255,0.8);}
			button.dgl2_groupButton:hover svg.dgl2_hover{opacity:1;}
			button.dgl2_groupButton:hover img.dgl2_group_image{opacity:0.3;}
			button.dgl2_groupButton:active{background-color:rgba(255,255,255,0.3);}
			button.dgl2_groupButton.dgl2_inGroup{background-image:linear-gradient(red, transparent);}
			span.dgl2_titleText{cursor:pointer;}
			span.dgl2_descr{font-family: CalibreRegular,sans-serif; font-weight: 400; font-size: 13px; font-display: swap; letter-spacing: 1.3px; margin-left: 32px; text-transform: uppercase;}
			button.dgl2_edit{height:0.5em;}
			button.dgl2_edit:hover path{fill:red;}
			#dgl2_CollTab button svg{width: 90%;}
			#dgl2_CollTab button.dgl2_visible:hover ellipse{stroke: red;}
			div.dgl2_addGroup{background-color: rgba(0, 255, 0, 0.3);}
			div.dgl2_remGroup{background-color: rgba(255, 0, 0, 0.3);}
			button.dgl2_inCollection{background-color: rgba(15, 104, 5, 0.7);}
			button.dgl2_export:hover ,button.dgl2_import:hover {opacity:0.8}
			button.dgl2_export:active ,button.dgl2_import:active {opacity:1}
			div.dgl2_CollTitleBut{cursor:pointer;display:inline-block;}
			div.dgl2_CollTitleBut:hover span{color:red;}
			.dgl2_journalSubF { overflow: hidden; height: 50px; font-size: xx-small; text-align: left; margin-bottom: 5px;}
			.groupPopup .ui-widget-content{background-color:#afcaa9 !important;color:black;}
			button.dgl2_letterfound {background-color: rgba(105, 14, 5, 0.7);}
			.folderInMacro {background-color:rgba(205, 24, 25, 0.6)!important;}
			@keyframes shadowPulse {0% {box-shadow: 0px 0px 50px 20px #f00;} 100% {box-shadow: 0px 0px 50px 20px #ff000000;}}
			.shadow-pulse {animation-name: shadowPulse;animation-duration: 0.5s;animation-iteration-count: infinite;animation-timing-function: linear; animation-direction:alternate;}
			#dgl2_grContext{display: none;z-index: 1000;position: absolute;overflow: hidden;white-space: nowrap;padding: 5px;background-color: #afcaa9;border-radius: 5px;border: 2px solid green;}
			#dgl2_grContext select{background: none; border: none;width:100%;margin:5px 0;}
			#dgl2_grContext select option{background-color: #ddffd8;}
			#dgl2_grContext select option:nth-child(even) {background-color: #6fd061;}
			#dgl2_grContext select option::selection {color: red;background: yellow;}
			#dgl2_grContext button {cursor:pointer; width: 100%;background-color: #408706;color: white;border: 1px outset black;border-radius: 5px;}
			#dgl2_grContext button:hover { background-color: #608706;}
			#dgl2_CollTab li[active='0'] button.dgl2_visible ellipse[role='iris'] { fill: lightgray;stroke:lightgray}
			#dgl2_CollTab li[active='0'] button.dgl2_visible ellipse { fill: lightgray;}
			#dgl2_CollTab li button{display: flex; height: 100%;align-items: center;}
#dgl2_CollTab li.dgl2_drgover{border-top: 2px solid blue;}
#dgl2_alertBox {color: #2d3a2d; z-index:7777;box-shadow: 1px 1px 2px black;position: fixed;top: 50%;left: 50%;background-color: #afcaa9;border-radius: 5px;border: 2px solid #285c00;transform: translate(-50%, -50%);}
#dgl2_alertBox .dgl2_alertTitle{cursor:move;background-color:#5d982d;color:white;font-weight: bold;}
#dgl2_alertBox div.dgl2_alertButtons div{padding: 5px;display: inline-block;border-radius: 5px;border: 1px solid;cursor: pointer;margin:5px;}
#dgl2_alertBox .dgl2_alertOKBut{background-color: #d0e8cb;}
#dgl2_alertBox .dgl2_alertCancelBut{background-color: #e8e3cb;}
#dgl2_alertBox>div{padding:5px;}
#dgl2_alertBox>div{padding:5px;}
#dgl2_alertBox div.dgl2_alertButtons{display: flex;flex-direction: row-reverse;}
#dgl2_promptVal{display: block;margin: 5px;border-radius: 5px;width: 90%;}
	button.dgl2_groupButton[activity="inactive"]{background-color:#666;}
`);

        // style.append(".noTitleDialog .ui-dialog-titlebar {display:none}");

        $("head").append(style);

        // $("head").append(
        //     '<link ' +
        //     'href="//ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/le-frog/jquery-ui.min.css" ' +
        //     'rel="stylesheet" type="text/css">'
        // );

    }

    //function from https://www.w3schools.com/howto/howto_js_draggable.asp
    //makes elements draggable
    function dragElement(elmnt) {
        let pos1 = 0,
            pos2 = 0,
            pos3 = 0,
            pos4 = 0;
        if (elmnt.querySelector(".dgl2_alertTitle")) {
            // if present, the header is where you move the DIV from:
            elmnt.querySelector(".dgl2_alertTitle").onmousedown = dragMouseDown;
        }
        if (elmnt.querySelector(".dgl2_titlebar")) {
            // if present, the header is where you move the DIV from:
            elmnt.querySelector(".dgl2_titlebar").onmousedown = dragMouseDown;
        } else {
            // otherwise, move the DIV from anywhere inside the DIV:
            elmnt.onmousedown = dragMouseDown;
        }

        function dragMouseDown(e) {
            e = e || window.event;
            if (e.target.tagName == "INPUT") return;

            e.preventDefault();
            // get the mouse cursor position at startup:
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            // call a function whenever the cursor moves:
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            // calculate the new cursor position:
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            // set the element's new position:
            elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
            elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
        }

        function closeDragElement() {
            // stop moving when mouse button is released:
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }
    //filling GUI DOM
    function hideHiddenGroups() {
        collections.forEach(col => {
            if (col.showing == 0) {
                col.groups.forEach(grID => {
                    $("button[groupID='" + grID + "']").hide();
                });
            }
        });
    }

    function insertFilteredGroups(id) {
        insertGroups();
        lastFilter = id;
        let allButs = $("button[type='group']");
        if (collections[id].groups.length == 0) {
            allButs.show();
        } else {
            allButs.hide();
            for (let grID of collections[id].groups) {
                $("button[groupID='" + grID + "']").show();
            }
        }
        hideHiddenGroups()
    }

    function insertMacroGroups(id) {
        insertGroups();
        lastFilter = id;
        let allButs = $("button[type='group']");
        if (macros[id].data.length == 0) {
            allButs.show();
        } else {
            allButs.hide();
            for (let grID of macros[id].data) {
                $("button[groupID='" + grID.groupID + "']").show();
            }
        }
    }

    function insertMacros() {
        let coltab = 0;
        let toAffect = $("div.dgl2_groupdialog").not("[dgl2]").attr("dgl2", 1);
        if (toAffect.length > 0) {
            coltab = $(getCollectionColTemplate());
            toAffect.append(coltab);
            coltab.find("div.buttons")
                .append(getNewColTemplate())
                .append(getExportButTemplate())
                .append(getImportButTemplate());

            coltab.click(Ev_colListClick);
        } else {
            coltab = $("#dgl2_CollTab");
        }
        let colList = coltab.find("ul");
        colList.empty();
        let el, descr;
        for (let col of macros) {
            descr = "";
            for (let gr of col.data) {
                descr += groupNameById(gr.groupID) + "/" + gr.folderName + "\n";
            }
            el = "<li colid=" + col.id + " title='" + escapeHtml(descr) + "' id='dgl2item-" + col.id + "'><span class='handle'></span><span>" + col.name + "</span>" + getEditButTemplate();
            el += getRecButTemplate() + getSubButTemplate() + getDelButTemplate();
            el += "</li>";
            colList.append(el);
        }
        if (macroOrder.length > 0) {
            $.each(macroOrder, function(i, position) {
                let $target = colList.find('#' + position);
                $target.appendTo(colList); // or prependTo for reverse
            });
        }


        makesortable();
    }

    function insertCollections() {
        let coltab = 0;
        let toAffect = $("div.dgl2_groupdialog").not("[dgl2]").attr("dgl2", 1); //group submission container
        if (toAffect.length > 0) {
            coltab = $(getCollectionColTemplate());
            toAffect.append(coltab);
            coltab.find("div.buttons")
                .append(getNewColTemplate())
                .append(getExportButTemplate())
                .append(getImportButTemplate());

            coltab.click(Ev_colListClick);
        } else {
            coltab = $("#dgl2_CollTab");
        }

        let colList = coltab.find("ul");
        colList.empty();
        let el;
        for (let col of collections) {
            el = `<li colid=${col.id} active='${col.showing}' id='dgl2item-${col.id}'><span class='handle'></span><span>${col.name}</span>${getEditButTemplate()}`;
            if (col.id > 0) el += getVisibleButTemplate() + getAddButTemplate() + getSubButTemplate() + getDelButTemplate();
            el += "</li>";
            colList.append(el);
        }
        if (collectionOrder.length > 0) {
            $.each(collectionOrder, function(i, position) {
                let $target = colList.find('#' + position);
                $target.appendTo(colList); // or prependTo for reverse
            });
        }

        makesortable();
    }

    function makesortable() {
        //ul.sortableList
        //li colid=col.id id='dgl2item-" + col.id,
        //<span class='handle'>

        let lists = document.querySelectorAll("ul.sortableList li");

        for (let i = 0; i < lists.length; ++i) {
            lists[i].draggable = "true";
            addDragHandler(lists[i]);
        }

    }

    //drag handler for drag-sortable lists
    function addDragHandler(entry) {
        entry.addEventListener('dragstart', function(e) {
            e.dataTransfer.setData('text/plain', this.id);
            e.dataTransfer.effectAllowed = 'move';
        }, false);
        // entry.addEventListener('dragenter', function(e){}, false)
        entry.addEventListener('dragover', function(e) {
            if (e.preventDefault) {
                e.preventDefault();
            }
            this.classList.add('dgl2_drgover');
            e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object.
            return false;
        }, false);
        entry.addEventListener('dragleave', function(e) {
            this.classList.remove('dgl2_drgover');
        }, false);
        entry.addEventListener('drop', function(e) {
            var dropHTML = e.dataTransfer.getData('text/plain');
            this.parentNode.insertBefore(document.querySelector("#" + dropHTML), this);
            this.classList.remove('dgl2_drgover');

            if (colListMode == 0) {
                collectionOrder = [...this.parentNode.querySelectorAll("[draggable]")].map(el => el.id);
                GM.setValue("collectionOrder", JSON.stringify(collectionOrder));
            } else if (colListMode == 1) {
                macroOrder = [...this.parentNode.querySelectorAll("[draggable]")].map(el => el.id);
                GM.setValue("macroOrder", JSON.stringify(macroOrder));
            }
            return false;
        }, false);
        entry.addEventListener('dragend', function(e) {
            this.classList.remove('dgl2_drgover');
        }, false);
    }

    function insertSearchBar() {
        let bar = $(getSearchBarTemplate());
        let refrBut = $(getRefreshButTemplate());

        $("div.dgl2_titlebar").append(refrBut).append(bar).append(
            $("<div class='dgl2_closeDiag'>X</div>").click(function() {
                $("div.dgl2_groupdialog").hide();
            })
        );

        bar.keyup(function() {
            let search = $(this).val();
            let allButs = $("button[type='group']").show();
            allButs.filter(function() {
                if (lastFilter > 1 && collections[lastFilter].groups.indexOf($(this).attr("groupID")) == -1) return 1;
                if (search == "") return 0;
                let words = search.split(" ");
                for (let word of words) {
                    if ($(this).attr('groupName').toLowerCase().indexOf(word) == -1) return 1;
                }
                return 0;
            }).hide();
        });
        //bar.mousedown(function(event){event.stopPropagation(); event.preventDefault();event.target.focus();});

        refrBut.click(Ev_getGroupClick);

        $("span.dgl2_titleText").click(function() {
            if (colListMode == 0) {
                insertGroups();
                $("li[colid=" + lastFilter + "]").click();

                let lastgrBut = $("button[groupid=" + lastGroupClickID + "]");
                lastgrBut[0].scrollIntoView();
                lastgrBut.addClass("shadow-pulse");
            } else {
                macroMode = 0; //abort macro mode add/remove
                insertGroups();
                displayModeText();
                $("div.dgl2_groupWrapper").removeClass("dgl2_addGroup").removeClass("dgl2_remGroup");
                $("button.dgl2_inCollection").removeClass("dgl2_inCollection");
            }
        });
    }

    function pulsing(element) {
        element.animate({ opacity: 0 }, 250, function() {
            $(this).animate({ opacity: 1 }, 250, pulsing);
        });
    }

    function insertSubFolders(subfolders) { //fill view with subfolders //subfolders not stored, request when needed
        let buts = $("button.dgl2_groupButton"); //button wrapper
        subfolders.sort(function(l, u) {
            return l.name.toLowerCase().localeCompare(u.name.toLowerCase());
        });

        if (buts.length > 0) {
            let subf;
            let newBut;
            if ($("#dgl2_grContext").is(":visible")) {
                let cont = $("#dgl2_grContext select");
                let newopt;
                cont.empty();
                for (subf of subfolders) {
                    if (subf.thumb == null) continue; //no thumb=db problem, so also no text entry
                    newBut = $(getSubFolderOptionTemplate(subf.name, subf.size, subf.owner.userId, subf.folderId, subf.type, subf.thumb));
                    cont.append(newBut);
                }
                if (subfolders.length == 0) {
                    $("#dgl2_grContext span.desc").html("This group does<br/>not allow submissions<br/>using the gallery<br/>system!");
                } else {
                    $("#dgl2_grContext span.desc").text("Submit to a Folder");
                }

            } else {
                let par = buts.first().parent();
                par.empty();
                for (subf of subfolders) {
                    if (subf.thumb == null) continue;
                    newBut = $(getSubFolderTemplate(subf.name, subf.size, subf.owner.userId, subf.folderId, subf.type, subf.thumb));
                    par.append(newBut);
                }
                if (subfolders.length == 0) {
                    par.append("This group does not allow submissions using the gallery system!");
                }
                par.not("[dgl2]").attr("dgl2", 1).click(Ev_groupClick);
            }
        }
    }

    function displayModeText() {
        let titl = "Add to Group";
        let descr = "Add this deviation to one of your groups";
        if (targetName != "") {
            if (colListMode == 0) { //colection
                if (colMode == 0) { //show
                    titl = "< Add to " + targetName;
                    descr = "Add this deviation to Group " + targetName;
                } else if (colMode == 1) { //add
                    titl = "< Add to " + targetName;
                    descr = "Add groups to Collection " + targetName;
                } else if (colMode == 2) { //remove
                    titl = "< Remove from " + targetName;
                    descr = "Remove groups from Collection " + targetName;
                }
            } else { //macros
                if (macroMode == 0) {} else if (macroMode == 1) {
                    titl = "< Add to Macro";
                    descr = "macro " + targetName + " is recording.";
                } else if (macroMode == 3) { //remove
                    titl = "< Remove from Macro";
                    descr = "remove groups from " + targetName;
                }
            }
        }
        $("span.dgl2_titleText").text(titl);
        $("span.dgl2_descr").text(descr);
    }

    function insertGroups() { //fill view with groups //groups are stored
        lastFilter = 0;
        groups.sort(function(l, u) {
            return l.username.toLowerCase().localeCompare(u.username.toLowerCase());
        });

        displayModeText();

        let par = $("div.dgl2_groupWrapper"); //group list wrapper
        let newBut;
        let hasEmptyId = false;
        par.empty();
        for (let gr of groups) {
            newBut = $(getGroupTemplate(gr.username, gr.usericon, gr.userId, gr.latestDate));
            newBut.contextmenu(Ev_groupContext);
            if (typeof gr.userId == "undefined") hasEmptyId = true;
            if (listedGroups.includes(parseInt(gr.userId))) {
                newBut.addClass("dgl2_inGroup"); //button div inside wrapper; used in template
            }
            if (par.find("div[groupName='" + gr.username + "']").length == 0) {
                par.append(newBut);
            }
        }
        par.not("[dgl2]").attr("dgl2", 1).click(Ev_groupClick);
        hideHiddenGroups();

        if (hasEmptyId && !notScanThisInstance) {
            requestAllGroupIDs();
        }
    }

    function uniqBy(a, key) {
        let seen = new Set();
        return a.filter(item => {
            let k = key(item);
            return seen.has(k) ? false : seen.add(k);
        });
    }
    let ngrpCnt = 0;
    let fetchItMsg = ""
    let ngrpleft = 0;

    function delayIteratefetchGrId(groups, index, delay) {
        if (index < groups.length) {
            grabIDfromPage(groups[index].username).catch(() => {
                if (fetchItMsg != "") fetchItMsg += ", ";
                fetchItMsg += groups[index].username;
                groups[index].userId = 0;
                groups[index].useridUuid = 0;
                GM.setValue("groups", JSON.stringify(groups));
            }).finally(() => {
                setTimeout(function() { delayIteratefetchGrId(groups, index + 1, delay); }, delay);
            });
        } else {
            if (fetchItMsg != "") myMsgBox("failed fetching IDs for groups: " + fetchItMsg + "<br/>They might be deleted.")
            $("#dgl2_refresh rect").css("fill", "");
            $("#dgl2_refresh").css("cursor", "pointer");
            //$("span.dgl2_descr").text("Add this deviation to a group folder");
            displayModeText();
        }
    }

    function requestAllGroupIDs() {
        let nullgr = groups.filter(gr => typeof gr.userId == "undefined");
        let remtim = nullgr.length * 1.1;
        let remtex = ""
        ngrpleft = nullgr.length;
        if (remtim < 60) { remtex = "seconds" } else if (remtim >= 60 && remtim < 60 * 60) {
            remtex = "minutes";
            remtim /= 60.0;
        } else if (remtim >= 60 * 60) {
            remtex = "hours";
            remtim /= 60.0 * 60.0;
        }
        myMsgBox(`${ngrpleft} group-IDs are not fetched.<br/>Fetching all group IDs now might take a while (est. ${Math.round((remtim + Number.EPSILON) * 100) / 100} ${remtex}).<br/>If you press "cancel" IDs are fetched dynamically when group-buttons are clicked.<br/>Collections, macros and list of already submitted deviations can not display groups without ID.<br/><br/>Fetch all remaining group-IDs now?`, "Fetch Group-IDs", 1).then((choice) => {
            if (choice) {
                ngrpCnt = 0;
                fetchItMsg = "";
                delayIteratefetchGrId(nullgr, 0, 250);
            }
        });
        notScanThisInstance = true;
    }

    function insertHTML() {

        if ($("div.dgl2_groupdialog").length > 0) return;

        addCss();
        $("<div class='dgl2_groupdialog'><div class='dgl2_titlebar'></div><div class='dgl2_groupCol'><div class='dgl2_groupWrapper'></div></div></div>").appendTo($("body"));
        $("div.dgl2_titlebar").html(getTitleBarTemplate());
        insertSearchBar();

        let devInd = location.href.indexOf("?");
        if (devInd == -1) {
            devID = location.href.match(/(\d+)\D*$/)[1];
        } else {
            devID = location.href.substring(0, devInd).match(/(\d+)\D*$/)[1];
        }

        userName = $("a.user-link").attr("data-username"); // "dediggefedde";

        let proms = [
            GM.getValue("collections", ""),
            GM.getValue("collectionOrder", ""),
            GM.getValue("macros", ""),
            GM.getValue("macroOrder", "")
        ];

        Promise.all(proms).then(([cols, colOrder, macs, macOrder]) => {
            if (cols != "") {
                collections = JSON.parse(cols);
            }
            collections.forEach(el => { if (!el.hasOwnProperty("showing")) { el.showing = 1; }; }); //backward-compatibility for collection-showing attribute before v3.0

            if (colOrder != "") {
                collectionOrder = JSON.parse(colOrder);
            }

            if (macs != "") {
                macros = JSON.parse(macs);
                macros.forEach(function(el) { el.data = uniqBy(el.data, JSON.stringify); }); //unique macros
            }

            if (macOrder != "") {
                macroOrder = JSON.parse(macOrder);
            }
            insertCollections();
        }).catch(function(e) {
            errorHndl(err(errtyps.Unknown_Error, "Error Loading Database", e));
            return insertCollections();
        });

        fillListedGroups(devID).then(function() {
            return GM.getValue("groups", "");
        }).then(function(grps) {
            if (grps == "") {
                Ev_getGroupClick();
            } else {
                groups = JSON.parse(grps);
                insertGroups();
            }
        }).catch(function(e) {
            errorHndl(err(errtyps.Unknown_Error, "fillListedGroups error", e));
        });

    }

    //outdated, now I'm using my own template
    //in case classNames (hopefully) change sometime. Also necessary since browsing=other class names than refreshing site.
    // function classNormalizer() { //_2n5r3 _3Isw- _3yw1l _1F3vX _34hvg
    //     // if($("div.dgl2_groupdialog").length>0)return;
    //     $("<div class='dgl2_groupdialog'><div class='dgl2_groupCol'><div class='dgl2_titlebar'></div><div class='dgl2_groupWrapper'></div></div></div>").appendTo($("body"));

    //     // $("div.dgl2_groupdialog div._13bQf").hide();
    //     // $("div.dgl2_groupdialog button._2-StF").hide();

    //     // $("span.u1km9, span._3M1Kg").addClass("dgl2_descr"); //refr u1km9 browse _3M1Kg //top description //added by template
    //     // $("button._3PBqc, button._3UhBt").addClass("dgl2_groupButton"); //refr _3UhBt _3PBqc, browse _3PBqc //button to add to group //added by template
    //     // $("div._1cFkE, div._3Lbo4").addClass("dgl2_groupButDiv"); //refr _1cFkE _11Rtb, browse _3Lbo4 _3EbZ8 //div inside button //outdated
    //     // $("div.A9uFL, div.JcCFR").addClass("dgl2_groupWrapper"); //refr A9uFL _2IeoY, browse JcCFR _159Ra //div parent of group buttons
    //     // $("div._1HNK0, div._1ezbx").addClass("dgl2_titlebar"); //refr _1HNK0, browse _1ezbx //title bar
    //     //  $("div._2n5r3, div._2qgZz").addClass("dgl2_groupdialog"); //refr _2n5r3, browse _2qgZz // dialog(opening div) for group submission
    //     // $("div._2UK-E, div._2kU4L").addClass("dgl2_groupCol"); //refr _2UK, browse _2kU4L//left column (containing groups)


    //     //         if($("div.dgl2_titlebar").length==0 && $("div.dgl2_groupCol:not([nogroups])").length>0){
    //     //             $("div.dgl2_groupCol").attr("nogroups",1).html('<div class="_2UK-E"><div class="_38jjU"><div class="_1HNK0">Add to Group<span class="_3g6BC _1Re00 _3YKkm _1d2qE"><svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill-rule="evenodd" d="M8.84210526,13 L8.84210526,21.1578947 L2,21.1578947 L2,9.57894737 L12,3 L22,9.57894737 L22,21.1578947 L15.1578947,21.1578947 L15.1578947,13 L8.84210526,13 Z"></path></svg></span><span class="u1km9">Add this deviation to one of your groups</span></div><div class="_2ZROG"><div class="A9uFL _2IeoY"><button class="_3UhBt"><div class="_1cFkE _11Rtb"><div class="_3_Bqs"><div class="pqF_I"><span class="_1X7Yj _14i4i"><img alt="NatureNation\'s avatar" data-hook="user_avatar" class="_19ZLc" src="https://a.deviantart.net/avatars-big/n/a/naturenation.jpg?2" loading="lazy"></span></div><div class="_3po1a"><span class="_3g6BC _2PY8v _3YKkm _1sm3t"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><path d="M4.75,3.25 L7,3.25 L7,4.75 L4.75,4.75 L4.75,7 L3.25,7 L3.25,4.75 L1,4.75 L1,3.25 L3.25,3.25 L3.25,1 L4.75,1 L4.75,3.25 Z"></path></svg></span></div></div><div class="_3j_sQ">NatureNation</div></div></button></div></div></div></div>');
    //     //             classNormalizer();
    //     //         }

    // }

    function getLowestFree(collection) {
        collection.sort(function(a, b) {
            return a.id - b.id;
        }); //changing order does not matter thanks to index/order array
        let lowest = -1;
        let i;
        for (i = 0; i < collection.length; ++i) {
            if (collection[i].id != i) {
                lowest = i;
                break;
            }
        }
        if (lowest == -1 && collection.length > 0) {
            lowest = collection[collection.length - 1].id + 1;
        } else if (collection.length == 0) lowest = 0;
        return lowest;

    }

    function escapeHtml(string) {
        return String(string).replace(/[&<>"'`=\/]/g, function(s) {
            return entityMap[s];
        });
    }

    function download(data, filename) {
        let file = new Blob([data], {
            type: "application/json"
        });
        let a = document.createElement("a"),
            url = URL.createObjectURL(file);
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        setTimeout(function() {
            document.body.removeChild(a);
            window.URL.revokeObjectURL(url);
        }, 0);
    }

    function upload() {
        return new Promise(function(resolve, reject) {
            let inp = $('<input type="file" id="input">').appendTo("body").click()
            inp.change(function() {
                let reader = new FileReader();
                reader.onload = function(evt) {
                    resolve(evt.target.result);
                };
                reader.readAsBinaryString($(this).prop("files")[0]);
            });
            return "";
        });
    }

    function colIndexById(id) {
        for (let i = 0; i < collections.length; ++i) {
            if (collections[i].id == id) return i;
        }
        return -1;
    }

    function makIndexById(id) {
        for (let i = 0; i < macros.length; ++i) {
            if (macros[i].id == id) return i;
        }
        return -1;
    }

    //shows an alert box with text
    //mode 0:alert, 1:confirm, 2 prompt
    function myMsgBox(tex, titl = "Notification", mode = 0, defText = "") {
        let dfd = new $.Deferred();

        let box = $("#dgl2_alertBox");
        if (box.length == 0) {
            box = $("<div id='dgl2_alertBox'></div>").appendTo("body");
        }

        box.html("<div class='dgl2_alertTitle'></div><div class='dgl2_alertText'></div><div class='dgl2_alertButtons'></div>");
        box.find("div.dgl2_alertText").html(tex);
        box.find("div.dgl2_alertTitle").html(titl);

        if (mode == 0) {
            $("<div class='dgl2_alertOKBut'>OK</div>").click(function() {
                dfd.resolve(true);
                $("#dgl2_alertBox").hide();
            }).appendTo(box.find("div.dgl2_alertButtons"));
        } else if (mode == 1) {
            $("<div class='dgl2_alertOKBut'>OK</div>").click(function() {
                dfd.resolve(true);
                $("#dgl2_alertBox").hide();
            }).appendTo(box.find("div.dgl2_alertButtons"));
            $("<div class='dgl2_alertCancelBut'>Cancel</div>").click(function() {
                dfd.resolve(false);
                $("#dgl2_alertBox").hide();
            }).appendTo(box.find("div.dgl2_alertButtons"));
        } else if (mode == 2) {
            $("<input type='text' id='dgl2_promptVal' value='" + defText + "' class='text ui-widget-content ui-corner-all'>").appendTo(box.find("div.dgl2_alertText"));
            $("<div class='dgl2_alertOKBut'>OK</div>").click(function() {
                dfd.resolve($("#dgl2_promptVal").val());
                $("#dgl2_alertBox").hide();
            }).appendTo(box.find("div.dgl2_alertButtons"));
            $("<div class='dgl2_alertCancelBut'>Cancel</div>").click(function() {
                dfd.resolve(false);
                $("#dgl2_alertBox").hide();
            }).appendTo(box.find("div.dgl2_alertButtons"));
        }
        box.show();

        dragElement(box[0]);

        return dfd.promise();
    }

    function showPopup(event) {
        event.preventDefault();
        event.stopPropagation();
        insertHTML(); //does nothing if already inserted

        let el = $("div.dgl2_groupdialog");

        el.show();
        dragElement(el[0]);
        el.attr("tabindex", "0");
        el.keydown(function(event) {
            if (event.target.tagName != "INPUT") {
                highlightLetter(String.fromCharCode(event.which));
            }
        });
        $("div.dgl2_groupdialog, div.dgl2_groupdialog>div").click(function(event) {
            event.stopPropagation();
            if (event.target.tagName != "INPUT") {
                $("div.dgl2_groupdialog").focus();
            }
        });

        // $("div.dgl2_groupdialog").dialog({
        //     width: "80%",
        //     height: (window.innerHeight * 0.8),
        //     draggable: false,
        //     resizable: false,
        //     dialogClass: 'groupPopup',
        //     create: function(event) {
        //         $(event.target).parent().css({
        //             'position': 'fixed',
        //             "left": '10%',
        //             "top": '10%'
        //         });
        //         $(event.target).parent().find("div.ui-dialog-titlebar").prepend($("div.dgl2_titlebar"));
        //         $(event.target).parent().find("span.ui-dialog-title").hide();
        //     }
        // });
        // $("div.groupPopup").keydown(function(event) {
        //     if (event.target.tagName != "INPUT" && !$("#dgl2_grContext").is(":visible")) {
        //         highlightLetter(String.fromCharCode(event.which));
        //     }
        // });
    }

    function groupNameById(id) {
        for (let g of groups) {
            if (g.userId == id) return g.username;
        }
        return "";
    }
    //bind script to buttons. dynamic browsing compatible
    function addListener() {
        let els = $('*[datahook="group_counter"]:not([dgl2=1]),*[data-hook="group_counter"]:not([dgl2=1])');
        els.attr("dgl2", 1).find("svg").html(
            '<path d="M18.63 17l1.89 5h2l-2.53-7h-6.67l.64 2zM4.04 15l-2.52 7h2l1.88-5h4.23l1.89 5h2l-2.53-7zM7.52 4.33c1.9304.011 3.4873 1.5829 3.48 3.5133-.0074 1.9303-1.5762 3.4903-3.5066 3.4866C5.563 11.3263 4 9.7604 4 7.83c0-1.933 1.567-3.5 3.5-3.5h.02zm-.02-2C4.4624 2.33 2 4.7924 2 7.83s2.4624 5.5 5.5 5.5 5.5-2.4624 5.5-5.5-2.4624-5.5-5.5-5.5zM13 3.37a5.59 5.59 0 0 1 1.5 1.45 3.41 3.41 0 0 1 1.5-.35c1.933 0 3.5 1.567 3.5 3.5s-1.567 3.5-3.5 3.5a3.41 3.41 0 0 1-1.5-.35 5.63 5.63 0 0 1-1.5 1.46c1.968 1.2806 4.532 1.1706 6.3831-.2738 1.8511-1.4445 2.5812-3.9047 1.8175-6.125C20.437 3.9608 18.348 2.4702 16 2.47a5.4102 5.4102 0 0 0-3 .9z"/>' +
            '<path stroke="#0A0" stroke-width="4" stroke-opacity="0.8" d="M12 18H24M18 12V24"/>'
        );
        els.parent().parent().click(showPopup);
    }
    $(document).ready(function() {
        $(document).mousedown(function(event) {
            if ($(event.target).closest("#dgl2_grContext").length == 0) {
                $("#dgl2_grContext").hide().find("select").empty();
            }
        });
    });

    setInterval(addListener, 1000);

})();

/* resources:
group template
<button class="_3PBqc"> <!--_3UhBt-->
<div class="_3Lbo4 _3EbZ8">
<div class="_1lUlZ">
<div class="_3tZow">
<span class="_23Ekg _3q2EJ">
<img data-hook="user_avatar" alt="iterators's avatar" class="dIDzJ" src="https://a.deviantart.net/avatars-big/i/t/iterators.png?3">
</span>
</div>
<div class="_3_Nxk">
<span class="_3HH04 _3y1P2 _3kEx1 _32s2-">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8">
<path d="M4.75,3.25 L7,3.25 L7,4.75 L4.75,4.75 L4.75,7 L3.25,7 L3.25,4.75 L1,4.75 L1,3.25 L3.25,3.25 L3.25,1 L4.75,1 L4.75,3.25 Z"></path>
</svg>
</span>
</div>
</div>
<div class="_22Jp8">iterators</div>
</div>
</button>

group_request_response_structure
userId	14471598
useridUuid	55b0c0d3-fed7-45ff-8951-0d8554eb01b1
username	deviantARTSupporters
usericon	https://a.deviantart.net/avatars-big/d/e/deviantartsupporters.gif?12
type	group
isNewDeviant	false

subfolder request response structure
commentCount: 0
description: ""
folderId: 13361368
name: "Featured"
owner: Object { userId: 11209451, useridUuid: "294e75e1-94ec-4009-883a-b13eff743164", username: "iterators", … }
parentId: null
size: 5
thumb:
author: Object { userId: 7514199, useridUuid: "56613871-de84-49fc-b1c1-f9fd0f796461", username: "Championx91", … }
blockReason: null
deviationId: 710763111
files: Array(9) [ {…}, {…}, {…}, … ]
0: Object { type: "150", src: "https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/56613871-de84-49fc-b1c1-f9fd0f796461/dbr64br-58702ee6-5e4e-4bab-b2ea-c5ca2563bb7f.png/v1/fit/w_150,h_150,strp/suggestions_unwatch_fav_by_championx91_dbr64br-150.png?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7ImhlaWdodCI6Ijw9ODIxIiwicGF0aCI6IlwvZlwvNTY2MTM4NzEtZGU4NC00OWZjLWIxYzEtZjlmZDBmNzk2NDYxXC9kYnI2NGJyLTU4NzAyZWU2LTVlNGUtNGJhYi1iMmVhLWM1Y2EyNTYzYmI3Zi5wbmciLCJ3aWR0aCI6Ijw9NjQxIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmltYWdlLm9wZXJhdGlvbnMiXX0.vyFqJs1s5cyralkRXS31gJbHrwFsBm72c2q9Hb0uJDs", height: 150, … }
isAntisocial: true
isBlocked: false
isCommentable: true
isDeleted: false
isDownloadable: false
isFavourited: false
isJournal: false
isMature: false
isPublished: true
isPurchasable: false
isShareable: false
isTextEditable: false
legacyTextEditUrl: null
printId: null
publishedTime: "2017-10-20T10:39:40-0700"
stats: Object { comments: 17, favourites: 19 }
title: "Suggestions unwatch fav"
typeId: 1
url: "https://www.deviantart.com/championx91/art/Suggestions-unwatch-fav-710763111"
type: "gallery"

required:
groupid =  groups request
folderid = subfolders/favourites requests
deviationid = url \d+$
csrf_token = $("input[name=validate_token]").value; //deviation page
moduleID = window.document.body.innerHTML.match(/{\\\"id\\\":(\d+),\\\"type\\\":\\\"group_list_members/i) //https://www.deviantart.com/dediggefedde/about

groups GET
https://www.deviantart.com/_napi/da-user-profile/api/module/groups/members?username=Dediggefedde&moduleid=1725444339&offset=30&limit=24
Limit max 60
finished if hasMore==false
needs moduleID, username

subfolders GET
https://www.deviantart.com/_napi/shared_api/deviation/group_folders?groupid=13042771&type=gallery
needs groupID

favourites GET
https://www.deviantart.com/_napi/shared_api/deviation/group_folders?groupid=28209298&type=collection
needs groupID

add to subfolder POST
{"groupid":11209451,"type":"gallery","folderid":23881334,"deviationid":501848540,"csrf_token":"wPPvYvd4br1JpNjT.pyuz1q.lVPn5jT3ohUO8uD0QLruZb-V9d5NDb1FOyl7OcHe7w"}
https://www.deviantart.com/_napi/shared_api/deviation/group_add
needs:
csrf_token	THjPWGH1SRH_-epZ.py75wu.VTYjdFg4YApQBqsZBS5ISBHG1W4aWv-oTkk5WDA4t-w
deviationid	298766207
folderid	23881334
groupid	11209451
type	gallery
*/