Skip to content

Instantly share code, notes, and snippets.

@machsix
Last active June 18, 2025 18:28
Show Gist options
  • Save machsix/e7c8a25b05609b95fbfdc87972088093 to your computer and use it in GitHub Desktop.
Save machsix/e7c8a25b05609b95fbfdc87972088093 to your computer and use it in GitHub Desktop.
Alist
// ==UserScript==
// @name Alist 图片预览增强
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 在 Alist 中加载列表后,点击悬浮按钮预览目录中的图片缩略图
// @author Kimi
// @match *://alist.nas.lan/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
// 缓存已获取过缩略图的路径
const thumbnailCache = {};
// 添加自定义样式
GM_addStyle(`
.preview-thumbnails {
position: fixed;
background: white;
border: 1px solid #ccc;
padding: 10px;
display: none;
z-index: 1000;
max-width: 600px; /* 调整悬浮窗大小 */
max-height: 400px;
overflow: auto;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.preview-thumbnails img {
width: 120px; /* 固定宽度 */
height: auto; /* 高度自适应 */
margin: 5px;
transition: transform 0.3s ease;
}
.preview-thumbnails img:hover {
transform: scale(1.2); /* 鼠标悬停时放大 */
}
.preview-button {
margin-left: 10px;
cursor: pointer;
color: blue;
text-decoration: underline;
}
.floating-preview-button {
position: fixed;
bottom: 20px;
left: 20px;
background: #007bff;
color: white;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
z-index: 10000;
}
`);
// 添加悬浮按钮
const floatingButton = document.createElement('button');
floatingButton.textContent = '加载图片预览';
floatingButton.className = 'floating-preview-button';
floatingButton.addEventListener('click', loadPreviews);
document.body.appendChild(floatingButton);
// 点击悬浮按钮后加载预览逻辑
function loadPreviews() {
const listItems = document.querySelectorAll('.list-item');
listItems.forEach((item) => {
addPreviewButton(item);
});
}
// 添加预览按钮
function addPreviewButton(listItem) {
const nameBox = listItem.querySelector('.name-box');
if (!nameBox.querySelector('.preview-button')) {
const button = document.createElement('span');
button.textContent = '预览图片';
button.className = 'preview-button';
button.addEventListener('mouseenter', (e) => showThumbnails(listItem, e));
button.addEventListener('mouseleave', () => hideThumbnails(listItem));
nameBox.appendChild(button);
}
}
// 显示缩略图
function showThumbnails(listItem, event) {
const nameElement = listItem.querySelector('.name');
const name = nameElement ? nameElement.textContent.trim() : '';
const path = getFullImagePath(name);
let previewDiv = listItem.querySelector('.preview-thumbnails');
if (!previewDiv) {
previewDiv = document.createElement('div');
previewDiv.className = 'preview-thumbnails';
document.body.appendChild(previewDiv); // 将悬浮窗添加到 body 中
if (thumbnailCache[path]) {
console.log('Using cached thumbnails for:', path);
renderThumbnails(previewDiv, thumbnailCache[path]);
} else {
// 获取缩略图
fetchThumbnails(path).then(thumbnails => {
thumbnailCache[path] = thumbnails; // 缓存结果
renderThumbnails(previewDiv, thumbnails);
}).catch(error => {
console.error('Error fetching thumbnails:', error);
});
}
// 调整悬浮窗位置
adjustPosition(previewDiv, event);
// 将悬浮窗存储在 listItem 上,方便后续访问
listItem.previewDiv = previewDiv;
// 添加鼠标离开悬浮窗时的关闭逻辑
previewDiv.addEventListener('mouseleave', () => {
if (previewDiv) {
previewDiv.style.display = 'none';
}
});
} else {
previewDiv.style.display = 'block';
adjustPosition(previewDiv, event);
}
}
// 隐藏缩略图
function hideThumbnails(listItem) {
const previewDiv = listItem.previewDiv; // 从 listItem 获取悬浮窗
if (previewDiv) {
previewDiv.style.display = 'none';
}
}
// 调整悬浮窗位置
function adjustPosition(div, event) {
const { clientX, clientY } = event;
const { innerWidth, innerHeight } = window;
const { width, height } = div.getBoundingClientRect();
// 计算悬浮窗位置,避免超出浏览器边界
const left = Math.min(clientX, innerWidth - width - 20); // 保留 20px 的边距
const top = Math.min(clientY, innerHeight - height - 20);
div.style.left = `${left}px`;
div.style.top = `${top}px`;
}
// 获取完整的图片路径并解码
function getFullImagePath(name) {
const currentPath = decodeURIComponent(window.location.pathname);
const fullPath = `${currentPath}/${name}`.replace(/\/+/g, '/');
return decodeURIComponent(fullPath);
}
// 渲染缩略图
function renderThumbnails(div, thumbnails) {
div.innerHTML = ''; // 清空内容
thumbnails.forEach(thumb => {
const img = document.createElement('img');
img.src = thumb;
div.appendChild(img);
});
div.style.display = 'block';
}
// 获取目录中的图片缩略图
function fetchThumbnails(path) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: '/api/fs/list',
data: JSON.stringify({
path: path,
password: '',
page: 1,
per_page: 0,
refresh: false
}),
headers: {
'Content-Type': 'application/json'
},
onload: function (response) {
try {
console.log('Response:', response.responseText); // 调试信息
const data = JSON.parse(response.responseText);
if (!data.data || !data.data.content) {
throw new Error('Invalid response format');
}
const thumbnails = data.data.content
.filter(item => item.thumb) // 筛选出有缩略图的项
.map(item => item.thumb); // 提取缩略图 URL
// 如果当前目录中没有图片,但所有项都是目录且只有一个目录,则递归进入该目录
if (thumbnails.length === 0 && data.data.content.length === 1 && data.data.content[0].is_dir) {
const nestedPath = `${path}/${data.data.content[0].name}`.replace(/\/+/g, '/');
console.log('No thumbnails found. Recursively fetching from nested directory:', nestedPath);
fetchThumbnails(nestedPath).then(nestedThumbnails => {
resolve(nestedThumbnails); // 递归调用的结果传递给 resolve
}).catch(reject);
} else {
resolve(thumbnails);
}
} catch (error) {
reject(error);
}
},
onerror: function (error) {
reject(new Error(`Network error: ${error.statusText}`));
}
});
});
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment