// ==UserScript== // @name za图库管理器 - 网页图片保存助手 // @namespace http://tampermonkey.net/ // @version 1.3 // @description 在网页图片上悬停时显示保存按钮,可选择图库并保存图片 // @author You // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @connect 127.0.0.1 // ==/UserScript== (function() { 'use strict'; // 添加样式 GM_addStyle(` .gallery-save-btn { position: absolute; background-color: #4CAF50; color: white; border: none; padding: 5px 10px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; cursor: pointer; z-index: 10000; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } .gallery-save-btn:hover { background-color: #45a049; } .gallery-modal { display: none; position: fixed; z-index: 10001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); } .gallery-modal-content { background-color: #2d2d2d; color: #ffffff; margin: 5% auto; padding: 20px; border: 1px solid #555; width: 80%; max-width: 600px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); max-height: 90vh; overflow-y: auto; } .gallery-modal-header { padding: 10px 0; border-bottom: 1px solid #555; display: flex; justify-content: space-between; align-items: center; } .gallery-modal-header h2 { color: #ffffff; margin: 0; } .gallery-modal-body { padding: 20px 0; } .gallery-modal-footer { padding: 10px 0; border-top: 1px solid #555; text-align: right; } .gallery-close { color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; } .gallery-close:hover, .gallery-close:focus { color: white; text-decoration: none; cursor: pointer; } .gallery-form-group { margin-bottom: 15px; } .gallery-form-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #ffffff; } .gallery-form-group select, .gallery-form-group input, .gallery-form-group textarea { width: 100%; padding: 8px; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; background-color: #3d3d3d; color: #ffffff; } .gallery-form-group select:focus, .gallery-form-group input:focus, .gallery-form-group textarea:focus { outline: none; border-color: #4CAF50; } .gallery-btn { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px; } .gallery-btn:hover { background-color: #45a049; } .gallery-btn-secondary { background-color: #666; } .gallery-btn-secondary:hover { background-color: #555; } .gallery-status { padding: 10px; margin: 10px 0; border-radius: 4px; } .gallery-status-error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .gallery-status-success { background-color: #4CAF50; color: white; border: 1px solid #45a049; } .folder-tree { border: 1px solid #555; border-radius: 4px; max-height: 200px; overflow-y: auto; margin-top: 5px; background-color: #3d3d3d; } .folder-item { padding: 5px 10px; cursor: pointer; display: flex; align-items: center; color: #ffffff; } .folder-item:hover { background-color: #4d4d4d; } .folder-item.selected { background-color: #4CAF50; color: white; } .folder-icon { margin-right: 5px; } .folder-children { margin-left: 20px; display: none; } .folder-children.expanded { display: block; } .folder-toggle { margin-right: 5px; cursor: pointer; width: 15px; display: inline-block; color: #ffffff; } `); // 全局变量 let currentImageUrl = ''; let currentImageTitle = ''; let galleries = []; let selectedGalleryPath = ''; let selectedFolderPath = ''; let currentButton = null; // 当前显示的按钮 // 初始化 function init() { // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupEventListeners); } else { setupEventListeners(); } } // 设置事件监听器 function setupEventListeners() { let hoverTimeout = null; // 使用事件委托处理图片悬停 document.addEventListener('mouseover', function(e) { if (e.target.tagName === 'IMG') { // 检查图片大小,跳过小图标 if (e.target.width >= 50 && e.target.height >= 50) { // 清除之前的定时器 if (hoverTimeout) { clearTimeout(hoverTimeout); } // 设置延迟显示按钮,避免在快速移动鼠标时频繁创建按钮 hoverTimeout = setTimeout(() => { showSaveButton(e.target); }, 300); } } }); // 处理鼠标离开图片时隐藏按钮 document.addEventListener('mouseout', function(e) { if (e.target.tagName === 'IMG') { // 清除之前的定时器 if (hoverTimeout) { clearTimeout(hoverTimeout); } // 延迟隐藏按钮,避免在图片间快速移动时闪烁 setTimeout(() => { if (currentButton) { const img = getCurrentImageForButton(); if (img !== e.target) { hideSaveButton(e.target); } } }, 100); } }); // 鼠标移动时检查是否需要隐藏按钮 document.addEventListener('mousemove', function(e) { // 如果有按钮显示,检查鼠标是否移出了当前图片 if (currentButton) { const img = getCurrentImageForButton(); if (img) { const rect = img.getBoundingClientRect(); const mouseX = e.clientX; const mouseY = e.clientY; // 如果鼠标不在图片上,隐藏按钮 if (mouseX < rect.left || mouseX > rect.right || mouseY < rect.top || mouseY > rect.bottom) { hideSaveButton(img); } } } }); // 创建模态框 createModal(); } // 获取当前按钮对应的图片元素 function getCurrentImageForButton() { if (!currentButton) return null; // 遍历所有图片,找到与按钮位置匹配的图片 const images = document.querySelectorAll('img'); for (let img of images) { if (img.hasAttribute('data-gallery-image')) { const rect = img.getBoundingClientRect(); const buttonRect = currentButton.getBoundingClientRect(); // 检查按钮是否在图片范围内 if (buttonRect.left >= rect.left && buttonRect.right <= rect.right && buttonRect.top >= rect.top && buttonRect.bottom <= rect.bottom) { return img; } } } return null; } // 显示保存按钮 function showSaveButton(img) { // 如果已经有一个按钮显示,先隐藏它 if (currentButton) { const currentImg = getCurrentImageForButton(); if (currentImg && currentImg !== img) { currentImg.removeAttribute('data-gallery-image'); currentButton.remove(); currentButton = null; } else if (currentImg === img) { // 如果是同一张图片,不需要重新创建按钮 return; } } // 创建保存按钮 const saveBtn = document.createElement('button'); saveBtn.className = 'gallery-save-btn'; saveBtn.textContent = '💾'; saveBtn.title = '保存到图库'; // 定位按钮在图片右上角 const rect = img.getBoundingClientRect(); saveBtn.style.left = (rect.right - 35) + 'px'; saveBtn.style.top = (rect.top + 5) + 'px'; saveBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); currentImageUrl = img.src; currentImageTitle = img.alt || document.title || '未命名图片'; showGallerySelector(); }); // 将按钮添加到页面 document.body.appendChild(saveBtn); currentButton = saveBtn; // 为图片添加标识,方便查找 img.setAttribute('data-gallery-image', 'true'); } // 隐藏保存按钮 function hideSaveButton(img) { // 直接隐藏按钮 if (currentButton) { currentButton.remove(); currentButton = null; } // 移除图片标识 if (img && img.hasAttribute('data-gallery-image')) { img.removeAttribute('data-gallery-image'); } } // 创建模态框 function createModal() { // 检查是否已经存在模态框 if (document.getElementById('gallery-modal')) { return; } const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); // 绑定事件 document.querySelector('.gallery-close').addEventListener('click', closeModal); document.querySelector('#gallery-cancel').addEventListener('click', closeModal); document.querySelector('#gallery-save').addEventListener('click', saveImageToGallery); // 点击模态框外部关闭 document.getElementById('gallery-modal').addEventListener('click', function(event) { if (event.target === this) { closeModal(); } }); // 监听图库选择变化 document.getElementById('gallery-select').addEventListener('change', function() { const selectedIndex = this.value; if (selectedIndex !== '' && galleries[selectedIndex]) { selectedGalleryPath = galleries[selectedIndex].path; loadFolderStructure(selectedGalleryPath); } }); } // 显示图库选择器 function showGallerySelector() { // 显示加载状态 showStatus('正在加载图库列表...', 'info'); // 获取图库列表 getGalleryList(function(galleryList) { galleries = galleryList; const select = document.getElementById('gallery-select'); select.innerHTML = ''; if (galleries.length === 0) { showStatus('未找到可用图库,请先在图库管理器中打开图库', 'error'); return; } galleries.forEach(function(gallery, index) { const option = document.createElement('option'); option.value = index; option.textContent = gallery.name; select.appendChild(option); }); // 默认选择第一个图库 if (galleries.length > 0) { select.value = 0; selectedGalleryPath = galleries[0].path; loadFolderStructure(selectedGalleryPath); } // 填充表单数据 document.getElementById('image-name').value = sanitizeFileName(currentImageTitle); document.getElementById('source-url').value = window.location.href; document.getElementById('image-notes').value = ''; // 隐藏状态信息 hideStatus(); // 显示模态框 document.getElementById('gallery-modal').style.display = 'block'; }); } // 加载文件夹结构 function loadFolderStructure(galleryPath) { showStatus('正在加载文件夹结构...', 'info'); GM_xmlhttpRequest({ method: 'GET', url: `http://127.0.0.1:7002/gallery/${encodeURIComponent(galleryPath)}/folders`, onload: function(response) { if (response.status === 200) { try { const folders = JSON.parse(response.responseText); renderFolderTree(folders, galleryPath); hideStatus(); } catch (e) { showStatus('解析文件夹结构失败', 'error'); } } else if (response.status === 404) { // 如果API不存在,创建默认根文件夹 renderFolderTree([{name: '/', path: galleryPath, isRoot: true}], galleryPath); hideStatus(); } else { showStatus('获取文件夹结构失败: ' + response.statusText, 'error'); } }, onerror: function() { // 如果API调用失败,创建默认根文件夹 renderFolderTree([{name: '/', path: galleryPath, isRoot: true}], galleryPath); hideStatus(); } }); } // 渲染文件夹树 function renderFolderTree(folders, rootPath) { const folderTree = document.getElementById('folder-tree'); folderTree.innerHTML = ''; // 构建文件夹树结构 const folderMap = {}; const rootFolders = []; // 初始化所有文件夹 folders.forEach(folder => { folder.children = []; folderMap[folder.path] = folder; // 如果是根目录,添加到根文件夹列表 if (folder.path === rootPath) { rootFolders.push(folder); } }); // 建立父子关系 folders.forEach(folder => { if (folder.path !== rootPath) { // 查找父文件夹 const parentPath = folder.path.substring(0, folder.path.lastIndexOf('/')); if (folderMap[parentPath]) { folderMap[parentPath].children.push(folder); } else { // 如果找不到父文件夹,添加到根文件夹列表 rootFolders.push(folder); } } }); // 递归渲染文件夹 rootFolders.forEach(folder => { const folderElement = createFolderElement(folder, rootPath, true); // 根节点默认展开 folderTree.appendChild(folderElement); }); // 获取上次选择的文件夹路径 const lastSelectedFolder = GM_getValue('lastSelectedFolder', ''); // 尝试选中上次选择的文件夹,如果没有则默认选中根目录 let folderSelected = false; if (lastSelectedFolder) { const folderItems = document.querySelectorAll('.folder-item'); for (let item of folderItems) { if (item.dataset.path === lastSelectedFolder) { item.click(); folderSelected = true; break; } } } // 如果没有选中任何文件夹,默认选中根目录 if (!folderSelected && rootFolders.length > 0) { selectedFolderPath = rootFolders[0].path; const firstItem = folderTree.querySelector('.folder-item'); if (firstItem) { firstItem.classList.add('selected'); } } } // 创建文件夹元素 function createFolderElement(folder, rootPath, isRoot = false) { const folderDiv = document.createElement('div'); const folderItem = document.createElement('div'); folderItem.className = 'folder-item'; folderItem.dataset.path = folder.path; // 创建切换按钮(如果有子文件夹) const toggleSpan = document.createElement('span'); toggleSpan.className = 'folder-toggle'; if (folder.children && folder.children.length > 0) { toggleSpan.textContent = isRoot ? '▼' : '▶'; // 根节点默认展开 } // 创建文件夹图标和名称 const iconSpan = document.createElement('span'); iconSpan.className = 'folder-icon'; iconSpan.textContent = '📁'; const nameSpan = document.createElement('span'); nameSpan.textContent = folder.path === rootPath ? '/ (根目录)' : folder.name; folderItem.appendChild(toggleSpan); folderItem.appendChild(iconSpan); folderItem.appendChild(nameSpan); folderItem.addEventListener('click', function(e) { e.stopPropagation(); // 如果有子文件夹且点击的是切换按钮或文件夹项本身 if (folder.children && folder.children.length > 0 && (e.target === toggleSpan || e.target === folderItem || e.target === iconSpan || e.target === nameSpan)) { const childrenDiv = this.nextElementSibling; if (childrenDiv && childrenDiv.classList.contains('folder-children')) { childrenDiv.classList.toggle('expanded'); toggleSpan.textContent = childrenDiv.classList.contains('expanded') ? '▼' : '▶'; } } // 清除其他选中状态 document.querySelectorAll('.folder-item').forEach(item => { item.classList.remove('selected'); }); // 设置当前项为选中 this.classList.add('selected'); selectedFolderPath = folder.path; // 保存选择的文件夹路径 GM_setValue('lastSelectedFolder', selectedFolderPath); }); folderDiv.appendChild(folderItem); // 如果有子文件夹,创建子文件夹容器 if (folder.children && folder.children.length > 0) { const childrenDiv = document.createElement('div'); childrenDiv.className = 'folder-children'; if (isRoot) { childrenDiv.classList.add('expanded'); // 根节点默认展开 } folder.children.forEach(child => { const childElement = createFolderElement(child, rootPath); childrenDiv.appendChild(childElement); }); folderDiv.appendChild(childrenDiv); } return folderDiv; } // 获取图库列表 function getGalleryList(callback) { GM_xmlhttpRequest({ method: 'GET', url: 'http://127.0.0.1:7002/galleries', onload: function(response) { if (response.status === 200) { try { const galleries = JSON.parse(response.responseText); callback(galleries); } catch (e) { showStatus('解析图库列表失败', 'error'); callback([]); } } else { showStatus('获取图库列表失败: ' + response.statusText, 'error'); callback([]); } }, onerror: function() { showStatus('无法连接到图库管理器,请确保图库管理器正在运行', 'error'); callback([]); } }); } // 关闭模态框 function closeModal() { document.getElementById('gallery-modal').style.display = 'none'; } // 保存图片到图库 function saveImageToGallery() { const imageName = document.getElementById('image-name').value; const sourceUrl = document.getElementById('source-url').value; const notes = document.getElementById('image-notes').value; if (!imageName) { showStatus('请输入图片名称', 'error'); return; } if (!selectedFolderPath) { showStatus('请选择目标文件夹', 'error'); return; } // 显示正在保存状态 showStatus('正在保存图片...', 'info'); // 构造API请求 const apiUrl = 'http://127.0.0.1:7002/save_image'; // 发送请求到图库管理器 GM_xmlhttpRequest({ method: 'POST', url: apiUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ imageUrl: currentImageUrl, folderPath: selectedFolderPath, imageName: imageName, sourceUrl: sourceUrl, notes: notes }), ignoreCache: true, onload: function(response) { if (response.status === 200) { try { const result = JSON.parse(response.responseText); if (result.success) { showStatus('保存成功', 'success'); // 立刻关闭模态框 setTimeout(function() { closeModal(); }, 1000); } else { showStatus('保存失败: ' + (result.error || '未知错误'), 'error'); } } catch (e) { showStatus('解析响应失败: ' + response.responseText, 'error'); } } else { try { const errorResult = JSON.parse(response.responseText); showStatus('保存失败: ' + (errorResult.error || response.statusText), 'error'); } catch (e) { showStatus('保存失败: ' + response.statusText, 'error'); } } }, onerror: function(response) { console.error('保存图片时发生错误:', response); showStatus('保存失败,请确保图库管理器正在运行并且网络连接正常', 'error'); } }); } // 显示状态信息 function showStatus(message, type) { const statusEl = document.getElementById('gallery-status'); if (!statusEl) return; statusEl.textContent = message; statusEl.className = 'gallery-status'; switch(type) { case 'error': statusEl.classList.add('gallery-status-error'); break; case 'success': statusEl.classList.add('gallery-status-success'); break; } statusEl.style.display = 'block'; } // 隐藏状态信息 function hideStatus() { const statusEl = document.getElementById('gallery-status'); if (statusEl) { statusEl.style.display = 'none'; } } // 清理文件名 function sanitizeFileName(name) { return name.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 100); } // 页面加载完成后初始化 init(); })();