// ==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
*/