您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display on thumbnail that art was generated using AI!
// ==UserScript== // @name dA_showAIOnThumb // @namespace http://phi.pf-control.de // @version 2024-05-10_2 // @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_addStyle // @grant GM.setValue // @grant GM.getValue // @license MIT; http://opensource.org/licenses/MIT // @sandbox DOM // @noframes // ==/UserScript== (function() { 'use strict'; let settings = { //default setting values onlyOnHover: false, //bool, true: check all thumbs for AI automatically; false: check only on hover AITags: ["ai","aiart","dreamup","ai_generated","stable_diffusion"], //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, matureHandling:1 //{0:ignore, 1:blurr, 2:remove} }; let bounceInterval =100; //int [ms], avoid multiple activation at once, minimum time before activating script again //helper variables let fetchedIDs={}; let fetchedMatures={}; let debounceTimeout = null; // Debounce-Timer //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"] { /*thumbnail-link detected as AI*/ position: relative; } [isAIGenerated="1"]::after { /* AI tag image above thumbnail*/ content: "AI" !important; position: relative !important; left: 50% !important; top: -95% !important; padding: 5px !important; background: radial-gradient(ellipse at center, rgb(var(--g-bg-primary-rgb, 255, 255, 255)) 0%, rgb(var(--g-bg-primary-rgb, 255, 255, 255)) 60%, rgb(var(--green4-rgb, 0, 230, 150)) 65%, rgb(var(--green4-rgb, 0, 230, 150)) 70%, rgba(0, 0, 0, 0) 75%) !important; color: var(--g-typography-primary, black) !important; width: 15px !important; height: 15px !important; line-height: 15px !important; display: block !important; filter: opacity(70%) !important; transform: translateX(-50%) !important; } [isMature="2"] { visibility:hidden; } [isMature="1"]>* { filter: blur(10px); } [isMature="1"]:hover>* { filter: none; } [isMature="1"]::after { content: "M" !important; position: relative !important; left: 50% !important; top: -95% !important; padding: 5px !important; width: 15px !important; height: 15px !important; line-height: 15px !important; display: block !important; filter: opacity(70%) !important; transform: translateX(-50%) !important; background: radial-gradient(ellipse at center, rgb(var(--g-bg-primary-rgb, 255, 255, 255)) 0%, rgb(var(--g-bg-primary-rgb, 255, 255, 255)) 60%, red 65%, red 70%, rgba(0, 0, 0, 0) 75%) !important; color: var(--g-typography-primary, black) !important; } [isMature="1"]:hover::after{ content: none!important; } .dA_saiot_oldwatch [isAIGenerated="1"] { /*thumbnail link in /notifications/watch*/ position: absolute; } .dA_saiot_oldwatch [isAIGenerated="1"]::after { /* AI tag in /notifications/watch*/ top: 5% !important; visibility: visible; } #dA_saiot_notify p { /* Notification text*/ font-weight: bold; text-align: center; margin: 0; color: var(--g-typography-secondary, black); } #dA_saiot_notify { /* Notification Container*/ position: fixed; width: 400px; display: block; top: 0%; background-color: var(--g-bg-tertiary, white); padding: 10px; border-radius: 0 10px 10px 0; border: 1px solid var(--g-divider1, black); box-shadow: 1px 1px 2px var(--g-bg-primary, black); transition: left; transition-duration: 0.5s; transform: translateY(100%) translateY(10px); color: var(--g-typography-primary, black); } div.settings_form label { cursor: pointer; } .da_saiot_radiogroup { display: flex; gap: 10px; /* Abstand zwischen den Radio-Buttons */ align-items: center; /* Vertikale Zentrierung */ } .da_saiot_radiogroup input { margin-right: 5px; /* Abstand zwischen Radio-Button und Label */ } .da_saiot_radiogroup label { margin-right: 20px; /* Abstand zwischen den Labeln */ } /*Settings form headings*/ `); 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 fetch(`https://www.deviantart.com/_puppy/dadeviation/init?deviationid=${devID}&username=${username}&type=${type}&include_session=false&csrf_token=${token}`, { method: "GET", headers: { "accept": 'application/json, text/plain, */*', "content-type": 'application/json;charset=UTF-8' }, credentials: 'include' // Cookies! }).then(async (response) => { if (!response.ok) { throw response; // HTTP-Statuscode } try{ const result = await response.json(); // JSON parsen if (result.deviation === undefined || result.error !== undefined) { throw result; } return result; // Erfolgreiche Antwort }catch(ex){ console.log("dA_showAiOnThumb error: parsing resonce",response); throw response; } }); } //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 handled=false; let url=el.href; let dats=/deviantart.com\/(.*?)\/(.*?)\/.*?-(\d+)$/gi.exec(url); //[match, artis, type, id] extracted from URL if(!dats)return; if(fetchedIDs[dats[3]]!==undefined){ //cached results for dev ID el.setAttribute("isAIGenerated",fetchedIDs[dats[3]]); handled=true; } if( settings.matureHandling>0 && fetchedMatures[dats[3]]!==undefined){ el.setAttribute("isMature",fetchedMatures[dats[3]]); handled=true; } if(handled>0)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); }else{ el.setAttribute("isAIGenerated","0"); fetchedIDs[dats[3]]="0"; } if(res.deviation.isMature){ el.setAttribute("isMature",settings.matureHandling); fetchedMatures[dats[3]]=settings.matureHandling; }else{ fetchedMatures[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(){ //called on DOM change if(window.location.href.indexOf("/notifications/watch") > -1) document.body.classList.add("dA_saiot_oldwatch"); else document.body.classList.remove("dA_saiot_oldwatch"); 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"> <label class="l">Handle Mature Submissions</label> <div class='da_saiot_radiogroup'> <input type="radio" id="mature_ignore" name="matureSetting" value="0" ${settings.matureHandling === 0 ? 'checked="checked"' : ''}> <label for="mature_ignore">Ignore</label><br> <input type="radio" id="mature_blur" name="matureSetting" value="1" ${settings.matureHandling === 1 ? 'checked="checked"' : ''}> <label for="mature_blur">Blur</label><br> <input type="radio" id="mature_hide" name="matureSetting" value="2" ${settings.matureHandling === 2 ? 'checked="checked"' : ''}> <label for="mature_hide">Hide</label> </div> <small>Choose how to handle mature submissions: Ignore, blur them, or hide them completely.</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();}); settings.matureHandling = parseInt(document.querySelector('input[name="matureSetting"]:checked')?.value??1); setTimeout(() => { GM.setValue('settings',JSON.stringify(settings)); notify("Settings 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 && el.tagName=="A")thmb=el; if(!thmb && el.parentNode.tagName=="A")thmb=el.parentNode; 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; if(!thmb.style.height)thmb.style.height=thmb.offsetHeight+"px"; //workaround for using "position:relative;top:-95%;" 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 } }); } //delayed debounce to avoid calling it multiple times at once function debouncer(){ if (debounceTimeout) { //within bounce interval clearTimeout(debounceTimeout); } debounceTimeout = setTimeout(() => { init(); debounceTimeout = null; }, bounceInterval); } //loading settings GM.getValue("settings").then((res)=>{ if(res==null)return; try{ let savedSettings=JSON.parse(res); Object.entries(savedSettings).forEach(([key,val])=>{settings[key]=val}); }catch(ex){ console.log("dA_showAiOnThumb Error: loading settings",ex); } if(settings.hideAIThumbs){ GM_addStyle("div:has(>[isAIGenerated='1']){display:none!important;}"); } }).finally(()=>{ const observer = new MutationObserver(debouncer); observer.observe(document.body,{ childList: true, subtree: true }); debouncer(); }); })();