Greasy Fork

Greasy Fork is available in English.

dA_showAIOnThumb

Display on thumbnail that art was generated using AI!

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
	});

})();