// ==UserScript==
// @name dA_showAIOnThumb
// @namespace http://phi.pf-control.de
// @version 2024-01-18
// @description Display on thumbnail that art was generated using AI!
// @author Dediggefedde
// @match *://*.deviantart.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=deviantart.com
// @grant GM.xmlHttpRequest
// @grant GM_addStyle
// @grant GM.setValue
// @grant GM.getValue
// @noframes
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
let settings = {
onlyOnHover: false, //bool, true: check all thumbs for AI automatically; false: check only on hover
AITags: ["ai","aiart","dreamup"], //tags that will mark an art as AI in addition to dA's own "created with AI tools" marker
hideAIThumbs: false, //remove AI thumbs instead of marking them
// moveOtherThumbs:false, //not implemented
autoIgnore:false
};
let bounceInterval =500; //int [ms], avoid multiple activation at once, minimum time before activating script again
//helper variables
let antiBounce=new Date();
let fetchedIDs={};
//style for isAIgenerated!
//checked items have the attribute. The value is "1" if they are AI generated, otherwise "0"
//here: AI text with white background and blue circle over the thumbnail with 70% transparency
GM_addStyle(`
[isAIGenerated="1"]{
position:relative;
}
[isAIGenerated="1"]::after {
content: "AI";
position: relative;
left: 50%;
top: -95%;
padding: 5px;
background: radial-gradient( ellipse at center, rgb(var(--g-bg-primary-rgb)) 0%, rgb(var(--g-bg-primary-rgb)) 60%, rgb(var(--green4-rgb)) 65%, rgb(var(--green4-rgb)) 70%, rgba(0,0,0,0) 75% );
color: var(--g-typography-primary);
width: 15px;
height: 15px;
line-height: 15px;
display: block;
filter: opacity(70%);
transform: translateX(-50%);
}
#dA_saiot_notify p {
font-weight: bold;
text-align: center;
margin: 0;
color: var(--g-typography-secondary);
}
#dA_saiot_notify{
position: fixed;
width: 400px;
display: block;
top: 0%;
background-color: var(--g-bg-tertiary);
padding: 10px;
border-radius: 0 10px 10px 0;
border: 1px solid var(--g-divider1);
box-shadow: 1px 1px 2px var(--g-bg-primary);
transition:left;
transition-duration:0.5s;
transform: translateY(100%) translateY(10px);
color: var(--g-typography-primary);
}
div.settings_form label{cursor:pointer;}
`);
let msgbox,viewtimer;
let thumbs;
function notify(text){
msgbox.innerHTML="
dA_showAiOnThumb
"+text;
msgbox.style.left="0px";
if(viewtimer!=null)clearTimeout(viewtimer);
viewtimer=setTimeout(()=>{msgbox.style.left="-450px";},2000);
}
//request deviation data. deviation id, username and type ("art") is in the url.
//include_session=false necessary
function requestDevData(devID, username,type){
let token=document.querySelector("input[name=validate_token]").value;
return new Promise((resolve, reject) =>{
GM.xmlHttpRequest({
method: "GET",
url: `https://www.deviantart.com/_puppy/dadeviation/init?deviationid=${devID}&username=${username}&type=${type}&include_session=false&csrf_token=${token}`,
headers: {
"accept": 'application/json, text/plain, */*',
"content-type": 'application/json;charset=UTF-8'
},
onerror: function(response) {
reject(response);
},
onload: async function(response) {
try{
let result=JSON.parse(response.response);
resolve(result);
}catch(ex){
reject(response);
}
}
});
});
}
/*not implemented. Not really working, very messy
function moveAIImgs(el){
let ind=thumbs.indexOf(el);
console.log("check move",el,ind,thumbs.length);
for(let j=ind+1;j{ //request of extented data from PUPPY-API
try{ //responce might be successfull but have other object members
if(res.deviation.isAiGenerated){ //extract and add information
el.setAttribute("isAIGenerated","1"); //set element information
fetchedIDs[dats[3]]="1"; //cache result for deviation id.
autoignoreNam(el);
// moveAIImgs(el);
}else{
el.setAttribute("isAIGenerated","0");
fetchedIDs[dats[3]]="0";
}
if(res.deviation.extended.tags!=null){
res.deviation.extended.tags.forEach(tg=>{
if(settings.AITags.includes(tg.name)){
el.setAttribute("isAIGenerated","1"); //set element information
fetchedIDs[dats[3]]="1"; //cache result for deviation id.
autoignoreNam(el);
// moveAIImgs(el);
}});
}
}catch(ex){
console.log("dA_showAIOnThumb Error 2",ex,res); //error code 2, exception and return from server
}
})
.catch(err=>{
console.log("dA_showAIOnThumb Error 3",err); //error code 3, error code from promise call
});
}
function init(mutationList, observer){ //called on DOM change
//debounce to avoid calling it multiple times at once
let dNow=new Date();
if(dNow-antiBounce{
document.querySelector("a.active").classList.remove("active");
ev.target.classList.add("active");
document.querySelector('div.settings_form').innerHTML=`
dA_showAIOnThumb Settings
`;
document.getElementById('da_saiot_saveSettings').addEventListener("click",(ev)=> {
settings.onlyOnHover = document.getElementById("da_saiot_checkhover").checked;
settings.hideAIThumbs = document.getElementById("da_saiot_removeAI").checked;
settings.autoIgnore = document.getElementById("da_saiot_autoIgnore").checked;
settings.AITags = document.getElementById("da_saiot_AITags").value.split(',').map((el)=>{return el.trim();});
setTimeout(() => {
GM.setValue('settings',JSON.stringify(settings));
notify("List saved!");
}, 0);
},false);
},false);
}
//check all thumbs which were not already checked
thumbs=[...document.querySelectorAll(`[data-testid="thumb"]:not([da_showaionthumb])`)];
thumbs.forEach(el=>{
let thmb=el.querySelector("a:not([data-username])");
if(!thmb)thmb=el.parentNode.querySelector("a");
if(!thmb)thmb=el.parentNode.parentNode.querySelector("a");
if(!thmb)thmb=el.parentNode.parentNode.parentNode.querySelector("a");
if(!thmb)return;
el.setAttribute("da_showaionthumb",""); //mark thumb as checked
if(!settings.onlyOnHover){ //check all immediatelly
checkAIGenerated(thmb); //function will cancel if already checked
}else{ //check on mouseover
thmb.addEventListener("mouseenter",(ev=>{
checkAIGenerated(ev.target);
}),false); //no bubbling
}
});
}
//new technique! checks DOM for mutation.
//might be better than setTimeout for idling and more responsive
//is triggered multiple times at once, maybe requires "debouncing"
//here actually not, since I mark all thumbnails and no expensive operation is done beside that
//technically, "debouncing" should cancel/delay first triggers and only use last one or have a delay to avoid missing things
//again, since I check all thumbnails and a lot of hovers/scrolls triggers mutation, it is probably fine. ^^'
GM.getValue("settings").then((res)=>{
if(res==null)return;
let savedSettings=JSON.parse(res);
Object.entries(savedSettings).forEach(([key,val])=>{settings[key]=val});
if(settings.hideAIThumbs){
GM_addStyle("div:has(>[isAIGenerated='1']){display:none!important;}");
}
}).finally(()=>{
const observer = new MutationObserver(init);
observer.observe(document.body,{ childList: true, subtree: true });
init(null,null);
});
})();