nodejsAnywhereBetterPage

Convert anywhere file directory into thumbnail view with image/video preview, fullscreen viewer, and keyboard navigation.

当前为 2025-02-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name nodejsAnywhereBetterPage
  3. // @namespace http://leizingyiu.net/
  4. // @version 20250225
  5. // @description Convert anywhere file directory into thumbnail view with image/video preview, fullscreen viewer, and keyboard navigation.
  6. // @author leizingyiu
  7. // @match http://*.*:8000/*
  8. // @match https://*.*:8001/*
  9. // @license GNU AGPLv3
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. let currentIndex = 0;
  17. let fileList = [];
  18.  
  19. function isTargetPage() {
  20. const urlPath = window.location.pathname.toLowerCase();
  21. const isAnyWhereFileView = document.querySelector('#files') !== null;
  22. return isAnyWhereFileView;
  23. }
  24.  
  25. function setupLazyGifLoading() {
  26. const observer = new IntersectionObserver((entries, observer) => {
  27. entries.forEach(entry => {
  28. if (entry.isIntersecting) {
  29. const img = entry.target;
  30. const gifSrc = img.dataset.gifSrc;
  31.  
  32. if (gifSrc) {
  33. img.src = gifSrc;
  34. img.classList.remove('gif-lazy');
  35. img.classList.add('gif-loaded');
  36. observer.unobserve(img);
  37. }
  38. } else {
  39. const img = entry.target;
  40. if (img.classList.contains('gif-loaded')) {
  41. img.src = '';
  42. img.classList.remove('gif-loaded');
  43. img.classList.add('gif-lazy');
  44. }
  45. }
  46. });
  47. }, {
  48. rootMargin: '50px',
  49. threshold: 0.01
  50. });
  51.  
  52. return observer;
  53. }
  54.  
  55. function createThumbnailView() {
  56. const fileListElements = Array.from(document.querySelectorAll('#files li a'));
  57. fileList = fileListElements.map(file => ({
  58. url: file.href,
  59. name: file.querySelector('.name').textContent
  60. }));
  61.  
  62. const container = document.createElement('div');
  63. container.classList.add('thumbnail-container');
  64.  
  65. const lazyLoadObserver = setupLazyGifLoading();
  66.  
  67. fileList.forEach((file, index) => {
  68. const { url, name } = file;
  69.  
  70. const wrapper = document.createElement('div');
  71. wrapper.classList.add('thumbnail-wrapper');
  72.  
  73. let hintTxt='双击预览'
  74. if (url.match(/\.(png|jpe?g|gif|webp)$/i)) {
  75. const img = document.createElement('img');
  76. img.alt = name;
  77. img.setAttribute('draggable', 'false');
  78.  
  79. if (url.match(/\.gif$/i)) {
  80. img.classList.add('gif-lazy');
  81. img.dataset.gifSrc = url;
  82. const gifLabel = document.createElement('div');
  83. gifLabel.textContent = 'GIF';
  84. gifLabel.classList.add('gif-label');
  85. wrapper.appendChild(gifLabel);
  86. } else {
  87. img.src = url;
  88. }
  89.  
  90. wrapper.appendChild(img);
  91. wrapper.addEventListener('dblclick', () => openFullscreen(index));
  92.  
  93. if (url.match(/\.gif$/i)) {
  94. lazyLoadObserver.observe(img);
  95. }
  96.  
  97. } else if (url.match(/\.(mp4|webm|ogg)$/i)) {
  98. const video = document.createElement('video');
  99. video.src = url;
  100. video.classList.add('video');
  101. video.muted = true;
  102. video.playsInline = true;
  103. video.preload = 'metadata';
  104. video.onloadedmetadata = () => video.currentTime = 0.1;
  105. wrapper.appendChild(video);
  106.  
  107. wrapper.addEventListener('dblclick', () => openFullscreen(index));
  108.  
  109. } else {
  110. hintTxt='双击下载';
  111.  
  112. wrapper.addEventListener('dblclick', () => {
  113. window.location.href = url;
  114. });
  115.  
  116.  
  117. }
  118. const downloadTip = document.createElement('div');
  119. downloadTip.classList.add('downloadTip');
  120. downloadTip.textContent = hintTxt;
  121. downloadTip.classList.add('download_tip');
  122.  
  123. wrapper.appendChild(downloadTip);
  124.  
  125.  
  126. const caption = document.createElement('div');
  127. caption.textContent = name;
  128. caption.classList.add('thumbnail-caption');
  129. wrapper.appendChild(caption);
  130.  
  131. container.appendChild(wrapper);
  132. });
  133.  
  134. document.body.innerHTML = '';
  135. document.body.appendChild(container);
  136. }
  137.  
  138. // 打开全屏预览
  139. function openFullscreen(index) {
  140. currentIndex = index;
  141.  
  142. const overlay = document.createElement('div');
  143. overlay.classList.add('fullscreen-overlay');
  144.  
  145. const content = document.createElement(fileList[currentIndex].url.match(/\.(mp4|webm|ogg)$/i) ? 'video' : 'img');
  146. content.src = fileList[currentIndex].url;
  147. content.classList.add('fullscreen-content');
  148. content.setAttribute('draggable', 'false');
  149. if (content.tagName === 'VIDEO') {
  150. content.controls = true;
  151. content.autoplay = true;
  152. }
  153. overlay.appendChild(content);
  154.  
  155. const closeButton = document.createElement('div');
  156. closeButton.textContent = '×';
  157. closeButton.classList.add('close-button');
  158. closeButton.addEventListener('click', () => overlay.remove());
  159. overlay.appendChild(closeButton);
  160.  
  161. let scale = 1;
  162. let isDragging = false;
  163. let startX, startY;
  164.  
  165. content.addEventListener('wheel', (e) => {
  166. e.preventDefault();
  167. scale += e.deltaY > 0 ? -0.1 : 0.1;
  168. scale = Math.max(0.1, Math.min(scale, 5));
  169. content.style.transform = `scale(${scale})`;
  170. });
  171.  
  172. content.addEventListener('mousedown', (e) => {
  173. isDragging = true;
  174. startX = e.clientX - content.offsetLeft;
  175. startY = e.clientY - content.offsetTop;
  176. content.style.cursor = 'grabbing';
  177. });
  178.  
  179. document.addEventListener('mousemove', (e) => {
  180. if (isDragging) {
  181. const x = e.clientX - startX;
  182. const y = e.clientY - startY;
  183. content.style.left = `${x}px`;
  184. content.style.top = `${y}px`;
  185. }
  186. });
  187.  
  188. document.addEventListener('mouseup', () => {
  189. isDragging = false;
  190. content.style.cursor = 'grab';
  191. });
  192.  
  193. document.body.appendChild(overlay);
  194.  
  195.  
  196. document.addEventListener('keydown', handleKeyDown);
  197.  
  198. overlay.addEventListener('remove', () => {
  199. document.removeEventListener('keydown', handleKeyDown);
  200. });
  201.  
  202. overlay.addEventListener('dblclick',()=>{
  203. const overlay = document.querySelector('.fullscreen-overlay');
  204. if (overlay) {
  205. overlay.remove();
  206. }
  207. })
  208. }
  209.  
  210.  
  211.  
  212.  
  213. function handleKeyDown(event) {
  214. if (event.key === 'ArrowLeft') {
  215. showPrevious();
  216. } else if (event.key === 'ArrowRight') {
  217. showNext();
  218. }else if (event.key === 'Escape' || event.keyCode === 27) { // 检测 Esc 键
  219. const overlay = document.querySelector('.fullscreen-overlay');
  220. if (overlay) {
  221. overlay.remove();
  222. }
  223. }
  224. }
  225.  
  226. // 显示上一张
  227. function showPrevious() {
  228. if (currentIndex > 0) {
  229. currentIndex--;
  230. updateFullscreenContent();
  231. }
  232. }
  233.  
  234. // 显示下一张
  235. function showNext() {
  236. if (currentIndex < fileList.length - 1) {
  237. currentIndex++;
  238. updateFullscreenContent();
  239. }
  240. }
  241.  
  242. // 更新全屏内容
  243. function updateFullscreenContent() {
  244. const overlay = document.querySelector('.fullscreen-overlay');
  245. if (!overlay) return;
  246.  
  247. const content = overlay.querySelector('.fullscreen-content');
  248. const newUrl = fileList[currentIndex].url;
  249.  
  250. if (content.tagName === 'VIDEO' && !newUrl.match(/\.(mp4|webm|ogg)$/i)) {
  251. // 如果当前是视频但新内容是图片,则替换为图片
  252. const img = document.createElement('img');
  253. img.src = newUrl;
  254. img.classList.add('fullscreen-content');
  255. img.setAttribute('draggable', 'false');
  256. overlay.replaceChild(img, content);
  257. } else if (content.tagName === 'IMG' && newUrl.match(/\.(mp4|webm|ogg)$/i)) {
  258. // 如果当前是图片但新内容是视频,则替换为视频
  259. const video = document.createElement('video');
  260. video.src = newUrl;
  261. video.classList.add('fullscreen-content');
  262. video.controls = true;
  263. video.autoplay = true;
  264. overlay.replaceChild(video, content);
  265. } else {
  266. // 同类型内容更新
  267. content.src = newUrl;
  268. }
  269. }
  270.  
  271. // 样式化
  272. function styling() {
  273. const styleTag = document.createElement('style');
  274. styleTag.textContent = `
  275. .thumbnail-container {
  276. display: grid;
  277. grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  278. gap: 10px;
  279. padding: 10px;
  280. }
  281. .thumbnail-wrapper {
  282. position: relative;
  283. overflow: hidden;
  284. border: 1px solid #ddd;
  285. border-radius: 5px;
  286. text-align: center;
  287. cursor: pointer;
  288. max-height: 40vh;
  289. overflow-y: scroll;
  290. padding: 0 !important;
  291. margin: 0 !important;
  292. }
  293. .thumbnail-wrapper img,
  294. .thumbnail-wrapper video {
  295. width: 100%;
  296. height: auto;
  297. }
  298. .gif-label {
  299. position: absolute;
  300. top: 5px;
  301. right: 5px;
  302. background: rgba(0, 0, 0, 0.7);
  303. color: #fff;
  304. font-size: 12px;
  305. padding: 2px 5px;
  306. border-radius: 3px;
  307. }
  308. .thumbnail-caption {
  309. font-size: 12px;
  310. padding: 5px;
  311. position: sticky;
  312. bottom: 0;
  313. width: 96%;
  314. overflow: hidden;
  315. word-break: break-all;
  316. background: #ffffffaa;
  317. }
  318. .fullscreen-overlay {
  319. position: fixed;
  320. top: 0;
  321. left: 0;
  322. width: 100%;
  323. height: 100%;
  324. background-color: rgba(255, 255, 255, 1);
  325. display: flex;
  326. align-items: center;
  327. justify-content: center;
  328. z-index: 999999999;
  329. }
  330. .fullscreen-content {
  331. max-width: 90%;
  332. max-height: 90%;
  333. position: absolute;
  334. user-select: none;
  335. }
  336. .close-button {
  337. position: absolute;
  338. top: 10px;
  339. right: 20px;
  340. font-size: 30px;
  341. color: #fff;
  342. cursor: pointer;
  343. mix-blend-mode: difference;
  344. user-select: none;
  345. }
  346. .fullscreen-content.video {
  347. pointer-events: none;
  348. }
  349. .gif-lazy {
  350. opacity: 0;
  351. transition: opacity 0.3s ease-in-out;
  352. }
  353. .gif-loaded {
  354. opacity: 1;
  355. }
  356.  
  357. .download_tip{
  358. opacity:0;
  359.  
  360. transition: opacity 0.2s ease;
  361. position: absolute;
  362. width: 100%;
  363. text-align-last: center;
  364. top: 50%;
  365. transform: translate(0, -50%);
  366. z-index: 9999;
  367. height: 100%;
  368. display: flex;
  369. align-items: center;
  370. justify-content: space-around;
  371.  
  372. background: #ffffff99;
  373. backdrop-filter: blur(1px);
  374.  
  375. }
  376. .download_tip:hover{
  377. opacity:1;
  378. }
  379.  
  380.  
  381. /* 针对 Webkit 内核浏览器(如 Chrome、Edge、Safari) */
  382. ::-webkit-scrollbar {
  383. width: 6px; /* 水平滚动条的高度 */
  384. height: 6px; /* 垂直滚动条的宽度 */
  385. }
  386.  
  387. ::-webkit-scrollbar-track {
  388. background: transparent; /* 滚动条轨道背景 */
  389. }
  390.  
  391. ::-webkit-scrollbar-thumb {
  392. background-color: rgba(0, 0, 0, 0.3); /* 滚动条滑块颜色 */
  393. border-radius: 3px; /* 滚动条滑块圆角 */
  394. }
  395.  
  396. ::-webkit-scrollbar-thumb:hover {
  397. background-color: rgba(0, 0, 0, 0.5); /* 滑块悬停时的颜色 */
  398. }
  399.  
  400. /* 针对 Firefox 浏览器 */
  401. * {
  402. scrollbar-width: thin; /* 设置滚动条为细 */
  403. scrollbar-color: rgba(0, 0, 0, 0.3) transparent; /* 滑块颜色和轨道颜色 */
  404. }
  405.  
  406.  
  407. `;
  408. document.head.appendChild(styleTag);
  409. }
  410.  
  411. // 主逻辑
  412. if (!isTargetPage()) {
  413. console.log('Not a target page, exiting...');
  414. return;
  415. } else {
  416. styling();
  417. createThumbnailView();
  418. }
  419. })();