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