// ==UserScript==
// @name Magic Eden批量选择器
// @namespace https://x.com/NotevenDe
// @version 3.1
// @description 批量选中Magic Eden上的NFT项目,支持部分选中、取消选择和智能刷新
// @author Not
// @match https://magiceden.io/*
// @match https://*.magiceden.io/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
let currentCollectionName = '';
let autoRefreshEnabled = true;
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
// 创建悬浮控制面板的样式
const panelStyle = `
position: fixed;
top: 50%;
right: 20px;
transform: translateY(-50%);
z-index: 10000;
background: linear-gradient(145deg, #3b82f6 0%, #1d4ed8 100%);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 12px 48px rgba(59, 130, 246, 0.25), 0 4px 20px rgba(59, 130, 246, 0.15);
min-width: 260px;
max-width: 320px;
transition: all 0.3s ease;
cursor: move;
backdrop-filter: blur(15px);
overflow: hidden;
`;
const headerStyle = `
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.1) 100%);
padding: 16px 20px 12px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
cursor: move;
user-select: none;
`;
const contentStyle = `
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
padding: 20px;
border-radius: 0 0 20px 20px;
`;
const buttonStyle = `
width: 100%;
border: none;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
`;
const primaryButtonStyle = `
${buttonStyle}
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
color: white;
border: 1px solid rgba(59, 130, 246, 0.3);
`;
const secondaryButtonStyle = `
${buttonStyle}
background: linear-gradient(135deg, #0ea5e9 0%, #0369a1 100%);
color: white;
border: 1px solid rgba(14, 165, 233, 0.3);
`;
const dangerButtonStyle = `
${buttonStyle}
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
border: 1px solid rgba(239, 68, 68, 0.3);
`;
const sliderStyle = `
width: 100%;
height: 8px;
border-radius: 4px;
background: linear-gradient(to right, #e2e8f0 0%, #cbd5e0 100%);
outline: none;
opacity: 0.9;
transition: opacity 0.2s;
margin: 12px 0;
cursor: pointer;
border: 1px solid rgba(59, 130, 246, 0.2);
`;
const sliderThumbStyle = `
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
cursor: pointer;
box-shadow: 0 3px 12px rgba(59, 130, 246, 0.4), 0 1px 4px rgba(59, 130, 246, 0.2);
transition: all 0.2s ease;
border: 2px solid white;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.6), 0 2px 8px rgba(59, 130, 246, 0.3);
}
input[type="range"]::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 3px 12px rgba(59, 130, 246, 0.4);
}
.dragging {
cursor: grabbing !important;
transform: rotate(2deg);
box-shadow: 0 15px 50px rgba(59, 130, 246, 0.3), 0 8px 25px rgba(59, 130, 246, 0.2);
}
`;
// 添加CSS样式到页面
function addCustomStyles() {
const style = document.createElement('style');
style.textContent = sliderThumbStyle;
document.head.appendChild(style);
}
// 获取NFT集合名称
function getCollectionName() {
const pathMatch = window.location.pathname.match(/\/ordinals\/marketplace\/([^\/]+)/);
if (pathMatch) {
return pathMatch[1];
}
const linkElement = document.querySelector('a[href*="/ordinals/marketplace/"] span');
if (linkElement) {
return linkElement.textContent.trim();
}
return 'Unknown Collection';
}
// 创建拖拽功能
function makeDraggable(panel) {
const header = panel.querySelector('#panel-header');
header.addEventListener('mousedown', (e) => {
isDragging = true;
panel.classList.add('dragging');
const rect = panel.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
function onMouseMove(e) {
if (!isDragging) return;
const x = e.clientX - dragOffset.x;
const y = e.clientY - dragOffset.y;
// 限制在窗口范围内
const maxX = window.innerWidth - panel.offsetWidth;
const maxY = window.innerHeight - panel.offsetHeight;
const constrainedX = Math.max(0, Math.min(x, maxX));
const constrainedY = Math.max(0, Math.min(y, maxY));
panel.style.left = constrainedX + 'px';
panel.style.top = constrainedY + 'px';
panel.style.right = 'auto';
panel.style.transform = 'none';
}
function onMouseUp() {
isDragging = false;
panel.classList.remove('dragging');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
}
// 创建控制面板
function createControlPanel() {
const panel = document.createElement('div');
panel.id = 'magic-eden-control-panel';
panel.style.cssText = panelStyle;
panel.innerHTML = `
<div id="panel-header" style="${headerStyle}">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<h3 style="margin: 0; color: white; font-size: 16px; font-weight: 700;">NFT批量选择器</h3>
<div id="collection-name" style="font-size: 12px; color: rgba(255,255,255,0.8); margin-top: 4px; word-break: break-word;">
${currentCollectionName}
</div>
</div>
<a href="https://x.com/NotevenDe" target="_blank" rel="noopener"
style="color: rgba(255,255,255,0.9); text-decoration: none; transition: all 0.2s ease; margin-left: 12px;"
onmouseover="this.style.color='white'; this.style.transform='scale(1.1)'"
onmouseout="this.style.color='rgba(255,255,255,0.9)'; this.style.transform='scale(1)'">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
</div>
</div>
<div style="${contentStyle}">
<div style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="color: #1e40af; font-size: 14px; font-weight: 500;">总数量</span>
<span id="total-count" style="color: #1e3a8a; font-weight: 700; font-size: 16px;">0</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="color: #1e40af; font-size: 14px; font-weight: 500;">可添加</span>
<span id="available-count" style="color: #0ea5e9; font-weight: 700; font-size: 16px;">0</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="color: #1e40af; font-size: 14px; font-weight: 500;">已选择</span>
<span id="selected-count" style="color: #ef4444; font-weight: 700; font-size: 16px;">0</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="color: #1e40af; font-size: 13px; font-weight: 500; display: block; margin-bottom: 8px;">
选择比例: <span id="percentage" style="color: #3b82f6; font-weight: 700;">0%</span>
</label>
<input type="range" id="selection-slider" min="0" max="100" value="0"
style="${sliderStyle}">
<div style="display: flex; justify-content: space-between; font-size: 11px; color: #64748b; margin-top: 4px; font-weight: 500;">
<span>0%</span>
<span>50%</span>
<span>100%</span>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
<button id="refresh-count" style="${secondaryButtonStyle}">
🔄 刷新统计
</button>
<div style="display: flex; gap: 8px;">
<button id="bulk-select" style="${primaryButtonStyle} flex: 1;" disabled>
➕ 选择
</button>
<button id="bulk-deselect" style="${dangerButtonStyle} flex: 1;" disabled>
➖ 取消
</button>
</div>
<div style="display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 8px;">
<label style="display: flex; align-items: center; font-size: 13px; color: #1e40af; cursor: pointer; font-weight: 500;">
<input type="checkbox" id="auto-refresh" checked
style="margin-right: 8px; width: 16px; height: 16px; accent-color: #3b82f6;">
自动刷新
</label>
</div>
</div>
<div id="status-message" style="margin-top: 12px; font-size: 12px; text-align: center; color: #1e40af; min-height: 16px; font-weight: 500; padding: 8px; background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(147, 197, 253, 0.15) 100%); border-radius: 8px; border: 1px solid rgba(59, 130, 246, 0.2);">
点击"刷新统计"开始
</div>
</div>
`;
document.body.appendChild(panel);
makeDraggable(panel);
return panel;
}
// 获取所有NFT项目的添加按钮(未选择的)
function getAddButtons() {
const buttons = document.querySelectorAll('button[data-test-id=""]');
return Array.from(buttons).filter(button => {
const svg = button.querySelector('svg');
if (!svg) return false;
const lines = svg.querySelectorAll('line');
if (lines.length !== 2) return false;
const line1 = lines[0];
const line2 = lines[1];
return (line1.getAttribute('x1') === '12' && line1.getAttribute('x2') === '12' &&
line2.getAttribute('y1') === '12' && line2.getAttribute('y2') === '12');
});
}
// 获取所有取消选择按钮(已选择的)
function getRemoveButtons() {
const selectedButtons = document.querySelectorAll('button[data-test-id="item-selected"]');
return Array.from(selectedButtons).filter(button => {
const svg = button.querySelector('svg');
if (!svg) return false;
const polyline = svg.querySelector('polyline');
if (polyline) {
const points = polyline.getAttribute('points');
return points && points.includes('20 6 9 17 4 12');
}
return false;
});
}
// 获取所有NFT项目
function getAllNFTItems() {
return document.querySelectorAll('[data-index]');
}
// 更新统计信息
function updateStats() {
const addButtons = getAddButtons();
const removeButtons = getRemoveButtons();
const totalItems = getAllNFTItems().length;
const availableCount = addButtons.length;
const selectedCount = removeButtons.length;
// 更新集合名称
currentCollectionName = getCollectionName();
const collectionElement = document.getElementById('collection-name');
if (collectionElement) {
collectionElement.textContent = currentCollectionName;
}
// 更新计数
document.getElementById('total-count').textContent = totalItems;
document.getElementById('available-count').textContent = availableCount;
document.getElementById('selected-count').textContent = selectedCount;
const slider = document.getElementById('selection-slider');
const selectCount = Math.floor(availableCount * (slider.value / 100));
document.getElementById('percentage').textContent = slider.value + '%';
// 更新按钮状态
const bulkSelectBtn = document.getElementById('bulk-select');
const bulkDeselectBtn = document.getElementById('bulk-deselect');
if (availableCount > 0) {
bulkSelectBtn.disabled = false;
bulkSelectBtn.textContent = selectCount > 0 ? `➕ 添加 ${selectCount}` : '➕ 添加 0';
} else {
bulkSelectBtn.disabled = true;
bulkSelectBtn.textContent = '➕ 无可添加';
}
if (selectedCount > 0) {
bulkDeselectBtn.disabled = false;
bulkDeselectBtn.textContent = `➖ 取消 ${selectedCount}`;
} else {
bulkDeselectBtn.disabled = true;
bulkDeselectBtn.textContent = '➖ 无已选择';
}
// 更新状态消息
let statusMessage = '';
if (totalItems === 0) {
statusMessage = '未找到NFT项目';
} else if (availableCount === 0 && selectedCount === 0) {
statusMessage = '正在加载NFT数据...';
} else {
statusMessage = `${currentCollectionName} - 可添加${availableCount}个,已选${selectedCount}个`;
}
document.getElementById('status-message').textContent = statusMessage;
return { totalItems, availableCount, selectedCount, selectCount };
}
// 刷新统计
function refreshCount() {
const refreshBtn = document.getElementById('refresh-count');
const originalText = refreshBtn.textContent;
refreshBtn.textContent = '🔄 统计中...';
refreshBtn.disabled = true;
setTimeout(() => {
const stats = updateStats();
refreshBtn.textContent = originalText;
refreshBtn.disabled = false;
console.log('统计更新:', stats);
}, 500);
}
// 随机选择指定数量的按钮
function getRandomButtons(buttons, count) {
if (count >= buttons.length) return buttons;
const shuffled = [...buttons].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
}
// 批量选择
function bulkSelect() {
const addButtons = getAddButtons();
const slider = document.getElementById('selection-slider');
const selectCount = Math.floor(addButtons.length * (slider.value / 100));
if (selectCount === 0) {
document.getElementById('status-message').textContent = '请选择要添加的数量';
return;
}
const confirmed = confirm(`确定要添加 ${selectCount}/${addButtons.length} 个NFT项目吗?`);
if (!confirmed) return;
executeOperation(getRandomButtons(addButtons, selectCount), '添加', '➕');
}
// 批量取消选择
function bulkDeselect() {
const removeButtons = getRemoveButtons();
if (removeButtons.length === 0) {
document.getElementById('status-message').textContent = '没有已选择的NFT';
return;
}
const confirmed = confirm(`确定要取消选择所有 ${removeButtons.length} 个NFT项目吗?`);
if (!confirmed) return;
executeOperation(removeButtons, '取消', '➖');
}
// 快速批量执行(优化版)
function executeOperation(buttons, actionName, icon) {
const bulkSelectBtn = document.getElementById('bulk-select');
const bulkDeselectBtn = document.getElementById('bulk-deselect');
const originalSelectText = bulkSelectBtn.textContent;
const originalDeselectText = bulkDeselectBtn.textContent;
bulkSelectBtn.disabled = true;
bulkDeselectBtn.disabled = true;
bulkSelectBtn.textContent = `${icon} ${actionName}中...`;
let completedCount = 0;
const totalCount = buttons.length;
// 快速批量模式:分批并发执行
const batchSize = 8; // 每批8个,平衡速度和稳定性
let currentBatch = 0;
function processBatch() {
const startIndex = currentBatch * batchSize;
const endIndex = Math.min(startIndex + batchSize, totalCount);
const currentButtons = buttons.slice(startIndex, endIndex);
// 并发点击当前批次的按钮
currentButtons.forEach((button, index) => {
setTimeout(() => {
try {
// 模拟鼠标悬停
const container = button.closest('.group');
if (container) {
container.classList.add('hover');
}
button.click();
completedCount++;
const progress = Math.round((completedCount / totalCount) * 100);
bulkSelectBtn.textContent = `${icon} ${actionName}中 ${progress}%`;
document.getElementById('status-message').textContent =
`快速${actionName}中... ${completedCount}/${totalCount}`;
// 如果所有操作完成
if (completedCount >= totalCount) {
bulkSelectBtn.textContent = `${icon} ${actionName}完成`;
document.getElementById('status-message').textContent =
`完成!快速${actionName}了 ${completedCount} 个NFT`;
setTimeout(() => {
bulkSelectBtn.textContent = originalSelectText;
bulkDeselectBtn.textContent = originalDeselectText;
refreshCount(); // 自动刷新统计
}, 1500);
}
} catch (error) {
console.error(`${actionName}失败:`, error);
completedCount++; // 确保进度继续
}
}, index * 15); // 每个按钮间隔15ms,快速执行
});
currentBatch++;
// 如果还有剩余批次,继续处理
if (currentBatch * batchSize < totalCount) {
setTimeout(processBatch, 80); // 批次间隔80ms
}
}
processBatch();
}
// 自动刷新功能
function setupAutoRefresh() {
let lastStats = { total: 0, available: 0, selected: 0 };
setInterval(() => {
if (!autoRefreshEnabled) return;
const panel = document.getElementById('magic-eden-control-panel');
if (!panel) return;
const addButtons = getAddButtons();
const removeButtons = getRemoveButtons();
const totalItems = getAllNFTItems().length;
const currentStats = {
total: totalItems,
available: addButtons.length,
selected: removeButtons.length
};
if (currentStats.total !== lastStats.total ||
currentStats.available !== lastStats.available ||
currentStats.selected !== lastStats.selected) {
lastStats = currentStats;
updateStats();
console.log('自动刷新: NFT状态变化', currentStats);
}
}, 2000);
}
// 初始化事件监听器
function initEventListeners() {
const slider = document.getElementById('selection-slider');
slider.addEventListener('input', updateStats);
document.getElementById('refresh-count').addEventListener('click', refreshCount);
document.getElementById('bulk-select').addEventListener('click', bulkSelect);
document.getElementById('bulk-deselect').addEventListener('click', bulkDeselect);
const autoRefreshCheckbox = document.getElementById('auto-refresh');
autoRefreshCheckbox.addEventListener('change', (e) => {
autoRefreshEnabled = e.target.checked;
console.log('自动刷新:', autoRefreshEnabled ? '开启' : '关闭');
});
}
// 等待页面加载完成
function waitForPageLoad() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPlugin);
} else {
initPlugin();
}
}
// 初始化插件
function initPlugin() {
const isNFTListPage = document.querySelector('[data-test-id="grid-container"]') !== null;
if (!isNFTListPage) {
return;
}
setTimeout(() => {
if (document.getElementById('magic-eden-control-panel')) {
return;
}
currentCollectionName = getCollectionName();
addCustomStyles();
createControlPanel();
initEventListeners();
setupAutoRefresh();
setTimeout(refreshCount, 1000);
console.log('Magic Eden批量选择器已加载 - 优化版');
}, 1000);
}
// 监听页面变化
function observePageChanges() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
const hasGridContainer = document.querySelector('[data-test-id="grid-container"]');
const hasPanel = document.getElementById('magic-eden-control-panel');
if (hasGridContainer && !hasPanel) {
setTimeout(initPlugin, 500);
} else if (!hasGridContainer && hasPanel) {
hasPanel.remove();
}
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 启动插件
waitForPageLoad();
observePageChanges();
})();