// ==UserScript==
// @name dA_showAIOnThumb
// @namespace http://phi.pf-control.de
// @version 2023-12-31
// @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
// ==/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="<p>dA_showAiOnThumb</p>"+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<thumbs.length;++j){
console.log("checked:",thumbs[j]);
if(!thumbs[j].hasAttribute("isAIGenerated")){ //only if at end of container/row (only rows?)
// el.parentNode.parentNode.insertBefore(thumbs[j].parentNode, el.parentNode.nextSibling);
console.log("at",el,"moved",thumbs[j]);
thumbs[j].parentNode.setAttribute("moved","1");
return;
}
}
console.log("not moved");
}
*/
//uses the da_ignore script (v2.2) to add AI making usernames automatically to an ignore list
function autoignoreNam(el){
if(!settings.autoIgnore)return;
let nam=el.parentNode.querySelector("[data-username]").dataset.username;
let ignoreEl=document.createElement("div");
ignoreEl.classList.add("dA_ignore_externalAddName");
ignoreEl.innerHTML=nam;
document.body.appendChild(ignoreEl);
}
//takes a thumbnail link element, extracts information, triggers request and adds isAIGenerated attribute
function checkAIGenerated(el){
if(el.hasAttribute("isAIGenerated"))return; //skip for items already checked
let url=el.href;
let dats=/deviantart.com\/(.*?)\/(.*?)\/.*?-(\d+)$/gi.exec(url); //[match, artis, type, id] extracted from URL
if(fetchedIDs[dats[3]]!=null){ //cached results for dev ID
el.setAttribute("isAIGenerated",fetchedIDs[dats[3]]);
return;
}
requestDevData(dats[3],dats[1],dats[2]).then((res)=>{ //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<bounceInterval)return;
antiBounce=dNow;
if (location.href.indexOf('https://www.deviantart.com/settings') == 0 && document.getElementById("dA_showAiOnThumb_Options")==null) {
if(!document.querySelector("#dA_saiot_notify")){
msgbox=document.createElement("div");
msgbox.id="dA_saiot_notify";
msgbox.style.left="-450px";
document.body.append(msgbox);
}
let menuPoint = document.createElement("li");
menuPoint.innerHTML='<a href="#">AI Thumbnail</a>';
menuPoint.id="dA_showAiOnThumb_Options";
document.getElementById("settings_public").parentNode.after(menuPoint);
menuPoint.firstChild.addEventListener("click",(ev)=>{
document.querySelector("a.active").classList.remove("active");
ev.target.classList.add("active");
document.querySelector('div.settings_form').innerHTML=`
<div class="fooview ch">
<div class="fooview-inner">
<h3>dA_showAIOnThumb Settings</h3>
<div class="altaltview altaltview-wider">
<div class="row">
<input ${ settings.onlyOnHover ? 'checked="checked"' : '' } type="checkbox" id="da_saiot_checkhover" class="icheckbox">
<label for="da_saiot_checkhover" class="l">Mark AI thumbs only on hover</label>
<br><small>Check and mark AI images only when moving the cursor over the Thumbnail. Can improve performance on slower computers/connections. Otherwise all thumbnail are checked and marked when the appear.</small>
</div>
<div class="row">
<input value='${ settings.AITags?settings.AITags.join(","):'' }' type="text" id="da_saiot_AITags" class="itext_uplifted" />
<label for="da_saiot_AITags" class="l">Tags that mark deviations as AI</label>
<br><small>Comma-separated. Deviations with tags in this list will be marked as AI-generated. If Deviantart marks a submission as AI-generated/assisted on its own, it will be marked in any case.</small>
</div>
<div class="row">
<input ${ settings.hideAIThumbs ? 'checked="checked"' : '' } type="checkbox" id="da_saiot_removeAI" class="icheckbox">
<label for="da_saiot_removeAI" class="l">Remove AI thumbs instead of marking them</label>
<br><small>Instead of marking AI thumbnails, this will remove them and leave an empty space in place.</small>
</div>
<div class="row">
<input ${ settings.autoIgnore ? 'checked="checked"' : '' } type="checkbox" id="da_saiot_autoIgnore" class="icheckbox">
<label for="da_saiot_removeAI" class="l">Automatically ignore users that post AI images</label>
<br><small>This requires the userscript <a href='https://www.deviantart.com/dediggefedde/art/dA-Ignore-455554874'>dA_ignore</a>! It will add users that have posted AI art automatically to the ignore-list of dA_ignore.</small>
</div>
</div>
<div class=" buttons ch hh " id="submit">
<div style="text-align:right" class="rr">
<a class="smbutton smbutton-green" href="javascript:void(0)"><span id="da_saiot_saveSettings">Save</span></a>
</div>
</div>
</div>
</div>
`;
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(`div[data-hook='deviation_std_thumb']>a[data-hook='deviation_link']:not([da_showaionthumb]),
div[data-testid='grid-row'] a[data-hook='deviation_link']:not([da_showaionthumb])`)];
thumbs.forEach(el=>{
el.setAttribute("da_showaionthumb",""); //mark thumb as checked
if(!settings.onlyOnHover){ //check all immediatelly
checkAIGenerated(el); //function will cancel if already checked
}else{ //check on mouseover
el.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);
});
})();