Greasy Fork is available in English.

dev_group_list2

Better Submit-to-group dialog

// ==UserScript==
// @name         dev_group_list2
// @namespace    http://www.deviantart.com/
// @version      6.2
// @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 userId=0;
	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;
	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);
	}

	function fillAdminGroups(offset){
			let token = $("input[name=validate_token]").val();
			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: function(response) {
									let rex=/<section id="group_list_admins" .*?<\/section>/i;
									let res=rex.exec(response.responseText);
									if(res==null)return resolve(groups);

									let rex2=/"https:\/\/www.deviantart.com\/(.*?)"/gi;
									let groupnames=[];
									let rex2mtch;
									while(rex2mtch=rex2.exec(res[0]))groupnames.push(rex2mtch[1]);

									let grpcnt=groupnames.length;
									if(grpcnt==0)return resolve(groups);

									groupnames.forEach(groupName=>{
											//group array structure {userId,useridUuid,username,usericon,type,isNewDeviant,latestDate}
											//response structure {{gruser},{owner},{pagedata}}, owner=userId,useridUuid,etc.
											GM.xmlHttpRequest({
													method: "GET",
													url: `https://www.deviantart.com/_puppy/dauserprofile/init/about?username=${groupName}&csrf_token=${token}`,
													onerror: function(response) {
															if(--grpcnt==0)resolve(groups);
													},
													onload: function(response) {
															let resp=JSON.parse(response.responseText);
															if(resp.owner==null)console.log(groupName+ " can not be added since it's not migrated yet.");
															else groups = groups.concat(resp.owner);
															if(--grpcnt==0)resolve(groups);
													}
											});
									});

							}
					});
			});
	}

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

			let token = $("input[name=validate_token]").val();
			let murl = "https://www.deviantart.com/_puppy/gruser/module/groups/members?username=" + userName + "&moduleid=" + moduleID +"&gruserid="+userId+"&gruser_typeid=4&offset=" + offset + "&limit=" + grPerReq + "&csrf_token=" + token;
			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) {
											reject(err(errtyps.Connection_Error, "Connection error " + murl + " failed", response));
											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);
									}
							}
					});
			});
	}

	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 token = $("input[name=validate_token]").val();
					let murl=`https://www.deviantart.com/_puppy/dadeviation/group_folders?groupid=${groupID}&type=${type}&csrf_token=${token}`;
					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 reaching ${murl} 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,cursor) {
			let token = $("input[name=validate_token]").val();
			let murl = `https://www.deviantart.com/_puppy/dadeviation/featured_in_groups?deviationid=${devID}&limit=25&csrf_token=${token}&cursor=${cursor}`;
			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 res;
									try{
											res = JSON.parse(response.responseText);
									}catch(ers){
											reject(err(errtyps.Connection_Error, "Reading " + murl + " failed", {ers,response}));
											return;
									}
									for (let entr of res.results){
											listedGroups.push(parseInt(entr.userId));
									}

									if(res.cursor)
											resolve(fillListedGroups(devID,res.cursor));
									else
											resolve(listedGroups);
							}
					});
			});

	}

	function requestAddSubmission(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 token = $("input[name=validate_token]").val();
					let dat = {
							"groupid": parseInt(groupID),
							"type": type.toString(),
							"folderid": parseInt(folderID),
							"deviationid": parseInt(devID),
							"csrf_token":token.toString(),
					};
					let murl= `https://www.deviantart.com/_puppy/dadeviation/group_add`;
					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: murl,
									headers: {
											"Accept": 'application/json, text/plain, */*',
											"Accept-Language":"de,en-US;q=0.7,en;q=0.3",
											"Content-Type": 'application/json',
											"Pragma":"no-cache",
											"Cache-Control":"no-cache"
									},
									dataType: 'json',
									data: JSON.stringify(dat),
									onerror: function(response) {
											response.gname = groupNameById(groupID);
											reject(err(errtyps.Connection_Error, "Connection to https://www.deviantart.com/_puppy/dadeviation/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 = [];
			for (let d of macros[index].data) {
					promises.push(requestAddSubmission( 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");

			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") {
									requestAddSubmission(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 ? JSON.stringify(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
											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("Loading List of Admin-Groups...");
					return fillAdminGroups(0);
			}).then(function() {

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

					// $("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 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,parentId) { //return HTML string
			loadedFolders.set("" + foID, name);
			let imgstring;
			if(img==null){ //no thumbnail (empty or readonly)
					imgstring="<div class='dgl2_group_image'></div>";
			}else 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 + "'/>";
			}
			let parentIdStr=(parentId?" parentId='"+parentId+"'":"");

			return "<button class='dgl2_groupButton' groupID=" + grID + " folderName='" + escapeHtml(name) + "' folderID=" + foID + " folderType='" + foType + "'"+parentIdStr+" 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%;z-index:42;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{vertical-align:bottom;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; }
	button.dgl2_groupButton[parentId]{border-bottom:2px solid #d57917;margin-bottom:15px;}
	button.dgl2_groupButton[parentId] img {height: 25px;}
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) { not used
	// 		element.animate({ opacity: 0 }, 250, function() {
	// 				$(this).animate({ opacity: 1 }, 250, pulsing);
	// 		});
	// }

	function array_move(arr, old_index, new_index) {
			if (new_index >= arr.length) {
					var k = new_index - arr.length + 1;
					while (k--) {
							arr.push(undefined);
					}
			}
			arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
			return arr; // for testing
	};

	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());
			});*/

			subfolders = subfolders //sorting sub-folders
			.filter(el => el.parentId==null) //grouping by lack of parent
			.sort((a,b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) //sort folders by name
			.reduce((acc, curr) =>{
					const children = subfolders
					.filter(({parentId}) => parentId === curr.folderId) //assigning subfolders to folders
					.sort((a,b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); //sorting subfolders by name
					acc.push(curr, ...children);
					return acc;
			}, []);


			if (buts.length > 0) {
					let subf;
					let newBut;
					if ($("#dgl2_grContext").is(":visible")) {
							let cont = $("#dgl2_grContext select");
							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) {
									newBut = $(getSubFolderTemplate(subf.name, subf.size, subf.owner.userId, subf.folderId, subf.type, subf.thumb,subf.parentId));
									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=groups.filter(el=>{return el!=null && el.username!=null;}); //sanitize in case of errors
			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;
					else 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";
			userId = $("a.user-link").attr("data-userid"); // "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();
			});

			GM.getValue("groups", "").then(function(grps) {
					if (grps == "") {
							Ev_getGroupClick();
					} else {
							groups = JSON.parse(grps);
							insertGroups();
					}
					return fillListedGroups(devID,"");
			}).then(()=>{
					listedGroups.forEach(grId=>{
							let grbut=document.querySelector("button.dgl2_groupButton[groupid='"+grId+"']");
							if(grbut!=null)grbut.classList.add("dgl2_inGroup");
					});
			}).catch(function(e) {
					errorHndl(err(errtyps.Unknown_Error, "fillListedGroups error", e));
			});

	}

	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();
					}
			});
	}

	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 = $("div:nth-child(2) > span:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > button:nth-child(1):not([dgl2='1'])");//svg button
			//$('*[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);

})();