您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show avatars loaded from Supabase
// ==UserScript== // @name Tandro Avatar stealer // @namespace https://tandro.de/ // @icon https://i.ibb.co/BKwQLg9P/logo-small-4.png // @version 1.12 // @author ########## // @license MIT // @description Show avatars loaded from Supabase // @match https://tandro.de/* // @grant none // ==/UserScript== (function () { 'use strict'; const avatarHost = 'https://cyehwjytcqcjmsvprrgh.supabase.co/storage/v1/object/public/avatars/'; const seen = new Set(); let container = null; let avatarList = null; let header = null; // State for dragging let isDragging = false; let dragStartX = 0; let dragStartY = 0; let panelStartLeft = 0; let panelStartTop = 0; function createPanel() { if (container) return; container = document.createElement('div'); container.id = 'avatar-monitor'; Object.assign(container.style, { position: 'fixed', top: '80px', right: '80px', width: '320px', height: '420px', background: '#2a1a3d', border: '1px solid #4b367c', borderRadius: '8px', boxShadow: '0 4px 12px rgba(75, 54, 124, 0.7)', resize: 'both', overflow: 'hidden', padding: '0', zIndex: '9999', fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif', fontSize: '13px', userSelect: 'none', cursor: 'default', display: 'flex', flexDirection: 'column', color: '#ddd', }); // Header (sticky, fixed within panel) header = document.createElement('div'); Object.assign(header.style, { flexShrink: '0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 12px', backgroundColor: '#5e3b9a', color: '#eee', fontWeight: '600', fontSize: '14px', cursor: 'move', userSelect: 'none', borderTopLeftRadius: '8px', borderTopRightRadius: '8px', boxShadow: '0 2px 6px rgba(0,0,0,0.4)', position: 'relative', zIndex: '10', }); header.textContent = '🖼️ click to download'; // Close button inside header const closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.title = 'Schließen'; Object.assign(closeBtn.style, { fontSize: '18px', background: 'transparent', border: 'none', color: '#eee', cursor: 'pointer', padding: '0 6px', userSelect: 'none', lineHeight: '1', borderRadius: '4px', transition: 'background-color 0.2s', }); closeBtn.onmouseenter = () => closeBtn.style.backgroundColor = 'rgba(255,255,255,0.15)'; closeBtn.onmouseleave = () => closeBtn.style.backgroundColor = 'transparent'; closeBtn.onclick = () => { container.remove(); container = null; }; header.appendChild(closeBtn); container.appendChild(header); // Avatar list container scrolls separately avatarList = document.createElement('div'); Object.assign(avatarList.style, { flexGrow: '1', overflowY: 'auto', padding: '10px', display: 'flex', flexWrap: 'wrap', gap: '8px', backgroundColor: '#3a255f', borderBottomLeftRadius: '8px', borderBottomRightRadius: '8px', }); container.appendChild(avatarList); document.body.appendChild(container); // Set up dragging behavior on header header.addEventListener('mousedown', dragStart); } // Drag event handlers function dragStart(e) { if (e.target.tagName === 'BUTTON') return; isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; // Get current left/top as numbers, fallback if style not set const rect = container.getBoundingClientRect(); panelStartLeft = rect.left; panelStartTop = rect.top; // Change cursor globally and disable text selection document.body.style.userSelect = 'none'; document.body.style.cursor = 'move'; window.addEventListener('mousemove', dragMove); window.addEventListener('mouseup', dragEnd); } function dragMove(e) { if (!isDragging) return; const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; // Update container position (constrain to viewport) let newLeft = panelStartLeft + dx; let newTop = panelStartTop + dy; // Prevent going too far off screen (you can adjust these limits) const maxLeft = window.innerWidth - container.offsetWidth; const maxTop = window.innerHeight - container.offsetHeight; newLeft = Math.min(Math.max(0, newLeft), maxLeft); newTop = Math.min(Math.max(0, newTop), maxTop); Object.assign(container.style, { left: `${newLeft}px`, top: `${newTop}px`, right: 'auto', // disable right positioning once dragged }); } function dragEnd() { isDragging = false; document.body.style.userSelect = ''; document.body.style.cursor = ''; window.removeEventListener('mousemove', dragMove); window.removeEventListener('mouseup', dragEnd); } // Add avatar images if not yet shown function addAvatar(url) { if (seen.has(url)) return; seen.add(url); createPanel(); const img = document.createElement('img'); img.src = url; Object.assign(img.style, { width: '64px', height: '64px', objectFit: 'cover', borderRadius: '6px', border: '1px solid #ccc', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', cursor: 'default', }); img.title = url.split('/').pop(); avatarList.appendChild(img); } // Scan page for avatars function scanAndAddAvatars() { document.querySelectorAll('img').forEach(img => { if (img.src.startsWith(avatarHost)) { addAvatar(img.src); } }); } // Watch DOM for new avatars added dynamically function watchForAvatars() { scanAndAddAvatars(); const observer = new MutationObserver(mutations => { for (const m of mutations) { for (const node of m.addedNodes) { if (node.tagName === 'IMG' && node.src.startsWith(avatarHost)) { addAvatar(node.src); } else if (node.querySelectorAll) { node.querySelectorAll('img').forEach(img => { if (img.src.startsWith(avatarHost)) { addAvatar(img.src); } }); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } // Helper to generate random filename function randomFilename(extension = 'png') { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let randomStr = ''; for (let i = 0; i < 6; i++) { randomStr += chars.charAt(Math.floor(Math.random() * chars.length)); } return `avatar_${randomStr}.${extension}`; } // Add click-to-download with EXIF strip for avatars function addAvatar(url) { if (seen.has(url)) return; seen.add(url); createPanel(); const img = document.createElement('img'); img.src = url; Object.assign(img.style, { width: '64px', height: '64px', objectFit: 'cover', borderRadius: '6px', border: '1px solid #ccc', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', cursor: 'pointer', // clickable now }); img.title = url.split('/').pop(); // On click, download stripped & randomized filename img.addEventListener('click', async () => { try { const response = await fetch(url, {cache: 'no-store'}); if (!response.ok) throw new Error('Network response was not ok'); const blob = await response.blob(); // Create ImageBitmap for canvas drawing (strip EXIF) const imageBitmap = await createImageBitmap(blob); const canvas = document.createElement('canvas'); canvas.width = imageBitmap.width; canvas.height = imageBitmap.height; const ctx = canvas.getContext('2d'); ctx.drawImage(imageBitmap, 0, 0); // Convert canvas to blob (PNG) — this strips EXIF canvas.toBlob((strippedBlob) => { if (!strippedBlob) { alert('Failed to process image.'); return; } const downloadUrl = URL.createObjectURL(strippedBlob); const a = document.createElement('a'); a.href = downloadUrl; // Try to preserve extension or use png if unknown let ext = 'png'; const parts = url.split('.'); if (parts.length > 1) { const possibleExt = parts.pop().toLowerCase(); if (['png','jpg','jpeg','gif','webp','bmp'].includes(possibleExt)) { ext = possibleExt === 'jpeg' ? 'jpg' : possibleExt; } } a.download = randomFilename(ext); document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(downloadUrl); }, 'image/png'); } catch (err) { alert('Download failed: ' + err.message); } }); avatarList.appendChild(img); } // Insert toggle button in sidebar function createToggleButton() { if (document.getElementById('avatar-toggle-btn')) return; const btn = document.createElement('button'); btn.id = 'avatar-toggle-btn'; Object.assign(btn.style, { position: 'fixed', top: '1px', left: '1px', zIndex: '10000', width: '60px', height: '60px', padding: '0', border: 'none', borderRadius: '6px', cursor: 'pointer', backgroundColor: 'transparent', boxShadow: '0 2px 6px rgba(0,0,0,0.2)', }); // Create img inside button const img = document.createElement('img'); img.src = 'https://i.ibb.co/BKwQLg9P/logo-small-4.png'; // img.alt = 'start stealer'; Object.assign(img.style, { width: '100%', height: '100%', display: 'block', borderRadius: '6px', }); btn.appendChild(img); btn.addEventListener('mouseenter', () => { btn.style.filter = 'brightness(0.8)'; }); btn.addEventListener('mouseleave', () => { btn.style.filter = 'none'; }); btn.addEventListener('click', () => { if (container && document.body.contains(container)) { container.remove(); container = null; } else { createPanel(); scanAndAddAvatars(); } }); document.body.appendChild(btn); } createToggleButton(); insertSidebarButton(); watchForAvatars(); })();