Greasy Fork is available in English.

dA_sort_gallery

Sorting deviantart.com gallery folder pictures

  1. // ==UserScript==
  2. // @name dA_sort_gallery
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @description Sorting deviantart.com gallery folder pictures
  6. // @author dediggefedde
  7. // @match https://www.deviantart.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=deviantart.com
  9. // @grant GM.xmlHttpRequest
  10. // @grant GM.setValue
  11. // @grant GM.getValue
  12. // @noframes
  13. // ==/UserScript==
  14.  
  15.  
  16. const sortimg = `<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 -50 400 500">
  17. <rect x="16" y="40" width="340" height="28"/>
  18. <rect x="16" y="140" width="290" height="28"/>
  19. <rect x="16" y="240" width="240" height="28"/>
  20. <rect x="16" y="340" width="190" height="28"/>
  21. </svg>`;
  22.  
  23. (function() {
  24. 'use strict';
  25. let interSortDelay=500; //milliseconds between sort requests
  26. let actFolder = null;
  27. let isfetching = false;
  28. let token = null;
  29. let username = null;
  30. let totalDevs = 0;
  31. let fetchedDevs = 0;
  32. let db = []; //array of entries {folderId, deviationid, title, publishedTime, views, favs, thumbUrl, reqDate}, format date "2022-10-08T16:26:40-0700"
  33.  
  34. let dbsel = null,
  35. dbsort = null; //temporary db selection
  36.  
  37. let progFetch = null, //html elements, quickaccess
  38. progSort = null,
  39. dialog = null,
  40. style = null,
  41. slider = null,
  42. prevCont = null;
  43. let moveOrder = []; //moving requests
  44. let totalToMove = 0;
  45. let today;
  46.  
  47. function reqSort() {
  48. /*
  49. request sort: POST: https://www.deviantart.com/_napi/shared_api/gallection/folders/update_deviation_order
  50. csrf_token "d7okysuxM7dW9__i.rk0w3p.aUFLMyo3Oa2uuKoCH6X68dSmTRvIi126lcBQJsxqdCI"
  51. deviationid 932351217
  52. folderid 84979945
  53. position 5
  54. type "gallery"
  55. */
  56.  
  57. token = document.querySelector("input[name=validate_token]").value;
  58. return new Promise(function(resolve, reject) {
  59. if (moveOrder.length == 0) {
  60. resolve();
  61. return;
  62. }
  63. let mv = moveOrder.shift(); //el, ind, oldind
  64. let dat = {
  65. "csrf_token": token.toString(),
  66. "deviationid": mv.el,
  67. "folderid": parseInt(actFolder),
  68. "type": "gallery",
  69. "position": mv.ind,
  70. "da_minor_version": "2023071020230710",
  71. "username":username
  72. };
  73. GM.xmlHttpRequest({
  74. method: "POST",
  75. headers: {
  76. "Accept": 'application/json, text/plain, */*',
  77. "Accept-Language":"de,en-US;q=0.7,en;q=0.3",
  78. "Content-Type": 'application/json',
  79. "Pragma":"no-cache",
  80. "Cache-Control":"no-cache"
  81. },
  82. dataType: 'json',
  83. data: JSON.stringify(dat),
  84. url: `https://www.deviantart.com/_puppy/dashared/gallection/folders/update_deviation_order`,
  85. onerror: function(response) {
  86. reject("dA_sort_gallery request failed:", response);
  87. },
  88. onload: function(response) {
  89. console.log(dat, response.responseText);
  90. setProgress(progSort, totalToMove - moveOrder.length, totalToMove);
  91. if (moveOrder.length == 0)
  92. resolve();
  93. else{
  94. setTimeout(() => {
  95. resolve(reqSort());
  96. }, interSortDelay);
  97. }
  98. }
  99. });
  100. });
  101. }
  102.  
  103. function reqEntries(offset = 0) {
  104. today = (new Date());
  105. /*
  106. username=Dediggefedde&type=gallery
  107. &folderid=84979945
  108. &offset=0
  109. &limit=24
  110. &mature_content=true
  111. &csrf_token=d7okysuxM7dW9__i.rk0w3p.aUFLMyo3Oa2uuKoCH6X68dSmTRvIi126lcBQJsxqdCI
  112. */
  113. return new Promise(function(resolve, reject) {
  114. GM.xmlHttpRequest({//https://www.deviantart.com/_napi/shared_api/gallection/contents
  115. method: "GET",
  116. url: `https://www.deviantart.com/_puppy/dashared/gallection/contents?type=gallery&username=${username}&folderid=${actFolder}&offset=${offset}&limit=24&mature_content=true&csrf_token=${token}`,
  117. onerror: function(response) {
  118. reject("dA_sort_gallery request failed:", response);
  119. },
  120. onload: function(response) {
  121. try{
  122. let resp = JSON.parse(response.responseText);
  123.  
  124. fetchedDevs += resp.results.length;
  125. setProgress(progFetch, fetchedDevs, totalDevs);
  126. db = [].concat(db, resp.results.map((el) => {
  127. let thumb = "";
  128. let token = "";
  129. try {
  130. if (el.media.token != null)
  131. token = "?token=" + el.media.token[0];
  132. if (el.media.types[0].c == null)
  133. thumb = el.media.baseUri + token;
  134. else
  135. thumb = el.media.baseUri + el.media.types[0].c.replace("<prettyName>", el.media.prettyName) + token;
  136. } catch (ex) {
  137. console.error("dA_sort_gallery: Thumb error:", ex, el);
  138. }
  139. return { folderId: actFolder, deviationId: el.deviationId, title: el.title, publishedTime: el.publishedTime, views: el.stats.views, favs: el.stats.favourites, thumbUrl: thumb, reqDate: today };
  140. }));
  141. if (resp.hasMore) {
  142. setTimeout(() => {
  143. resolve(reqEntries(resp.nextOffset));
  144. }, 500);
  145. } else {
  146. resolve(resp);
  147. }
  148. }catch(ex){
  149. alert("An error occured while parsing the website response. Please contact the developer to provide an update");
  150. console.error("dA_sort_gallery: Error while parsing website:",ex,response.responseText);
  151. }
  152. }
  153. });
  154. });
  155. }
  156.  
  157. function arraymove(arr, fromIndex, toIndex) {
  158. var element = arr[fromIndex];
  159. arr.splice(fromIndex, 1);
  160. arr.splice(toIndex, 0, element);
  161. }
  162.  
  163. function evSort(ev) { // sort button
  164. let oldOrder = dbsel.map(el => el.deviationId);
  165. let newOrder = dbsort.map(el => el.deviationId);
  166. let checkOrder = [...oldOrder];
  167. moveOrder = [];
  168. //reactive move algorithm, sometimes reduces
  169. let maxind = document.getElementById("dA_sort_gallery_affected").value;
  170.  
  171. newOrder.forEach((el, ind) => {
  172. if (ind >= maxind) return;
  173. if (checkOrder[ind] != el) {
  174. let oldind = checkOrder.indexOf(el);
  175. let altind = newOrder.indexOf(checkOrder[ind]);
  176. arraymove(checkOrder, oldind, ind);
  177. moveOrder.push({ el: el, ind: ind, old: oldind });
  178.  
  179. if (checkOrder[ind + 1] != newOrder[ind + 1] && altind < maxind) {
  180. arraymove(checkOrder, ind + 1, altind);
  181. moveOrder.push({ el: checkOrder[altind], ind: altind, old: ind + 1 });
  182. }
  183. }
  184. });
  185. if (moveOrder.length > newOrder.length) { //avg algorithm 70%, but sometimes runs >N. complete reinsert always runs N times
  186. moveOrder = [];
  187. checkOrder = [...oldOrder];
  188. newOrder.slice(0, maxind).reverse().forEach((el, ind) => {
  189. let oldI = checkOrder.indexOf(el);
  190. if (oldI == 0) return;
  191. arraymove(checkOrder, oldI, 0);
  192. moveOrder.push({ el: el, ind: 0, old: oldI });
  193. })
  194. }
  195.  
  196. let testEq = newOrder.filter((el, ind) => { return checkOrder[ind] != el; }).length == 0;
  197.  
  198. totalToMove = moveOrder.length;
  199. if (moveOrder.length == 0) alert("Already Sorted!");
  200. else if (confirm(`This order requires ${totalToMove} move requests. Continue?`)) {
  201. reqSort().then(() => {
  202. alert("Sorting complete!\nPressing 'OK' will reload the page.\nPlease fetch entries again before further sorting.");
  203. location.reload();
  204. }).catch(err => {
  205. alert("An error occured while sorting! More details can be found in the console (F12)\n" + err);
  206. console.error("dA_sort_gallery: Gallery sorting error:", err);
  207. });
  208. }
  209. }
  210.  
  211. function evSelect(ev) { //select sorting target or type
  212. prevCont.innerHTML = "";
  213. let selslope = document.getElementById("dA_sort_gallery_slope").value == "asc" ? 1 : -1; //asc, desc
  214. let seltarget = document.getElementById("dA_sort_gallery_target").value;
  215. let pfrag = new DocumentFragment();
  216. dbsel = db.filter(el => el.folderId == actFolder);
  217. if (seltarget == "invert") {
  218. dbsort = [...dbsel].reverse();
  219. } else {
  220. dbsort = [...dbsel].sort((a, b) => {
  221. return selslope * ((a[seltarget] > b[seltarget]) - (a[seltarget] < b[seltarget]))
  222. });
  223. }
  224.  
  225. for (let i = 0; i < 4 && i < dbsort.length; ++i) {
  226. let domEl = document.createElement("img");
  227. domEl.src = dbsort[i].thumbUrl;
  228. domEl.title = `${dbsort[i].title}\n${dbsort[i].publishedTime}\nViews: ${dbsort[i].views}\nFavourites: ${dbsort[i].favs}`;
  229. pfrag.appendChild(domEl);
  230. }
  231. prevCont.appendChild(pfrag);
  232. }
  233.  
  234. function evInvokeClick(ev) { //shows/init dialog
  235. let checkFol = /\/gallery\/(\d+)\//i.exec(location.href);
  236. if (checkFol == null) {
  237. actFolder = document.querySelector("[data-hook=gallection_folder_1]").parentNode.href.match(/\/(\d+)\//)[1]; //favourites always second in list
  238. } else {
  239. actFolder = checkFol[1];
  240. }
  241. token = document.querySelector("input[name=validate_token]").value;
  242. document.getElementById("dA_sort_gallery_folderID").innerHTML = actFolder;
  243. let d1 = null,
  244. d2 = null;
  245. fetchedDevs = 0;
  246. fetchedDevs = db.reduce((cnt, el) => {
  247. if (d1 == null || d1 < el.reqDate) d1 = el.reqDate;
  248.  
  249. if (el.folderId == actFolder) {
  250. if (d2 == null || d2 < el.reqDate) d2 = el.reqDate;
  251. return cnt + 1;
  252. } else {
  253. return cnt;
  254. }
  255. }, 0);
  256. let text1, text2;
  257. if (d1 != null) text1 = d1.toLocaleDateString();
  258. else text1 = "not scanned";
  259. if (d2 != null) text2 = d2.toLocaleDateString();
  260. else text2 = "not scanned";
  261. document.getElementById("dA_sort_gallery_folderEntries").innerHTML = fetchedDevs + " (" + text2 + ")";
  262. document.getElementById("dA_sort_gallery_allchoice").innerHTML = "All " + fetchedDevs;
  263. document.getElementById("dA_sort_gallery_allchoice").value = fetchedDevs;
  264. document.getElementById("dA_sort_gallery_dataEntries").innerHTML = db.length + " (" + text1 + ")";
  265.  
  266. dialog.style.display = "block";
  267. scrollPage(0);
  268. }
  269.  
  270. function evFetchFolder(ev) { //click fetch button
  271. if (isfetching) return;
  272. isfetching = true;
  273.  
  274. db = db.filter(el => { return el.folderId != actFolder; });
  275. fetchedDevs = 0;
  276.  
  277. reqEntries(0).then((ret) => {
  278. GM.setValue("db", JSON.stringify(db));
  279. document.getElementById("dA_sort_gallery_folderEntries").innerHTML = fetchedDevs + " (" + today.toLocaleDateString() + ")";
  280. document.getElementById("dA_sort_gallery_dataEntries").innerHTML = db.length + " (" + today.toLocaleDateString() + ")";
  281. document.getElementById("dA_sort_gallery_allchoice").innerHTML = "All " + fetchedDevs;
  282. document.getElementById("dA_sort_gallery_allchoice").value = fetchedDevs;
  283. setTimeout(() => { scrollPage(1); }, 500);
  284. }).catch(() => {
  285. alert("An error occured while fetching! More details can be found in the console (F12)\n" + err);
  286. console.error("dA_sort_gallery: Gallery fetching error:", err);
  287. }).finally(() => {
  288. isfetching = false;
  289. });
  290. }
  291.  
  292. function scrollPage(page) {
  293. slider.style.transform = `translate(-${(425*page)}px)`;
  294. if (page == 1) evSelect(null);
  295. }
  296.  
  297. function setProgress(bar, value, total) {
  298. if (total == 0 || bar == null) return;
  299. let perc = Math.ceil(value / total * 100);
  300. bar.dataset.label = `${value}/${total} (${perc}%)`;
  301. bar.getElementsByTagName("span")[0].style.width = perc + "%";
  302. }
  303.  
  304. function addStyle() {
  305. if (document.getElementById("dA_sort_gallery_style") != null) return;
  306. style = document.createElement("style");
  307. style.id = "dA_sort_gallery_style";
  308. style.innerHTML = `
  309. #dA_sort_gallery_buttonCont{display: flex;margin: 5px;font-size: small;color:#7579ff;fill:#7579ff;cursor:pointer}
  310. #dA_sort_gallery_buttonCont:hover{color: var(--D8);fill:currentColor;}
  311. #dA_sort_gallery_buttonCont svg{height:1em;margin:0 5px;}
  312. #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;}
  313. #dA_sort_gallery_dialog h3{text-align:center;font-size:x-large;margin-bottom:1em;}
  314. #dA_sort_gallery_dialog h4{text-align:left;font-size:large;margin-bottom:0.5em;}
  315. #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;}
  316. #dA_sort_gallery_dialog .dA_sort_gallery_buttons{display:flex;justify-content: space-around;}
  317. #dA_sort_gallery_dialog button{background:none;border:none;font-size:large;font-weight: bold;font-style: italic;color:#050;cursor:pointer;}
  318. #dA_sort_gallery_dialog button:hover{color:#370;}
  319. #dA_sort_gallery_dialog button:active{color:#770;}
  320. #dA_sort_gallery_dialog section{display: inline-flex;flex-direction: column;gap: 10px;width: 400px;margin-right: 20px;height:100%;}
  321. #dA_sort_gallery_dialog label{margin-right:20px;display:inline-block;}
  322. #dA_sort_gallery_fetching label{width:50%;}
  323. .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;}
  324. .dA_sort_gallery_progress:before { content: attr(data-label); font-size: 0.8em; position: absolute; text-align: center; top: 5px; left: 0; right: 0;}
  325. .dA_sort_gallery_progress span {background-color: #7cc4ff; display: inline-block; height: 100%;}
  326. #dA_sort_gallery_clearDB{font-size:normal;}
  327. #dA_sort_gallery_slider{height: 300px;width: 300%;transition: transform; transition-duration: 0.25s;}
  328. #dA_sort_gallery_imgPrev{flex:1;display:flex;gap:10px;height:75px;}
  329. #dA_sort_gallery_imgPrev img {align-self: center;object-fit: cover;width: 100%;max-height: 100%;}
  330. #dA_sort_gallery_dialog .disabled {color:#ccc;}
  331. `; //transform: translateX(-425px);
  332. document.head.appendChild(style);
  333. }
  334.  
  335. function addDialog() {
  336. if (document.getElementById("dA_sort_gallery_dialog") != null) return;
  337. dialog = document.createElement("div");
  338. dialog.id = "dA_sort_gallery_dialog";
  339. dialog.innerHTML = `
  340. <h3>Sorting a Gallery</h3>
  341. <div id="dA_sort_gallery_slider">
  342. <section id="dA_sort_gallery_fetching">
  343. <h4>Fetching Gallery Entries</h4>
  344. <div><label>Gallery folder:</label><span id='dA_sort_gallery_folderID'>0</span></div>
  345. <div><label>Folder entries:</label><span id='dA_sort_gallery_folderEntries'>0</span></div>
  346. <div><label>Database entries:</label><span id='dA_sort_gallery_dataEntries'>0</span></div>
  347. <div style="flex:1"><button id='dA_sort_gallery_clearDB'>Clear Database</button></div>
  348. <div id="dA_sort_gallery_fetchProgress" class="dA_sort_gallery_progress" data-label=""><span style="width:0%;"></span></div>
  349. <div class="dA_sort_gallery_buttons">
  350. <button id='dA_sort_gallery_cancel'>Cancel</button>
  351. <button id="dA_sort_gallery_fatch">Fetch Images</button>
  352. <button id="dA_sort_gallery_skip">Skip</button>
  353. </div>
  354. </section>
  355. <section id="dA_sort_gallery_sorting">
  356. <h4>Sorting Submissions</h4>
  357. <div>
  358. <label>Result</label>
  359. <select id="dA_sort_gallery_affected" title="After sorting, only the first # follow the rule">
  360. <option value="24">First 24</option>
  361. <option value="48">First 48</option>
  362. <option id='dA_sort_gallery_allchoice' value="all">All</option>
  363. </select>
  364. </div>
  365. <div>
  366. <label>Sort Property</label>
  367. <select id="dA_sort_gallery_target">
  368. <option value="publishedTime">Date</option>
  369. <option value="title">Name</option>
  370. <option value="views">Views</option>
  371. <option value="favs">Favourites</option>
  372. <option value="invert">Invert</option>
  373. </select>
  374. <select id="dA_sort_gallery_slope">
  375. <option value="desc">Descending</option>
  376. <option value="asc">Ascending</option>
  377. </select>
  378. </div>
  379. <div>Preview:</div>
  380. <div id="dA_sort_gallery_imgPrev">
  381. </div>
  382. <div id="dA_sort_gallery_sortingProgress" class="dA_sort_gallery_progress" data-label=""><span style="width:0%;"></span></div>
  383. <div class="dA_sort_gallery_buttons">
  384. <button id='dA_sort_gallery_cancel2'>Cancel</button>
  385. <button id="dA_sort_gallery_back">Back</button>
  386. <button id="dA_sort_gallery_sort">Sort</button>
  387. </div>
  388. </section>
  389. </div>
  390. `;
  391.  
  392. document.body.appendChild(dialog);
  393. progFetch = document.getElementById("dA_sort_gallery_fetchProgress");
  394. progSort = document.getElementById("dA_sort_gallery_sortingProgress");
  395. slider = document.getElementById("dA_sort_gallery_slider");
  396. prevCont = document.getElementById("dA_sort_gallery_imgPrev");
  397.  
  398. document.getElementById("dA_sort_gallery_cancel").addEventListener("click", function(ev) {
  399. dialog.style.display = "";
  400. }, false);
  401. document.getElementById("dA_sort_gallery_cancel2").addEventListener("click", function(ev) {
  402. dialog.style.display = "";
  403. }, false);
  404. document.getElementById("dA_sort_gallery_back").addEventListener("click", function(ev) {
  405. scrollPage(0);
  406. }, false);
  407. document.getElementById("dA_sort_gallery_fatch").addEventListener("click", evFetchFolder, false);
  408. document.getElementById("dA_sort_gallery_skip").addEventListener("click", (ev) => {
  409. if (fetchedDevs == 0) {
  410. alert("Please scan your gallery first!")
  411. } else
  412. scrollPage(1);
  413. }, false);
  414. document.getElementById("dA_sort_gallery_clearDB").addEventListener("click", () => {
  415. db = [];
  416. GM.setValue("db", JSON.stringify(db));
  417. document.getElementById("dA_sort_gallery_folderEntries").innerHTML = "0";
  418. document.getElementById("dA_sort_gallery_dataEntries").innerHTML = "0";
  419. }, false);
  420. document.getElementById("dA_sort_gallery_target").addEventListener("change", evSelect, false);
  421. document.getElementById("dA_sort_gallery_slope").addEventListener("change", evSelect, false);
  422. document.getElementById("dA_sort_gallery_sort").addEventListener("click", evSort, false);
  423. }
  424.  
  425. function init() {
  426. if (!/gallery/i.test(location.href)) return;
  427. username = /deviantart\.com\/(.*?)\/gallery/i.exec(location.href)[1];
  428.  
  429. if(document.querySelector("[dA_sort_gallery_img]")!=null)return
  430.  
  431. addStyle();
  432. addDialog();
  433.  
  434. let parCont=document.querySelector("#sub-folder-gallery svg:not([dA_sort_gallery_img])");
  435. if(parCont==null)return;
  436. parCont.setAttribute("dA_sort_gallery_img", 1);
  437. let sortBut=document.createElement("div");
  438. sortBut.id="dA_sort_gallery_buttonCont";
  439. sortBut.innerHTML=sortimg+"Sort";
  440. parCont.parentNode.parentNode.parentNode.after(sortBut);
  441.  
  442. sortBut.addEventListener("click", evInvokeClick, false);
  443.  
  444. GM.getValue("db").then((val) => {
  445. db = JSON.parse(val);
  446. db.forEach((el, ind, arr) => { arr[ind].reqDate = new Date(el.reqDate); });
  447. document.getElementById("dA_sort_gallery_dataEntries").innerHTML = db.length;
  448. });
  449. }
  450.  
  451.  
  452. const observer = new MutationObserver(init);
  453. observer.observe(document.body,{ childList: true, subtree: true });
  454. init();
  455. })();
  456.  
  457. /*
  458. request sort: POST: https://www.deviantart.com/_napi/shared_api/gallection/folders/update_deviation_order
  459. csrf_token "d7okysuxM7dW9__i.rk0w3p.aUFLMyo3Oa2uuKoCH6X68dSmTRvIi126lcBQJsxqdCI"
  460. deviationid 932351217
  461. folderid 84979945
  462. position 5
  463. type "gallery"
  464.  
  465. ###
  466. request entries
  467. GET https://www.deviantart.com/_napi/shared_api/gallection/contents?
  468. username=Dediggefedde&type=gallery
  469. &folderid=84979945
  470. &offset=0
  471. &limit=24
  472. &mature_content=true
  473. &csrf_token=d7okysuxM7dW9__i.rk0w3p.aUFLMyo3Oa2uuKoCH6X68dSmTRvIi126lcBQJsxqdCI
  474. */