// ==UserScript==
// @name dA_fav_search
// @namespace http://tampermonkey.net/
// @version 2.0
// @description Search within favourites
// @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==
(function() {
'use strict';
let style = null; //CSS style injection HTMLElement
let db = []; //db entries for just this folder
let tdb = []; //temporary database during fetching
let dbMarks = {}; // db for marks: tag=>[list of devID]
let fetchedDevs = 0; //during fetching counter
let username; //your username here!
let folderid; //this folder id (-1=all)
let token; //security
let devCont = null; //container for deviations
let resultcont = null; //container for results
let pagination = null; //original pagination, hide when searching
let filteredRes = []; //search result;
let setdiag = null; //settings dialog
let resultContent = ""; //html code to display results
let totalDbEntries = 0; //amount of entries in internal db.
let activeMark = ""; //target mark in add/remove mark mode
let curReqCnt = 0; //counter for request delay inverval
let dbgFlg=false;
let svgRefresh = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150" style="width:20px;height:20px">
<path d="M 50 30 a 50 50 0 1 0 50 0" stroke="#000000" fill="transparent" stroke-width="15"/>
<polyline points="15,23 56,23 55,63" fill="transparent" stroke="#000000" stroke-width="15"/>
</svg>`;
let disTyp = { table: 0, flow: 1 };
let showOffset=0;
let offsetStep=100;
let settings = {
display: disTyp.table,
flowHeight: "200", //px, height of pictures in flow-mode
tableHeight: "200", //px, height of row in table-mode
progressive: false, //bool, scan only for new items
scanDelay: 0, //s, waiting time between requests
scanInterval: 1, //#, number of consecutive requests before waiting
};
function addStyle() { //CSS, one style tag
if (document.getElementById("dA_fav_search_style") != null) return;
style = document.createElement("style");
style.id = "dA_fav_search_style";
style.innerHTML = `
#dA_fav_search{position:relative;}
#dA_fav_search>*{margin:0 10px}
#dA_fav_search_status{cursor:default;}
#dA_fav_search_text{border-radius: 5px;padding: 5px;width:20vw;}
#dA_fav_search button{font-size: 20pt;padding: 0;line-height: 0.8em;vertical-align: middle;cursor: pointer;background: white;border-radius: 5px;box-shadow: -1px -1px 3px #777 inset;}
#dA_fav_search_results img{max-height:100%;max-width:100%;display:inline-block;}
#dA_fav_search_results .dA_search_fav_journal{display:inline-block;width:200px;height:100%;}
#dA_fav_search_results>*{background-color:#ccc3;}
#dA_fav_search_results .dA_search_fav_res_row.marked {box-shadow: 0 0 5px 5px red;}
#dA_fav_search_results.dA_search_fav_tableView div.dA_search_fav_res_row:first-child{height:1em;font-weight:bold;}
#dA_fav_search_results.dA_search_fav_tableView .dA_search_fav_res_row{
display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; margin: 5px; overflow: hidden; grid-gap: 10px;grid-auto-rows: /*1*//*1*/;
}
#dA_fav_search_results.dA_search_fav_flowView {display:flex;flex-wrap: wrap;}
#dA_fav_search_results.dA_search_fav_flowView .dA_search_fav_res_row{display:inline-block;height:/*2*//*2*/;max-width:50vw;position:relative;vertical-align:middle;min-width:100px;margin:5px;flex:1 max-content;text-align:center;}
#dA_fav_search_results.dA_search_fav_flowView .dA_search_fav_res_row>span{position: absolute;left: 5px;display: none;width: 95%;word-break: break-word;text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;color: white;}
#dA_fav_search_results.dA_search_fav_flowView .dA_search_fav_res_row:hover>span{display:inline;}
#dA_fav_search_results.dA_search_fav_flowView .dA_search_fav_res_row:hover img{filter: brightness(30%)}
#dA_fav_search_results.dA_search_fav_flowView .dA_search_fav_res_row>span:nth-of-type(1){top:5px;}
#dA_fav_search_results.dA_search_fav_flowView .dA_search_fav_res_row>span:nth-of-type(2){top:50%;}
#dA_fav_search_results.dA_search_fav_flowView .dA_search_fav_res_row>span:nth-of-type(3){bottom:5px;}
#dA_fav_search_settings{position: absolute;width: 200px;display: flex;flex-direction: column;z-index: 99;background-color: var(--g-bg-tertiary);right: 0;top: 120%;padding:5px;border-radius:5px;gap:10px;user-select:none;box-shadow: 2px 2px 2px black;}
#dA_fav_search_settings label {margin: 10px 0px 0px 0px;}
#dA_fav_search_format>div{display:inline-block;padding:2px;border:1px solid black;background-color:#aaa7;border-radius:5px;cursor:pointer;margin:5px;}
#dA_fav_search_format>div:hover{filter:brightness(120%);}
#dA_fav_search_format>div.active{background-color:#faa7;}
#dA_fav_search_settings button{font-size:16pt;margin: 5px 10%;padding: 5px;}
#dA_fav_search_markBar button[data-role="close"]{filter:sepia(100%) saturate(600%) brightness(70%) hue-rotate(300deg)}
#dA_fav_search_markBar button[data-role="add"]{filter: sepia(100%) saturate(500%) hue-rotate(50deg);}
#dA_fav_search_deleteButtons button {font-size: 10pt;margin: auto;}
#dA_fav_search_settings input[type="text"] {width: 20px;margin: 2px 7px;border-radius: 5px;text-align:center;}
#dA_fav_search_markBar {display:flex;flex-flow: row-reverse wrap;margin: -10px 0 10px;}
#dA_fav_search_markBar button {padding: 3px;margin: 2px;cursor: pointer;border-radius:2px;}
#dA_fav_search_markBar button.active {filter: sepia(100%) saturate(500%) hue-rotate(300deg);}
#dA_fav_search_marks {font-family: Courier New;padding: 4px!important;font-size: calc(20pt - 8px) !important;margin: 0!important;}
#dA_fav_search_more {display: block;width: 100%;height: 40px;line-height: 40px;text-align: center;cursor: pointer;margin: 20px;}
#dA_fav_search_more:hover{filter:brightness(120%);}
`;
// #dA_fav_search_settings button[data-role="marks"]{background-color:#fcc;}
document.head.appendChild(style);
applyStyle();
}
//requests favourites using new nAPI and GET requests.
//one request with per call, calls itself with changed offset when response "hasmore" is true
function reqEntries(offset,type) { //type "collection"=favourites, "gallery"=gallery
// console.log(`https://www.deviantart.com/_napi/shared_api/gallection/contents?username=${username}&type=${type}&folderid=${folderid}&include_session=false&offset=${offset}&limit=60&mature_content=true&${folderid==-1||folderid=="all"?"all_folder=true&":""}csrf_token=${token}`)
// console.log(`https://www.deviantart.com/_puppy/dashared/gallection/contents?username=${username}&type=${type}&offset=${offset}&limit=60&${folderid==-1||folderid=="all"?"all_folder=true&":""}da_minor_version=20230710&csrf_token=${token}`);
return new Promise(function(resolve, reject) {
GM.xmlHttpRequest({
method: "GET",
url: `https://www.deviantart.com/_napi/shared_api/gallection/contents?username=${username}&type=${type}&folderid=${folderid}&include_session=false&offset=${offset}&limit=60&mature_content=true&${folderid==-1||folderid=="all"?"all_folder=true&":""}csrf_token=${token}`,
onerror: function(response) {
reject("dA_fav_search request failed:", response);
},
onload: function(response) {
let resp = JSON.parse(response.responseText); //see script bottom for structure
if(dbgFlg)console.log(response, "csrf",token)
console.log(resp);
fetchedDevs += resp.results.length; //progress indicator
document.getElementById("dA_fav_search_status").innerHTML = `${fetchedDevs}/...`;
let disdb = resp.results.map((el) => { //extract information from response
let thumb = "";
let token = "";
let types = [];
try {
//extract thumbnail in preview quality
types = el.media.types.filter(tp => tp.c != null && tp.t == "preview");
if (el.media.token != null) { //extract security token if present
token = "?token=" + el.media.token[0];
}
if (el.media.baseUri == null) {
thumb = ""; //journal
} else if (types.length == 0) { //preview image, like for videos or flash files
thumb = el.media.baseUri + token;
} else { //normal case, see script bottom for composition
thumb = el.media.baseUri + types.slice(-1)[0].c.replace("<prettyName>", el.media.prettyName) + token;
}
} catch (ex) {
console.log("thumb error:", ex, el, types);
}
return { folderid: folderid, deviationId: el.deviationId, title: el.title, publishedTime: el.publishedTime, thumbUrl: thumb, author: el.author, url: el.url };
});
tdb = [].concat(tdb, disdb); //add result to demporary database
//stop when known deviations are detected:
if (settings.progressive) {
let oldIDs = db.map(el => el.deviationId); //list of old devids
let newEls = tdb.filter(el => !oldIDs.includes(el.deviationId)); //list of devids not yet in old list
if (newEls.length != tdb.length) { // some ids known
//break progressive
tdb = [].concat(newEls, db) //return old list +new elements
fetchedDevs += newEls.length - resp.results.length;
resolve(resp);
return;
} //otherwise: all new, continue scanning
}
//recursive call
if (resp.hasMore) {
let waits = 500;
if (++curReqCnt >= settings.scanInterval) {
waits = settings.scanDelay * 1e3;
curReqCnt = 0;
}
setTimeout(() => {
resolve(reqEntries(resp.nextOffset,type));
}, waits);
} else {
resolve(resp);
}
}
});
});
}
function replaceTmpl(text, mark, val) {
let rex = new RegExp("\\/\\*" + mark + "\\*\\/.*?\\/\\*" + mark + "\\*\\/", "ig");
return text.replace(rex, `/*${mark}*/${val}/*${mark}*/`);
}
function applyStyle() {
style.innerHTML = replaceTmpl(style.innerHTML, "1", settings.tableHeight + "px"); //table height
style.innerHTML = replaceTmpl(style.innerHTML, "2", settings.flowHeight + "px"); //table height
}
//updates entries for this folder in database.
function updateDB() {
GM.getValue("db", "").then(val => {
let rdb;
if (val == "") {
rdb = db;
} else {
rdb = JSON.parse(val).filter(el => el.folderid != folderid);
rdb = [].concat(rdb, db);
}
totalDbEntries = rdb.length;
GM.setValue("db", JSON.stringify(rdb));
})
}
function evRefresh(ev) {
tdb = [];
fetchedDevs = 0;
let type = /deviantart\.com\/.*?\/(favourites|gallery)\/?([^\/\?]*)/i.exec(location.href)[1];
if(type=="favourites")type="collection";
reqEntries(0,type).then((ret) => {
db = tdb;
updateDB();
console.log(ret);
document.getElementById("dA_fav_search_status").innerHTML = `${db.length}/${db.length}`;
alert(`Fetching favourites for this folder is done.\n${fetchedDevs} new entries acquired!`);
}).catch((err) => {
alert("An error occured while fetching! More details can be found in the console (F12)\n" + err);
console.log("Gallery fetching error:", err);
}).finally(() => {
//done
});
}
function displayResults() {
let cont = "";
resultContent = filteredRes.slice(0,showOffset+offsetStep).map(el => {
let date = (new Date(el.publishedTime)).toLocaleString()
if (el.thumbUrl == "") { //journal
return `<div class='dA_search_fav_res_row'><a href="${el.url}"><span class='dA_search_fav_journal'>${el.title}</span></a><span></span><span>${el.author.username}</span><span>${date}</span></div>`;
} else {
return `<div class='dA_search_fav_res_row' data-id=${el.deviationId}>
<a href="${el.url}" target="_blank" rel="noopener noreferrer">
<img src="${el.thumbUrl}" title="Preview"/>
</a>
<span>${el.title}</span>
<span><a href="https://www.deviantart.com/${el.author.username}" target="_blank" rel="noopener noreferrer">${el.author.username}</a></span>
<span>${date}</span>
</div>`;
}
}).join("");
document.getElementById("dA_fav_search_status").innerHTML = `${filteredRes.length}/${db.length}`;
switch (settings.display) {
case disTyp.table:
cont = "<div class='dA_search_fav_res_row'><span>Image</span><span>Title</span><span>Author</span><span>Time</span></div>" + resultContent;
resultcont.classList.remove("dA_search_fav_flowView");
resultcont.classList.add("dA_search_fav_tableView");
break;
case disTyp.flow:
cont = resultContent;
resultcont.classList.remove("dA_search_fav_tableView");
resultcont.classList.add("dA_search_fav_flowView");
break;
}
resultcont.innerHTML = cont+"<div id='dA_fav_search_more'>Load next "+offsetStep+"</div>";
resultcont.parentNode.parentNode.style.display="";
if (activeMark != "") markMarked();
document.getElementById("dA_fav_search_more").addEventListener("click",(ev)=>{
showOffset+=offsetStep;
displayResults();
},false);
}
function evSearch(ev) {
let val = ev.target.value;
showOffset=0;
if (val == "") { //no search, normal layout
devCont.style.display = "";
pagination.style.display = "";
resultcont.style.display = "none";
document.getElementById("dA_fav_search_status").innerHTML = `${db.length}/${db.length}`;
} else { //search, activate custom layout
devCont.style.display = "none";
pagination.style.display = "none";
resultcont.style.display = "";
//actual filter here!!!
let tosort=[];
let filts = val.split(",").map(req => {
let cont = req.trim();
if (cont.substr(0, 5) == "date:") {
return { type: "date", text: cont.substr(5) };
} else if (cont.substr(0, 7) == "author:") {
return { type: "author", text: cont.substr(7) };
} else if (cont.substr(0, 6) == "title:") {
return { type: "title", text: cont.substr(6) };
} else if (cont.substr(0, 1) == "#") {
return { type: "mark", text: cont.substr(1) };
} else if (cont.substr(0,5)=="sort:"){
if(cont.substr(5,1)=="!")
tosort.push({asc:-1,type:cont.substr(6)});
else{
tosort.push({asc:1,type:cont.substr(5)});
}
return{type:"", text: ""}
} else {
return { type: "misc", text: cont };
}
});
filteredRes = db.filter(el => {
let date = new Date(el.publishedTime);
for (let i = 0; i < filts.length; ++i) {
let fi = filts[i];
// let fcnt = filts.reduce((cnt, fi) => {
switch (fi.type) {
case "misc":
if (!(el.title.search(new RegExp(fi.text, "i")) != -1 ||
el.author.username.search(new RegExp(fi.text, "i")) != -1)){
return false;}
break;
case "title":
if (!(el.title.search(new RegExp(fi.text, "i")) != -1)){
return false;}
break;
case "author":
if (!(el.author.username.search(new RegExp(fi.text, "i")) != -1)){
return false;}
break;
case "mark":
if (dbMarks[fi.text] == null || !dbMarks[fi.text].includes(el.deviationId.toString())){
return false;}
break;
case "date":
if (fi.text.substr(0, 1) == "<") {
if (!(date <= new Date(fi.text.substr(1)))){
return false;}
} else if (fi.text.substr(0, 1) == ">") {
if (!(date >= new Date(fi.text.substr(1)))){
return false;}
} else {
if (!(date.toLocaleString().indexOf(fi.text) != -1)){
return false;}
}
break;
}
}
return true;
});
document.getElementById("dA_fav_search_status").innerHTML = `${filteredRes.length}/${db.length}`;
for(let i=0;i<tosort.length;++i){
if(tosort[i].type=="author"){
filteredRes.sort((x,y)=>{
return tosort[i].asc*(x.author.username.toLowerCase()<y.author.username.toLowerCase()?-1:x.author.username.toLowerCase()>y.author.username.toLowerCase()?1:0);
});
}else if(tosort[i].type=="title"){
filteredRes.sort((x,y)=>{
return tosort[i].asc*(x.title.toLowerCase()<y.title.toLowerCase()?-1:x.title.toLowerCase()>y.title.toLowerCase()?1:0);
});
}else if(tosort[i].type=="date"){
filteredRes.sort((x,y)=>{
return tosort[i].asc*(x.publishedTime>y.publishedTime?-1:x.publishedTime<y.publishedTime?1:0);
});
}
}
displayResults();
}
}
function evSettings() {
if (setdiag != null && setdiag.style.display != "none") {
setdiag.style.display = "none";
} else {
showSettings();
}
}
function evdSettingChange(ev) {
//event delegation
if (ev.target.id == "dA_fav_search_flowHeight") {
settings.flowHeight = ev.target.value;
applyStyle();
displayResults();
} else if (ev.target.id == "dA_fav_search_tableHeight") {
settings.tableHeight = ev.target.value;
applyStyle();
displayResults();
}
GM.setValue("settings", JSON.stringify(settings));
}
function showMarks() {
let bar = document.getElementById("dA_fav_search_markBar");
let els = bar.querySelectorAll("button[data-role='mark']");
els.forEach(e => e.remove());
const fragment = new DocumentFragment();
let mrk = document.createElement("button");
mrk.dataset.role = "mark";
Object.keys(dbMarks).forEach(el => {
mrk = mrk.cloneNode();
mrk.innerHTML = "#" + el;
mrk.dataset.mark = el;
mrk.className = "";
mrk.title="Click to set active.\nClick on deviations to add/remove the mark.\nClick the mark again to rename it.\nRename to an empty name to delete the mark."
if (activeMark != "" && mrk.dataset.mark == activeMark) {
mrk.className = "active";
}
fragment.append(mrk);
});
bar.append(fragment);
if(document.getElementById("dA_fav_search_results").style.display=="none"){
document.getElementById("dA_fav_search_text").value=".";
evSearch({target:{value:"."}});
}
}
function evMarkInputKeyUp(ev) {
let val = ev.target.value;
if (ev.keyCode === 13) { //enter
let ref = ev.target.dataset.ref;
if (ref != null && val == "") { //remove
delete dbMarks[ref];
activeMark = "";
ev.target.remove();
showMarks();
GM.setValue("dbMarks", JSON.stringify(dbMarks));
return;
} else if (ref != null && val != "") { //change name
if (ref == val || dbMarks[val] != null) { //no change or exists already
ev.target.remove();
showMarks();
return;
}
activeMark = val;
let arr = dbMarks[ref];
delete dbMarks[ref];
dbMarks[val] = arr;
ev.target.remove();
showMarks();
GM.setValue("dbMarks", JSON.stringify(dbMarks));
} else if (val != "" && dbMarks[val] == null) { //add
dbMarks[val] = [];
GM.setValue("dbMarks", JSON.stringify(dbMarks));
ev.target.remove();
activeMark = val;
showMarks();
}
} else if (ev.keyCode == 27) { //escape
ev.target.remove();
showMarks();
}
}
function evdMarkerbarClick(ev) {
//event delegation
let bar = document.getElementById("dA_fav_search_markBar");
if (ev.target.tagName != "INPUT") {
bar.querySelectorAll("input[type='text']").forEach(el => el.remove());
}
let el, siz;
switch (ev.target.dataset.role) {
case "add":
el = document.createElement("input");
el.type = "text";
bar.append(el);
el.addEventListener("keyup", evMarkInputKeyUp, false);
el.focus();
activeMark = "";
break;
case "mark":
if (activeMark == ev.target.dataset.mark) {
el = document.createElement("input");
siz = ev.target.getBoundingClientRect();
el.type = "text";
el.title="New mark name. Confirm with Enter key. Cancel with ESC key.";
el.value = ev.target.dataset.mark;
el.style.height = siz.height;
el.style.width = siz.width;
ev.target.after(el);
el.dataset.ref = ev.target.dataset.mark;
el.addEventListener("keyup", evMarkInputKeyUp, false);
el.focus();
break;
} else {
activeMark = ev.target.dataset.mark;
el = document.querySelector("button.active[data-role='mark']");
if (el != null) el.classList.remove("active");
ev.target.classList.add("active");
markMarked();
}
break;
case "close":
activeMark = "";
bar.style.display = "none";
markMarked();
break;
case null:
default:
return;
}
ev.stopPropagation();
ev.preventDefault();
}
function injectMarkBar() {
let bar = document.getElementById("dA_fav_search_markBar");
if (bar == null) {
let el = document.createElement("div");
el.id = "dA_fav_search_markBar";
el.innerHTML = "<button data-role='close' title='close bar'>X</button><button data-role='add' title='add new mark'>+</button>"
el.addEventListener("click", evdMarkerbarClick, true);
resultcont.parentNode.prepend(el);
} else {
bar.style.display = "";
}
showMarks();
}
function evdSettingClick(ev) {
//event delegation
let el;
switch (ev.target.dataset.role) {
case "format":
settings.display = parseInt(ev.target.dataset.type);
el = document.querySelector("#dA_fav_search_format div.active");
if (el != null) el.classList.remove("active");
document.querySelector("#dA_fav_search_format div[data-type='" + settings.display + "']").classList.add("active");
break;
case "close":
setdiag.style.display = "none";
break;
case "delFolder":
if (!confirm(`This action will remove all ${db.length} entries for this folder from the script database.\nContinue?`)) return;
db = [];
GM.getValue("db", "").then(val => {
let rdb;
if (val == "") {
rdb = [];
} else {
rdb = JSON.parse(val).filter(el => el.folderid != folderid);
}
GM.setValue("db", JSON.stringify(rdb));
displayResults();
})
break;
case "delAll":
if (!confirm(`This action will remove all ${totalDbEntries} entries from the script database.\nAny folder you want to search in will need to be indexed again.\nContinue?`)) return;
db = [];
GM.setValue("db", JSON.stringify(db));
break;
case "delMarks":
if (!confirm("This action will remove all stored marks.\nWarning: Deleting can not be reversed. You will need to input all marks again.\nContinue?")) return;
dbMarks = {};
GM.setValue("dbMarks", JSON.stringify(dbMarks));
break;
case "progressive":
settings.progressive = document.getElementById("dA_fav_search_progressive").checked;
GM.setValue("settings", JSON.stringify(settings));
return;
case "scanDelay":
case "scanInterval":
el = document.querySelector("#dA_fav_search_settings input[data-role='scanInterval']");
settings.scanInterval = parseInt(el.value) || 0;
if (settings.scanInterval < 1) settings.scanInterval = 1;
el.value = settings.scanInterval;
el = document.querySelector("#dA_fav_search_settings input[data-role='scanDelay']");
settings.scanDelay = parseInt(el.value) || 0;
if (settings.scanInterval < 0) settings.scanInterval = 0;
el.value = settings.scanDelay
calcEstimate();
GM.setValue("settings", JSON.stringify(settings));
return;
case null:
default:
return;
}
ev.stopPropagation();
ev.preventDefault();
displayResults();
GM.setValue("settings", JSON.stringify(settings));
}
function calcEstimate() {
let max = document.getElementById("dA_fav_search").parentNode.querySelector("[role='button'] span").innerText;
let est = max / 60 * 1; //60 entries per page, 1s per page request
est += Math.floor(Math.floor(max / 60) / settings.scanInterval) * settings.scanDelay; //each [scanInterval] pages add [scandelay] seconds
est /= 60.0; //minutes
est = Math.round(est * 10) / 10; //1 decimal digit formating
let el = document.getElementById("dA_fav_search_estimate");
el.innerHTML = `~ ${est} Minutes`;
el.title = `Estimated time to fetch ${max} deviations`;
}
function showSettings() {
if (setdiag == null || document.getElementById("dA_fav_search_settings") == null) {
setdiag = document.createElement("div");
setdiag.innerHTML = `
<label for='dA_fav_search_format'>Display Format</label>
<div id='dA_fav_search_format'>
<div data-role="format" data-type='${disTyp.table}'>Table</div>
<div data-role="format" data-type='${disTyp.flow}'>Flow</div>
</div>
<label for='dA_fav_search_flowHeight'>Flow Item Height</label>
<input type="range" min="100" max="500" value="${settings.flowHeight}" step=50 id="dA_fav_search_flowHeight">
<label for='dA_fav_search_tableHeight'>Table Row Height</label>
<input type="range" min="50" max="450" value="${settings.tableHeight}" step=50 id="dA_fav_search_tableHeight">
<label>Delete Database</label>
<div id="dA_fav_search_deleteButtons">
<button data-role="delFolder">Folder</button>
<button data-role="delAll">All Folders</button>
<button data-role="delMarks">Marks</button>
</div>
<div>
<input data-role='progressive' id='dA_fav_search_progressive' type="checkbox" ${settings.progressive?"checked='checked'":""}/>
<label for="dA_fav_search_progressive">Scan Only Newest</label>
</div>
<label>Scan Delay <span id='dA_fav_search_estimate'></span></label>
<div>
<input data-role='scanDelay' type="text" value='${settings.scanDelay}'/>s per
<input data-role='scanInterval' type="text" value='${settings.scanInterval}'/>page
</div>
<button data-role="close">Close</button>
`;
//<button data-role="marks">Manage Marks</button>
setdiag.id = "dA_fav_search_settings";
document.getElementById("dA_fav_search").append(setdiag);
document.getElementById("dA_fav_search_settings").addEventListener("click", evdSettingClick, true);
document.getElementById("dA_fav_search_settings").addEventListener("change", evdSettingChange, true);
} else {
setdiag.style.display = "";
}
let el = document.querySelector("#dA_fav_search_format div.active");
if (el != null) el.classList.remove("active");
document.querySelector("#dA_fav_search_format div[data-type='" + settings.display + "']").classList.add("active");
document.getElementById("dA_fav_search_flowHeight").value = settings.flowHeight;
document.getElementById("dA_fav_search_tableHeight").value = settings.tableHeight;
calcEstimate();
if (resultcont.style.display == "none") {
evSearch({ target: { value: "." } });
}
}
function addGUI() {
let el = document.createElement("div");
el.innerHTML = `<input type='text' placeholder='Search' id='dA_fav_search_text' title='Press Enter key to search.\nSeparate conditions with "," ("cat, brown").\nRegular expressions supported ("^dra.*t$").\nUsing no specifier searches in all fields (author,title,date).\nSpecifiers are "author:", "title:", "date:", "sort:" ("author:dediggefedde, title:dA")\n"date:" supports before < and after > and partial dates ("date:<2021-05").\nSearch for "marked" deviations with leading # ("#dragons").\nUse the "sort:" specifier and the field to sort the results ("sort:author").\nReverse the sorting with aleading "!" ("sort:!date").\nYou can sort multiple fields at once ("sort:author, sort:date").'/>
<span id='dA_fav_search_status'>0/0</span>
<button id='dA_fav_search_refresh' title='Build Index'>${svgRefresh}</button>
<button id='dA_fav_search_marks' title='Marks tagging'>#M</button>
<button id='dA_fav_search_setdiag' title='Settings'>...</button>
`;
el.id = "dA_fav_search";
document.querySelector("#sub-folder-gallery [role=button]").parentNode.parentNode.parentNode.append(el);
document.getElementById("dA_fav_search_refresh").addEventListener("click", evRefresh, false);
document.getElementById("dA_fav_search_text").addEventListener("change", evSearch, false);
document.getElementById("dA_fav_search_setdiag").addEventListener("click", evSettings, false);
document.getElementById("dA_fav_search_marks").addEventListener("click",(ev)=>{
let bar= document.getElementById("dA_fav_search_markBar");
if(bar==null||bar.style.display=="none"){
injectMarkBar();
}else{
activeMark = "";
bar.style.display = "none";
markMarked();
}
ev.stopPropagation();
ev.preventDefault();
},false);
devCont = document.querySelector("[data-testid='content_row']").parentNode.parentNode;
resultcont = document.createElement("div");
resultcont.id = "dA_fav_search_results";
resultcont.style.display = "none";
devCont.after(resultcont);
resultcont.addEventListener("click", evdResultClick, true);
}
function fetchGlobals() {
username = /deviantart\.com\/(.*?)\/(?:favourites|gallery)/i.exec(location.href)[1];
token = document.querySelector("input[name=validate_token]").value;
folderid = /deviantart\.com\/.*?\/(?:favourites|gallery)\/?([^\/\?]*)/i.exec(location.href)[1];
if (folderid == "all") folderid = "-1";
if (folderid == "") folderid = /deviantart\.com\/.*?\/(?:favourites|gallery)\/?([^\/]*)/i.exec(document.querySelector("div.ds-card-selected").parentNode.href)[1];
pagination = document.querySelector("#sub-folder-gallery>div>div:last-of-type");
if (pagination==null || (pagination.innerText.indexOf("Prev") == -1 && pagination.innerText.indexOf("Next") == -1)) pagination = { style: { display: "" } };
}
function markMarked() {
if (activeMark == "") {
document.querySelectorAll(".dA_search_fav_res_row.marked").forEach(el => {
el.classList.remove("marked");
});
} else {
document.querySelectorAll(".dA_search_fav_res_row").forEach(el => {
if (dbMarks[activeMark].includes(el.dataset.id)) {
el.classList.add("marked");
} else {
el.classList.remove("marked");
}
});
}
}
function evdResultClick(ev) {
if (activeMark == "") return;
let el = ev.target.closest(".dA_search_fav_res_row");
if (el == null) return;
ev.preventDefault();
ev.stopPropagation();
let id = el.dataset.id;
if (dbMarks[activeMark].indexOf(id) == -1) {
dbMarks[activeMark].push(id);
el.classList.add("marked");
} else {
dbMarks[activeMark] = dbMarks[activeMark].filter(el => el != id);
el.classList.remove("marked");
}
GM.setValue("dbMarks", JSON.stringify(dbMarks));
}
function init() {
if (location.href.search(/www.deviantart.com\/[^\/]+\/(?:favourites|gallery)($|\/)/i) == -1) {if(dbgFlg)console.log(1);return};
if (document.getElementById("dA_fav_search") != null) {if(dbgFlg)console.log(2);return;}
if (document.querySelector("#sub-folder-gallery [role=button]") == null){if(dbgFlg)console.log(3); return;}
if (document.querySelector("[data-testid='content_row']") == null){if(dbgFlg)console.log(4); return;}
addStyle();
addGUI();
fetchGlobals();
GM.getValue("db", "").then((val) => {
if (val == "") return;
db = JSON.parse(val);
totalDbEntries = db.length;
db = db.filter(el => el.folderid == folderid);
document.getElementById("dA_fav_search_status").innerHTML = `${db.length}/${db.length}`;
});
GM.getValue("settings", "").then(val => {
if (val == "") return;
let stoSet = JSON.parse(val);
Object.entries(stoSet).forEach(([key, val]) => { //only load present settings, keep default for new ones.
if (key in settings) settings[key] = val;
})
applyStyle();
})
GM.getValue("dbMarks", "").then(val => {
if (val == "") return;
dbMarks = JSON.parse(val);
})
}
setInterval(init, 1000);
})();
/*
GET
https://www.deviantart.com/_napi/shared_api/gallection/contents?
username=Dediggefedde&
type=collection&
folderid=-1&
offset=48&
limit=60&
mature_content=true&
all_folder=true&
csrf_token=cDRbk8Kai
https://www.deviantart.com/_napi/shared_api/gallection/contents?username=Dediggefedde&type=collection&folderid=-1&offset=48&limit=60&mature_content=true&all_folder=true&csrf_token=cDRbk8KaiVaute
return
"hasMore": true,
"nextOffset": 60,
"results": []
deviationId": 935383722,
"type": "image",
"typeId": 1,
"printId": null,
"url": "https://www.deviantart.com/natoli/art/Trust-935383722",
"title": "Trust",
"isJournal": false,
"isVideo": false,
"isPurchasable": false,
"isFavouritable": true,
"publishedTime": "2022-11-02T15:13:33-0700",
"isTextEditable": false,
"isBackgroundEditable": false,
"legacyTextEditUrl": null,
"isShareable": true,
"isCommentable": true,
"isFavourited": true,
"isDeleted": false,
"isMature": false,
"isDownloadable": true,
"isAntisocial": false,
"isBlocked": false,
"isPublished": true,
"isDailyDeviation": false,
"hasPrivateComments": false,
"hasNft": false,
"isDreamsofart": false,
"isAiUseDisallowed": false,
"blockReasons": [ ],
"author": {
"userId": 949579,
"useridUuid": "9ea3ebd1-5904-4ade-8023-77ee378b7049",
"username": "Natoli",
"usericon": "https://a.deviantart.net/avatars-big/n/a/natoli.gif?1",
"type": "regular",
"isWatching": true,
"isSubscribed": false,
"isNewDeviant": false
},
"stats": {
"comments": 1,
"favourites": 32,
"views": 502,
"downloads": 3
},
media": {
"baseUri": "https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/d5ccb67d-1d14-454f-ab94-7307c8a4a2a0/dcjujzo-60862fe9-6fda-4933-9a3f-b0456f0a909d.jpg",
"prettyName": "dragon_roasted_coffee__speedpaint_by_goldendruid_dcjujzo",
"token": [
"eyJ0eXAiOiJKV1QJWLwSdzp27Fb4",
"eyJ0eXAiOiJKV1QiLCDOoM9aSa1Qqq3HyXKw"
],
"types": [
{
"t": "150",
"r": 0,
"c": "/v1/fit/w_150,h_150,q_70,strp/<prettyName>-150.jpg",
"h": 150,
"w": 123,
"ss": [
{
"x": 2,
"c": "/v1/fit/w_300,h_300,q_70,strp/<prettyName>-150-2x.jpg"
}
]
},
{
wanted:
baseUri/tapes[i].c?token=token
*/