// ==UserScript== // @name 图库管理器 - 网页图片保存助手 // @namespace http://tampermonkey.net/ // @version 1.4 // @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: fixed; 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; opacity: 0; transition: opacity 0.3s; } .gallery-save-btn.visible { opacity: 1; } .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; } .settings-btn { background: none; border: none; font-size: 18px; cursor: pointer; color: #666; padding: 5px; border-radius: 4px; transition: background-color 0.2s; position: absolute; right: 10px; top: 10px; } .settings-btn:hover { background: #f0f0f0; color: #333; } `); // 全局变量 let currentImageUrl = ''; let currentImageTitle = ''; let galleries = []; let selectedGalleryPath = ''; let selectedFolderPath = ''; let saveBtn = null; // 全局唯一的按钮 let currentImg = null; // 当前悬停的图片 let hideTimer = null; // 隐藏按钮的定时器 // 初始化 function init() { // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupEventListeners); } else { setupEventListeners(); } } // 设置事件监听器 function setupEventListeners() { // 创建样式 const style = document.createElement('style'); style.textContent = ` .image-save-btn { position: fixed; /* 修改为fixed定位 */ background:rgb(231, 186, 165); color: white; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 10px rgba(0,0,0,0.2); transition: all 0.3s; z-index: 9999; opacity: 0; pointer-events: none; transition: opacity 0.3s, transform 0.3s; } .image-save-btn.visible { opacity: 1; pointer-events: auto; } .image-save-btn:hover { background: #218838; transform: scale(1.1); } `; document.head.appendChild(style); // 创建单个全局按钮 const saveBtn = document.createElement('button'); saveBtn.className = 'image-save-btn'; saveBtn.innerHTML = '💾'; saveBtn.title = '保存图片'; document.body.appendChild(saveBtn); // 存储当前悬停的图片和定时器 let currentImg = null; let hideTimer = null; // 更新按钮位置 function updateButtonPosition(img) { const rect = img.getBoundingClientRect(); // 定位在图片右侧中间位置 saveBtn.style.left = `${rect.right - 10}px`; saveBtn.style.top = `${rect.top + rect.height/2 - 20}px`; saveBtn.dataset.imageUrl = img.src; } // 显示按钮 function showButton(img) { // 清除之前的隐藏定时器 if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } currentImg = img; updateButtonPosition(img); saveBtn.classList.add('visible'); } // 隐藏按钮 function hideButton(delay = 2000) { if (hideTimer) clearTimeout(hideTimer); hideTimer = setTimeout(() => { saveBtn.classList.remove('visible'); currentImg = null; }, delay); } // 处理图片鼠标事件 function handleImageMouseOver(e) { const img = e.target; if (img.tagName !== 'IMG') return; // 更新按钮位置并显示 showButton(img); } function handleImageMouseOut(e) { const img = e.target; if (img.tagName !== 'IMG' || img !== currentImg) return; // 启动隐藏定时器 hideButton(); } // 窗口滚动时更新按钮位置 function handleWindowScroll() { if (currentImg) { updateButtonPosition(currentImg); } } // 窗口大小改变时更新按钮位置 function handleWindowResize() { if (currentImg) { updateButtonPosition(currentImg); } } // 为按钮添加点击事件 saveBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); if (currentImg) { currentImageUrl = currentImg.src; currentImageTitle = currentImg.alt || document.title || '未命名图片'; showGallerySelector(); } }); // 事件监听 document.addEventListener('mouseover', handleImageMouseOver, true); document.addEventListener('mouseout', handleImageMouseOut, true); window.addEventListener('scroll', handleWindowScroll); window.addEventListener('resize', handleWindowResize); // 阻止右键菜单 document.addEventListener('contextmenu', e => { if (e.target.tagName === 'IMG') { e.preventDefault(); } }, true); // 创建模态框 createModal(); } // 显示保存按钮 function showSaveButton(img) { // 清除之前的隐藏定时器 if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } currentImg = img; // 定位按钮在图片右上角 const rect = img.getBoundingClientRect(); saveBtn.style.left = (rect.right - 35) + 'px'; saveBtn.style.top = (rect.top + 5) + 'px'; saveBtn.classList.add('visible'); } // 隐藏保存按钮 function hideSaveButton(delay = 300) { if (hideTimer) { clearTimeout(hideTimer); } hideTimer = setTimeout(() => { saveBtn.classList.remove('visible'); currentImg = null; }, delay); } // 创建模态框 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.querySelector('#open-settings-btn').addEventListener('click', openSettings); // 点击模态框外部关闭 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 openSettings() { // 获取现有设置 const settings = getSettings(); // 创建设置模态框 const settingsModal = document.createElement('div'); settingsModal.id = 'settings-modal'; settingsModal.className = 'gallery-modal'; settingsModal.innerHTML = ` `; document.body.appendChild(settingsModal); // 绑定事件 const closeBtn = settingsModal.querySelector('.gallery-close'); const cancelBtn = settingsModal.querySelector('#settings-cancel'); const saveBtn = settingsModal.querySelector('#settings-save'); const closeSettings = () => { settingsModal.remove(); }; closeBtn.addEventListener('click', closeSettings); cancelBtn.addEventListener('click', closeSettings); settingsModal.addEventListener('click', (e) => { if (e.target === settingsModal) closeSettings(); }); saveBtn.addEventListener('click', () => { const rules = document.getElementById('large-image-rules').value; const enabled = document.getElementById('enable-large-image').checked; // 保存设置 GM_setValue('largeImageRules', rules); GM_setValue('enableLargeImage', enabled); alert('设置已保存'); closeSettings(); }); // 显示设置模态框 settingsModal.style.display = 'block'; } // 获取设置 function getSettings() { return { enableLargeImage: GM_getValue('enableLargeImage', true), largeImageRules: GM_getValue('largeImageRules', '') }; } // 尝试解析大图URL function tryParseLargeImage(originalUrl) { try { const settings = getSettings(); // 检查是否启用大图解析 if (!settings.enableLargeImage || !settings.largeImageRules) { return originalUrl; } return parseLargeImageUrl(originalUrl, settings.largeImageRules); } catch (error) { console.error('大图解析失败:', error); return originalUrl; } } // 解析大图URL function parseLargeImageUrl(originalUrl, rules) { const ruleLines = rules.split('\n').filter(line => line.trim() && !line.trim().startsWith('#')); for (const rule of ruleLines) { const parts = rule.split('->').map(part => part.trim()); if (parts.length !== 2) continue; const [pattern, replacement] = parts; try { // 如果URL包含模式,则进行替换 if (originalUrl.includes(pattern)) { // 简单替换模式 return originalUrl.replace(pattern, replacement); } } catch (error) { console.error('规则解析错误:', rule, error); } } return originalUrl; // 如果没有匹配的规则,返回原URL } // 显示图库选择器 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'); // 尝试解析大图URL const finalImageUrl = tryParseLargeImage(currentImageUrl); // 构造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: finalImageUrl, 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(); })();