Greasy Fork is available in English.

dA_sort_gallery

Sorting deviantart.com gallery folder pictures

// ==UserScript==
// @name         dA_sort_gallery
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Sorting deviantart.com gallery folder pictures
// @author       dediggefedde
// @match        https://www.deviantart.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deviantart.com
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @noframes
// ==/UserScript==


/*
	TODO:
		cancel to cancel progress
		disable buttons during process 
		import/export
*/
const sortimg = `<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 -50 400 500">
    <rect x="16" y="40"  width="340" height="28"/>
    <rect x="16" y="140" width="290" height="28"/>
    <rect x="16" y="240" width="240" height="28"/>
    <rect x="16" y="340" width="190" height="28"/>
</svg>`;

(function() {
    'use strict';
    let interSortDelay=500; //milliseconds between sort requests
    let actFolder = null;
    let isfetching = false;
    let token = null;
    let username = null;
    let totalDevs = 0;
    let fetchedDevs = 0;
    let db = []; //array of entries {folderId, deviationid, title, publishedTime, views, favs, thumbUrl, reqDate}, format date "2022-10-08T16:26:40-0700"

    let dbsel = null,
        dbsort = null; //temporary db selection

    let progFetch = null, //html elements, quickaccess
        progSort = null,
        dialog = null,
        style = null,
        slider = null,
        prevCont = null;
    let moveOrder = []; //moving requests
    let totalToMove = 0;
    let today;

    function reqSort() {
        /*
        request sort: POST: https://www.deviantart.com/_napi/shared_api/gallection/folders/update_deviation_order
        csrf_token	"d7okysuxM7dW9__i.rk0w3p.aUFLMyo3Oa2uuKoCH6X68dSmTRvIi126lcBQJsxqdCI"
        deviationid	932351217
        folderid	84979945
        position	5
        type	"gallery"
         */

        return new Promise(function(resolve, reject) {
            if (moveOrder.length == 0) {
                resolve();
                return;
            }
            let mv = moveOrder.shift(); //el, ind, oldind
            let dat = {
                csrf_token: token,
                deviationid: mv.el,
                folderid: actFolder,
                type: "gallery",
                position: mv.ind
            };
            GM.xmlHttpRequest({
                method: "POST",
                headers: {
                    "accept": 'application/json, text/plain, */*',
                    "content-type": 'application/json;charset=UTF-8'
                },
                dataType: 'json',
                data: JSON.stringify(dat),
                url: `https://www.deviantart.com/_napi/shared_api/gallection/folders/update_deviation_order`,
                onerror: function(response) {
                    reject("dA_sort_gallery request failed:", response);
                },
                onload: function(response) {
                    setProgress(progSort, totalToMove - moveOrder.length, totalToMove);
                    if (moveOrder.length == 0)
                        resolve();
                    else{
                        setTimeout(() => {
                            resolve(reqSort());
                        }, interSortDelay);
                    }
                }
            });
        });
    }

    function reqEntries(offset = 0) {
        today = (new Date());
        /*
        username=Dediggefedde&type=gallery
        &folderid=84979945
        &offset=0
        &limit=24
        &mature_content=true
        &csrf_token=d7okysuxM7dW9__i.rk0w3p.aUFLMyo3Oa2uuKoCH6X68dSmTRvIi126lcBQJsxqdCI
        */
        return new Promise(function(resolve, reject) {
            GM.xmlHttpRequest({
                method: "GET",
                url: `https://www.deviantart.com/_napi/shared_api/gallection/contents?type=gallery&username=${username}&folderid=${actFolder}&offset=${offset}&limit=24&mature_content=true&csrf_token=${token}`,
                onerror: function(response) {
                    reject("dA_sort_gallery request failed:", response);
                },
                onload: function(response) {
                    let resp = JSON.parse(response.responseText);

                    fetchedDevs += resp.results.length;
                    setProgress(progFetch, fetchedDevs, totalDevs);
                    db = [].concat(db, resp.results.map((el) => {
                        let thumb = "";
                        let token = "";
                        try {
                            if (el.media.token != null)
                                token = "?token=" + el.media.token[0];
                            if (el.media.types[0].c == null)
                                thumb = el.media.baseUri + token;
                            else
                                thumb = el.media.baseUri + el.media.types[0].c.replace("<prettyName>", el.media.prettyName) + token;
                        } catch (ex) {
                            console.log("thumb error:", ex, el);
                        }
                        return { folderId: actFolder, deviationId: el.deviationId, title: el.title, publishedTime: el.publishedTime, views: el.stats.views, favs: el.stats.favourites, thumbUrl: thumb, reqDate: today };
                    }));
                    if (resp.hasMore) {
                        setTimeout(() => {
                            resolve(reqEntries(resp.nextOffset));
                        }, 500);
                    } else {
                        resolve(resp);
                    }
                }
            });
        });
    }

    function arraymove(arr, fromIndex, toIndex) {
        var element = arr[fromIndex];
        arr.splice(fromIndex, 1);
        arr.splice(toIndex, 0, element);
    }

    function evSort(ev) { // sort button
        let oldOrder = dbsel.map(el => el.deviationId);
        let newOrder = dbsort.map(el => el.deviationId);
        let checkOrder = [...oldOrder];
        moveOrder = [];
        //reactive move algorithm, sometimes reduces 
        let maxind = document.getElementById("dA_sort_gallery_affected").value;

        newOrder.forEach((el, ind) => {
            if (ind >= maxind) return;
            if (checkOrder[ind] != el) {
                let oldind = checkOrder.indexOf(el);
                let altind = newOrder.indexOf(checkOrder[ind]);
                arraymove(checkOrder, oldind, ind);
                moveOrder.push({ el: el, ind: ind, old: oldind });

                if (checkOrder[ind + 1] != newOrder[ind + 1] && altind < maxind) {
                    arraymove(checkOrder, ind + 1, altind);
                    moveOrder.push({ el: checkOrder[altind], ind: altind, old: ind + 1 });
                }
            }
        });
        if (moveOrder.length > newOrder.length) { //avg algorithm 70%, but sometimes runs >N. complete reinsert always runs N times
            moveOrder = [];
            checkOrder = [...oldOrder];
            newOrder.slice(0, maxind).reverse().forEach((el, ind) => {
                let oldI = checkOrder.indexOf(el);
                if (oldI == 0) return;
                arraymove(checkOrder, oldI, 0);
                moveOrder.push({ el: el, ind: 0, old: oldI });
            })
        }

        let testEq = newOrder.filter((el, ind) => { return checkOrder[ind] != el; }).length == 0;

        totalToMove = moveOrder.length;
        if (moveOrder.length == 0) alert("Already Sorted!");
        else if (confirm(`This order requires ${totalToMove} move requests. Continue?`)) {
            reqSort().then(() => {
                alert("Sorting complete!\nPressing 'OK' will reload the page.\nPlease fetch entries again before further sorting.");
                location.reload();
            }).catch(err => {
                alert("An error occured while sorting! More details can be found in the console (F12)\n" + err);
                console.log("Gallery sorting error:", err);
            });
        }
    }

    function evSelect(ev) { //select sorting target or type
        prevCont.innerHTML = "";
        let selslope = document.getElementById("dA_sort_gallery_slope").value == "asc" ? 1 : -1; //asc, desc
        let seltarget = document.getElementById("dA_sort_gallery_target").value;
        let pfrag = new DocumentFragment();
        dbsel = db.filter(el => el.folderId == actFolder);
        if (seltarget == "invert") {
            dbsort = [...dbsel].reverse();
        } else {
            dbsort = [...dbsel].sort((a, b) => {
                return selslope * ((a[seltarget] > b[seltarget]) - (a[seltarget] < b[seltarget]))
            });
        }

        for (let i = 0; i < 4 && i < dbsort.length; ++i) {
            let domEl = document.createElement("img");
            domEl.src = dbsort[i].thumbUrl;
            domEl.title = `${dbsort[i].title}\n${dbsort[i].publishedTime}\nViews: ${dbsort[i].views}\nFavourites: ${dbsort[i].favs}`;
            pfrag.appendChild(domEl);
        }
        prevCont.appendChild(pfrag);
    }

    function evInvokeClick(ev) { //shows/init dialog
        let checkFol = /\/gallery\/(\d+)\//i.exec(location.href);
        if (checkFol == null) {
            actFolder = document.querySelector("[data-hook=gallection_folder_1]").parentNode.href.match(/\/(\d+)\//)[1]; //favourites always second in list
        } else {
            actFolder = checkFol[1];
        }
        token = document.querySelector("input[name=validate_token]").value;
        document.getElementById("dA_sort_gallery_folderID").innerHTML = actFolder;
        let d1 = null,
            d2 = null;
        fetchedDevs = 0;
        fetchedDevs = db.reduce((cnt, el) => {
            if (d1 == null || d1 < el.reqDate) d1 = el.reqDate;

            if (el.folderId == actFolder) {
                if (d2 == null || d2 < el.reqDate) d2 = el.reqDate;
                return cnt + 1;
            } else {
                return cnt;
            }
        }, 0);
        let text1, text2;
        if (d1 != null) text1 = d1.toLocaleDateString();
        else text1 = "not scanned";
        if (d2 != null) text2 = d2.toLocaleDateString();
        else text2 = "not scanned";
        document.getElementById("dA_sort_gallery_folderEntries").innerHTML = fetchedDevs + " (" + text2 + ")";
        document.getElementById("dA_sort_gallery_allchoice").innerHTML = "All " + fetchedDevs;
        document.getElementById("dA_sort_gallery_allchoice").value = fetchedDevs;
        document.getElementById("dA_sort_gallery_dataEntries").innerHTML = db.length + " (" + text1 + ")";

        dialog.style.display = "block";
        scrollPage(0);
    }

    function evFetchFolder(ev) { //click fetch button
        if (isfetching) return;
        isfetching = true;

        db = db.filter(el => { return el.folderId != actFolder; });
        fetchedDevs = 0;

        reqEntries(0).then((ret) => {
            GM.setValue("db", JSON.stringify(db));
            document.getElementById("dA_sort_gallery_folderEntries").innerHTML = fetchedDevs + " (" + today.toLocaleDateString() + ")";
            document.getElementById("dA_sort_gallery_dataEntries").innerHTML = db.length + " (" + today.toLocaleDateString() + ")";
            document.getElementById("dA_sort_gallery_allchoice").innerHTML = "All " + fetchedDevs;
            document.getElementById("dA_sort_gallery_allchoice").value = fetchedDevs;
            setTimeout(() => { scrollPage(1); }, 500);
        }).catch(() => {
            alert("An error occured while fetching! More details can be found in the console (F12)\n" + err);
            console.log("Gallery fetching error:", err);
        }).finally(() => {
            isfetching = false;
        });
    }

    function scrollPage(page) {
        slider.style.transform = `translate(-${(425*page)}px)`;
        if (page == 1) evSelect(null);
    }

    function setProgress(bar, value, total) {
        if (total == 0 || bar == null) return;
        let perc = Math.ceil(value / total * 100);
        bar.dataset.label = `${value}/${total} (${perc}%)`;
        bar.getElementsByTagName("span")[0].style.width = perc + "%";
    }

    function addStyle() {
        if (document.getElementById("dA_sort_gallery_style") != null) return;
        style = document.createElement("style");
        style.id = "dA_sort_gallery_style";
        style.innerHTML = `
        #dA_sort_gallery_buttonCont{display: flex;margin: 5px;font-size: small;color:#7579ff;fill:#7579ff;cursor:pointer}
        #dA_sort_gallery_buttonCont:hover{color: var(--D8);fill:currentColor;}
        #dA_sort_gallery_buttonCont svg{height:1em;margin:0 5px;}
        #dA_sort_gallery_dialog{background-color:#f4fbf4;width:400px;position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);color:black;padding: 15px;border: 2px solid #076628;border-radius: 15px;display:none;z-index:99;overflow: hidden;}
        #dA_sort_gallery_dialog h3{text-align:center;font-size:x-large;margin-bottom:1em;}
        #dA_sort_gallery_dialog h4{text-align:left;font-size:large;margin-bottom:0.5em;}
        #dA_sort_gallery_dialog select{display: inline-block;vertical-align: middle;cursor: pointer;border: 1px solid green;background-color: #cfa;border-radius: 5px;padding: 5px;}
        #dA_sort_gallery_dialog .dA_sort_gallery_buttons{display:flex;justify-content: space-around;}
        #dA_sort_gallery_dialog button{background:none;border:none;font-size:large;font-weight: bold;font-style: italic;color:#050;cursor:pointer;}
        #dA_sort_gallery_dialog button:hover{color:#370;}
        #dA_sort_gallery_dialog button:active{color:#770;}
        #dA_sort_gallery_dialog section{display: inline-flex;flex-direction: column;gap: 10px;width: 400px;margin-right: 20px;height:100%;}
        #dA_sort_gallery_dialog label{margin-right:20px;display:inline-block;}
				#dA_sort_gallery_fetching label{width:50%;}
				.dA_sort_gallery_progress {border-radius: 5px; height: 1.5em; width: 100%; border: 1px inset black; box-shadow: 1px 1px 1px black inset; background: white; position: relative;}
				.dA_sort_gallery_progress:before { content: attr(data-label); font-size: 0.8em; position: absolute; text-align: center;  top: 5px; left: 0;  right: 0;}
				.dA_sort_gallery_progress span {background-color: #7cc4ff; display: inline-block; height: 100%;}
				#dA_sort_gallery_clearDB{font-size:normal;}
				#dA_sort_gallery_slider{height: 300px;width: 300%;transition: transform; transition-duration: 0.25s;}
				#dA_sort_gallery_imgPrev{flex:1;display:flex;gap:10px;height:75px;}
				#dA_sort_gallery_imgPrev img {align-self: center;object-fit: cover;width: 100%;max-height: 100%;}
				#dA_sort_gallery_dialog .disabled {color:#ccc;}
        `; //transform: translateX(-425px);
        document.head.appendChild(style);
    }

    function addDialog() {
        if (document.getElementById("dA_sort_gallery_dialog") != null) return;
        dialog = document.createElement("div");
        dialog.id = "dA_sort_gallery_dialog";
        dialog.innerHTML = `
            <h3>Sorting a Gallery</h3>
            <div id="dA_sort_gallery_slider">
              <section id="dA_sort_gallery_fetching">
                <h4>Fetching Gallery Entries</h4>
                <div><label>Gallery folder:</label><span id='dA_sort_gallery_folderID'>0</span></div>
                <div><label>Folder entries:</label><span id='dA_sort_gallery_folderEntries'>0</span></div>
                <div><label>Database entries:</label><span id='dA_sort_gallery_dataEntries'>0</span></div>
                <div style="flex:1"><button id='dA_sort_gallery_clearDB'>Clear Database</button></div>
                <div id="dA_sort_gallery_fetchProgress" class="dA_sort_gallery_progress" data-label=""><span style="width:0%;"></span></div>            
                <div class="dA_sort_gallery_buttons">
                  <button id='dA_sort_gallery_cancel'>Cancel</button>
                  <button id="dA_sort_gallery_fatch">Fetch Images</button>
                  <button id="dA_sort_gallery_skip">Skip</button>
                </div>
              </section>
              <section id="dA_sort_gallery_sorting">
                <h4>Sorting Submissions</h4>
                <div>
                <label>Result</label>
								<select id="dA_sort_gallery_affected" title="After sorting, only the first # follow the rule">
									<option value="24">First 24</option> 
									<option value="48">First 48</option>
									<option id='dA_sort_gallery_allchoice' value="all">All</option>
                </select>
                </div>
								<div>    
                <label>Sort Property</label>
								<select id="dA_sort_gallery_target">
									<option value="publishedTime">Date</option> 
									<option value="title">Name</option>
									<option value="views">Views</option>
									<option value="favs">Favourites</option>
									<option value="invert">Invert</option>
                </select>
								<select id="dA_sort_gallery_slope">
									<option value="desc">Descending</option>
									<option value="asc">Ascending</option>
                </select>
              </div>    
							<div>Preview:</div>
							<div id="dA_sort_gallery_imgPrev">
							</div>   
							<div id="dA_sort_gallery_sortingProgress" class="dA_sort_gallery_progress" data-label=""><span style="width:0%;"></span></div>    
							<div class="dA_sort_gallery_buttons">
								<button id='dA_sort_gallery_cancel2'>Cancel</button>
								<button id="dA_sort_gallery_back">Back</button>
								<button id="dA_sort_gallery_sort">Sort</button>
							</div>
              </section>
            </div>
        `;

        document.body.appendChild(dialog);
        progFetch = document.getElementById("dA_sort_gallery_fetchProgress");
        progSort = document.getElementById("dA_sort_gallery_sortingProgress");
        slider = document.getElementById("dA_sort_gallery_slider");
        prevCont = document.getElementById("dA_sort_gallery_imgPrev");

        document.getElementById("dA_sort_gallery_cancel").addEventListener("click", function(ev) {
            dialog.style.display = "";
        }, false);
        document.getElementById("dA_sort_gallery_cancel2").addEventListener("click", function(ev) {
            dialog.style.display = "";
        }, false);
        document.getElementById("dA_sort_gallery_back").addEventListener("click", function(ev) {
            scrollPage(0);
        }, false);
        document.getElementById("dA_sort_gallery_fatch").addEventListener("click", evFetchFolder, false);
        document.getElementById("dA_sort_gallery_skip").addEventListener("click", (ev) => {
            if (fetchedDevs == 0) {
                alert("Please scan your gallery first!")
            } else
                scrollPage(1);
        }, false);
        document.getElementById("dA_sort_gallery_clearDB").addEventListener("click", () => {
            db = [];
            GM.setValue("db", JSON.stringify(db));
            document.getElementById("dA_sort_gallery_folderEntries").innerHTML = "0";
            document.getElementById("dA_sort_gallery_dataEntries").innerHTML = "0";
        }, false);
        document.getElementById("dA_sort_gallery_target").addEventListener("change", evSelect, false);
        document.getElementById("dA_sort_gallery_slope").addEventListener("change", evSelect, false);
        document.getElementById("dA_sort_gallery_sort").addEventListener("click", evSort, false);
    }

    function init() {
        if (!/gallery/i.test(location.href)) return;
        username = /deviantart\.com\/(.*?)\/gallery/i.exec(location.href)[1];

        /*let editBut = document.querySelector("#sub-folder-gallery button[data-role='edit-control']:not([dA_sort_gallery
        if (editBut == null) return;
        totalDevs = parseInt(/(\d+)\nEdit/i.exec(editBut.parentNode.parentNode.parentNode.innerText)[1]); //TODO
        editBut.setAttribute("dA_sort_gallery", 1);
        editBut.style.display = "inline-flex";


        let sortBut = editBut.cloneNode(true);
        let chlds = sortBut.getElementsByTagName("span");
        chlds[0].innerHTML = sortimg;
        chlds[1].innerHTML = "Sort";
        */
        if(document.querySelector("[dA_sort_gallery_img]")!=null)return

        let parCont=document.querySelector("#sub-folder-gallery svg:not([dA_sort_gallery_img])");
        if(parCont==null)return;
        parCont.setAttribute("dA_sort_gallery_img", 1);
        let sortBut=document.createElement("div");
        sortBut.id="dA_sort_gallery_buttonCont";
        sortBut.innerHTML=sortimg+"Sort";
        parCont.parentNode.parentNode.parentNode.after(sortBut);

        sortBut.addEventListener("click", evInvokeClick, false);

        GM.getValue("db").then((val) => {
            db = JSON.parse(val);
            db.forEach((el, ind, arr) => { arr[ind].reqDate = new Date(el.reqDate); });
            document.getElementById("dA_sort_gallery_dataEntries").innerHTML = db.length;
        });
    }

    addStyle();
    addDialog();
    setInterval(init, 1000);
})();

/*
request sort: POST: https://www.deviantart.com/_napi/shared_api/gallection/folders/update_deviation_order
csrf_token	"d7okysuxM7dW9__i.rk0w3p.aUFLMyo3Oa2uuKoCH6X68dSmTRvIi126lcBQJsxqdCI"
deviationid	932351217
folderid	84979945
position	5
type	"gallery"

###
request entries
GET https://www.deviantart.com/_napi/shared_api/gallection/contents?
username=Dediggefedde&type=gallery
&folderid=84979945
&offset=0
&limit=24
&mature_content=true
&csrf_token=d7okysuxM7dW9__i.rk0w3p.aUFLMyo3Oa2uuKoCH6X68dSmTRvIi126lcBQJsxqdCI
*/