nodejsAnywhereBetterPage

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

  1. // ==UserScript==
  2. // @name nodejsAnywhereBetterPage
  3. // @namespace http://leizingyiu.net/
  4. // @version 20250305
  5. // @description Convert nodejs_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. const originHtml = document.getElementsByTagName('html')[0].outerHTML;
  14.  
  15. function globalKeyDown(event) {
  16. console.log(event);
  17. if (event.ctrlKey && event.altKey && event.code === 'KeyS') {
  18.  
  19. const content = originHtml.replace('</html>', '') + `<script>(${String(nodejsAnywhereBetterPage)})()</script><\/html>`;
  20. downloadThisHTML(content);
  21.  
  22. }
  23. }
  24.  
  25. function downloadThisHTML(content) {
  26. const blob = new Blob([content], { type: 'text/html' });
  27.  
  28. const link = document.createElement('a');
  29. link.href = URL.createObjectURL(blob);
  30. link.download = 'index.html';
  31.  
  32. link.click();
  33.  
  34. URL.revokeObjectURL(link.href);
  35. }
  36.  
  37.  
  38. function nodejsAnywhereBetterPage() {
  39. if (document.body.hasAttribute('yiu_nodejsAnywhereBetterPage')) {
  40. return;
  41. }
  42. 'use strict';
  43. let currentIndex = 0;
  44. let fileList = [];
  45.  
  46. function isTargetPage() {
  47. const urlPath = window.location.pathname.toLowerCase();
  48. const isAnyWhereFileView = document.querySelector('#files') !== null;
  49. return isAnyWhereFileView;
  50. }
  51.  
  52. function setupLazyGifLoading() {
  53. const observer = new IntersectionObserver((entries, observer) => {
  54. entries.forEach(entry => {
  55. if (entry.isIntersecting) {
  56. const img = entry.target;
  57. const gifSrc = img.dataset.gifSrc;
  58.  
  59. if (gifSrc) {
  60. img.src = gifSrc;
  61. img.classList.remove('gif-lazy');
  62. img.classList.add('gif-loaded');
  63. observer.unobserve(img);
  64. }
  65. } else {
  66. const img = entry.target;
  67. if (img.classList.contains('gif-loaded')) {
  68. img.src = '';
  69. img.classList.remove('gif-loaded');
  70. img.classList.add('gif-lazy');
  71. }
  72. }
  73. });
  74. }, {
  75. rootMargin: '50px',
  76. threshold: 0.01
  77. });
  78.  
  79. return observer;
  80. }
  81.  
  82. function createThumbnailView() {
  83. const fileListElements = Array.from(document.querySelectorAll('#files li a'));
  84. fileList = fileListElements.map(file => ({
  85. url: file.href,
  86. name: file.querySelector('.name').textContent
  87. }));
  88.  
  89. const container = document.createElement('div');
  90. container.classList.add('thumbnail-container');
  91.  
  92. const lazyLoadObserver = setupLazyGifLoading();
  93.  
  94. fileList.forEach((file, index) => {
  95. const { url, name } = file;
  96.  
  97. const wrapper = document.createElement('div');
  98. wrapper.classList.add('thumbnail-wrapper');
  99.  
  100. let hintTxt = '双击预览'
  101. if (url.match(/\.(png|jpe?g|gif|webp)$/i)) {
  102. const img = document.createElement('img');
  103. img.alt = name;
  104. img.setAttribute('draggable', 'false');
  105.  
  106. if (url.match(/\.gif$/i)) {
  107. img.classList.add('gif-lazy');
  108. img.dataset.gifSrc = url;
  109. const gifLabel = document.createElement('div');
  110. gifLabel.textContent = 'GIF';
  111. gifLabel.classList.add('gif-label');
  112. wrapper.appendChild(gifLabel);
  113. } else {
  114. img.src = url;
  115. }
  116.  
  117. wrapper.appendChild(img);
  118. wrapper.addEventListener('dblclick', () => openFullscreen(index));
  119.  
  120. if (url.match(/\.gif$/i)) {
  121. lazyLoadObserver.observe(img);
  122. }
  123.  
  124. } else if (url.match(/\.(mp4|webm|ogg)$/i)) {
  125. const video = document.createElement('video');
  126. video.src = url;
  127. video.classList.add('video');
  128. video.muted = true;
  129. video.playsInline = true;
  130. video.preload = 'metadata';
  131. video.onloadedmetadata = () => video.currentTime = 0.1;
  132. wrapper.appendChild(video);
  133.  
  134. wrapper.addEventListener('dblclick', () => openFullscreen(index));
  135.  
  136. } else {
  137.  
  138. hintTxt = '双击打开/下载';
  139.  
  140. wrapper.addEventListener('dblclick', () => {
  141. window.location.href = url;
  142. });
  143.  
  144.  
  145. }
  146. const downloadTip = document.createElement('div');
  147. downloadTip.classList.add('downloadTip');
  148. downloadTip.textContent = hintTxt;
  149. downloadTip.classList.add('download_tip');
  150.  
  151. wrapper.appendChild(downloadTip);
  152.  
  153.  
  154. const caption = document.createElement('div');
  155. caption.textContent = name;
  156. caption.classList.add('thumbnail-caption');
  157. wrapper.appendChild(caption);
  158.  
  159. container.appendChild(wrapper);
  160. });
  161.  
  162. document.body.innerHTML = '';
  163. document.body.appendChild(container);
  164. }
  165.  
  166. // 打开全屏预览
  167. function openFullscreen(index) {
  168. currentIndex = index;
  169.  
  170. const overlay = document.createElement('div');
  171. overlay.classList.add('fullscreen-overlay');
  172.  
  173. const content = document.createElement(fileList[currentIndex].url.match(/\.(mp4|webm|ogg)$/i) ? 'video' : 'img');
  174. content.src = fileList[currentIndex].url;
  175. content.classList.add('fullscreen-content');
  176. content.setAttribute('draggable', 'false');
  177. if (content.tagName === 'VIDEO') {
  178. content.controls = true;
  179. content.autoplay = true;
  180. }
  181.  
  182.  
  183. let backgroundBrightness = 255;
  184. let accumulatedDelta = 0;
  185. overlay.addEventListener('wheel', (e) => {
  186. e.preventDefault();
  187. if (e.target === overlay) {
  188. accumulatedDelta += e.deltaY * 0.02;
  189. backgroundBrightness = Math.floor(
  190. 127.5 + 127.5 * Math.sin(accumulatedDelta * 0.1)
  191. );
  192.  
  193. overlay.style.backgroundColor = `rgba(${backgroundBrightness}, ${backgroundBrightness}, ${backgroundBrightness}, 1)`;
  194. }
  195. });
  196. overlay.appendChild(content);
  197.  
  198.  
  199. const fileNameP = document.createElement('p');
  200. let _fileName = fileList[currentIndex].url.split('/');
  201. fileNameP.classList.add('fileNameP');
  202. fileNameP.textContent = decodeURIComponent(_fileName[_fileName.length - 1]);
  203. overlay.appendChild(fileNameP);
  204.  
  205.  
  206. const closeButton = document.createElement('div');
  207. closeButton.textContent = '×';
  208. closeButton.classList.add('close-button');
  209. closeButton.addEventListener('click', () => overlay.remove());
  210. overlay.appendChild(closeButton);
  211.  
  212.  
  213.  
  214. const downloadBtn = document.createElement('div');
  215. downloadBtn.textContent = '⬇️';
  216. downloadBtn.classList.add('download-button');
  217. overlay.appendChild(downloadBtn);
  218.  
  219. // 绑定点击事件
  220. downloadBtn.addEventListener('click', function () {
  221. // 指定要下载的文件链接
  222. const fileUrl = fileList[currentIndex].url; // 替换为实际文件链接
  223. let pathGroup = fileUrl.split('/');
  224. const fileName = decodeURIComponent(pathGroup[pathGroup.length - 1]); // 下载后的文件名
  225.  
  226. const a = document.createElement('a');
  227. a.href = fileUrl;
  228. a.download = fileName; // 设置下载的文件名
  229. a.style.display = 'none'; // 隐藏 <a> 标签
  230.  
  231. // 将 <a> 标签添加到文档中
  232. document.body.appendChild(a);
  233.  
  234. // 触发点击事件以开始下载
  235. a.click();
  236.  
  237. // 移除 <a> 标签
  238. document.body.removeChild(a);
  239. });
  240.  
  241.  
  242.  
  243.  
  244. let scale = 1;
  245. let isDragging = false;
  246. let startX, startY;
  247.  
  248. content.addEventListener('wheel', (e) => {
  249. e.preventDefault();
  250. scale += e.deltaY > 0 ? -0.1 : 0.1;
  251. scale = Math.max(0.1, Math.min(scale, 5));
  252. content.style.transform = `scale(${scale})`;
  253. });
  254.  
  255. content.addEventListener('mousedown', (e) => {
  256. isDragging = true;
  257. startX = e.clientX - content.offsetLeft;
  258. startY = e.clientY - content.offsetTop;
  259. content.style.cursor = 'grabbing';
  260. });
  261.  
  262. document.addEventListener('mousemove', (e) => {
  263. if (isDragging) {
  264. const x = e.clientX - startX;
  265. const y = e.clientY - startY;
  266. content.style.left = `${x}px`;
  267. content.style.top = `${y}px`;
  268. }
  269. });
  270.  
  271. document.addEventListener('mouseup', () => {
  272. isDragging = false;
  273. content.style.cursor = 'grab';
  274. });
  275.  
  276. document.body.appendChild(overlay);
  277.  
  278.  
  279. document.addEventListener('keydown', handleKeyDown);
  280.  
  281. overlay.addEventListener('remove', () => {
  282. document.removeEventListener('keydown', handleKeyDown);
  283. });
  284.  
  285. overlay.addEventListener('dblclick', () => {
  286. const overlay = document.querySelector('.fullscreen-overlay');
  287. if (overlay) {
  288. overlay.remove();
  289. }
  290. })
  291. }
  292.  
  293.  
  294.  
  295. // 防抖函数
  296. function debounce(func, delay) {
  297. let timer;
  298. return function (...args) {
  299. clearTimeout(timer); // 清除之前的定时器
  300. timer = setTimeout(() => func.apply(this, args), delay); // 设置新的定时器
  301. };
  302. }
  303.  
  304. function toggleBodyClass(cls, t) {
  305. const body = document.body;
  306. body.classList.add(cls);
  307. setTimeout(() => {
  308. body.classList.remove(cls);
  309. }, t);
  310. }
  311.  
  312. // 使用防抖包装 toggleBodyClass 函数
  313. const debouncedToggleBodyClass = debounce(() => { toggleBodyClass('hiliArrow', 3000) }, 500);
  314.  
  315.  
  316.  
  317.  
  318.  
  319.  
  320.  
  321.  
  322.  
  323.  
  324.  
  325. function debounce(func, delay) {
  326. let timer;
  327. return function (...args) {
  328. clearTimeout(timer);
  329. timer = setTimeout(() => func.apply(this, args), delay);
  330. };
  331. }
  332.  
  333. function addBodyClass(cls) {
  334. const body = document.body;
  335. if (!body.classList.contains(cls)) {
  336. body.classList.add(cls);
  337. }
  338. }
  339.  
  340. function removeBodyClass(cls) {
  341. const body = document.body;
  342. body.classList.remove(cls);
  343. }
  344.  
  345. function createDebouncedRemoveBodyClass(cls, delay) {
  346. return debounce(() => removeBodyClass(cls), delay);
  347. }
  348.  
  349.  
  350.  
  351.  
  352.  
  353.  
  354.  
  355.  
  356.  
  357.  
  358.  
  359. function handleKeyDown(event) {
  360. if (event.key === 'ArrowLeft') {
  361. showPrevious();
  362. const className = 'hiliArrowLeft';
  363. addBodyClass(className);
  364. const debouncedRemoveBodyClass = createDebouncedRemoveBodyClass(className, 500);
  365. debouncedRemoveBodyClass();
  366. } else if (event.key === 'ArrowRight') {
  367. showNext();
  368. const className = 'hiliArrowRight';
  369. addBodyClass(className);
  370. const debouncedRemoveBodyClass = createDebouncedRemoveBodyClass(className, 500);
  371. debouncedRemoveBodyClass();
  372. } else if (event.key === 'Escape' || event.keyCode === 27) { // 检测 Esc 键
  373. const overlay = document.querySelector('.fullscreen-overlay');
  374. if (overlay) {
  375. overlay.remove();
  376. }
  377. }
  378. }
  379.  
  380.  
  381.  
  382.  
  383. document.addEventListener('keydown', typeof globalKeyDown != 'undefined' ? globalKeyDown : () => { });
  384.  
  385. // 显示上一张
  386. function showPrevious() {
  387. if (currentIndex > 0) {
  388. currentIndex--;
  389. updateFullscreenContent();
  390. }
  391. }
  392.  
  393. // 显示下一张
  394. function showNext() {
  395. if (currentIndex < fileList.length - 1) {
  396. currentIndex++;
  397. updateFullscreenContent();
  398. }
  399. }
  400.  
  401. // 更新全屏内容
  402. function updateFullscreenContent() {
  403. const overlay = document.querySelector('.fullscreen-overlay');
  404. if (!overlay) return;
  405.  
  406. const content = overlay.querySelector('.fullscreen-content');
  407. const newUrl = fileList[currentIndex].url;
  408.  
  409. if (content.tagName === 'VIDEO' && !newUrl.match(/\.(mp4|webm|ogg)$/i)) {
  410. // 如果当前是视频但新内容是图片,则替换为图片
  411. const img = document.createElement('img');
  412. img.src = newUrl;
  413. img.classList.add('fullscreen-content');
  414. img.setAttribute('draggable', 'false');
  415. overlay.replaceChild(img, content);
  416. } else if (content.tagName === 'IMG' && newUrl.match(/\.(mp4|webm|ogg)$/i)) {
  417. // 如果当前是图片但新内容是视频,则替换为视频
  418. const video = document.createElement('video');
  419. video.src = newUrl;
  420. video.classList.add('fullscreen-content');
  421. video.controls = true;
  422. video.autoplay = true;
  423. overlay.replaceChild(video, content);
  424. } else {
  425. // 同类型内容更新
  426. content.src = newUrl;
  427. }
  428.  
  429. let _fileName = newUrl.split('/');
  430. const fileNameP = overlay.querySelector('.fileNameP');
  431. fileNameP.textContent = decodeURIComponent(_fileName[_fileName.length - 1])
  432. }
  433.  
  434. // 样式化
  435. function styling() {
  436. const styleTag = document.createElement('style');
  437. styleTag.textContent = `
  438. .thumbnail-container {
  439. display: grid;
  440. grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  441. gap: 10px;
  442. padding: 10px;
  443. }
  444. .thumbnail-wrapper {
  445. position: relative;
  446. overflow: hidden;
  447. border: 1px solid #ddd;
  448. border-radius: 5px;
  449. text-align: center;
  450. cursor: pointer;
  451. max-height: 40vh;
  452. overflow-y: scroll;
  453. padding: 0 !important;
  454. margin: 0 !important;
  455.  
  456. display: flex;
  457. flex-direction: column;
  458. justify-content: space-between;
  459. }
  460. .thumbnail-wrapper img,
  461. .thumbnail-wrapper video {
  462. width: 100%;
  463. height: auto;
  464. flex-grow: 1;
  465. object-fit: contain;
  466. }
  467. .gif-label {
  468. position: absolute;
  469. top: 5px;
  470. right: 5px;
  471. background: rgba(0, 0, 0, 0.7);
  472. color: #fff;
  473. font-size: 12px;
  474. padding: 2px 5px;
  475. border-radius: 3px;
  476. }
  477. .thumbnail-caption {
  478. font-size: 12px;
  479. padding: 5px;
  480. position: sticky;
  481. bottom: 0;
  482. width: 96%;
  483. overflow: hidden;
  484. word-break: break-all;
  485. background: #ffffffaa;
  486. }
  487. .fullscreen-overlay {
  488. position: fixed;
  489. top: 0;
  490. left: 0;
  491. width: 100%;
  492. height: 100%;
  493. background-color: rgba(255, 255, 255, 1);
  494. display: flex;
  495. align-items: center;
  496. justify-content: center;
  497. z-index: 999999999;
  498. }
  499.  
  500. .fullscreen-overlay:after {
  501. content: '>';
  502. right: 2em;
  503. }
  504.  
  505. .fullscreen-overlay:before {
  506. content: "<";
  507. left: 2em;
  508. }
  509. .fullscreen-overlay:before, .fullscreen-overlay:after {
  510. position: absolute;
  511. top: 50%;
  512. opacity: 0.2;
  513. transform: translate(0, -50%);
  514. font-size: 2em;
  515. transition:opacity 0.3s ease;
  516. }
  517.  
  518. .hiliArrowLeft .fullscreen-overlay:before,
  519. .hiliArrowRight .fullscreen-overlay:after {
  520. opacity: 1;
  521. }
  522.  
  523.  
  524. .fullscreen-content {
  525. max-width: 90%;
  526. max-height: 90%;
  527. position: absolute;
  528. user-select: none;
  529. }
  530. .close-button {
  531. position: absolute;
  532. top: 10px;
  533. right: 20px;
  534. font-size: 30px;
  535. color: #fff;
  536. cursor: pointer;
  537. mix-blend-mode: difference;
  538. user-select: none;
  539. }
  540.  
  541. .download-button {
  542. position: absolute;
  543. bottom: 10px;
  544. right: 20px;
  545. font-size: 30px;
  546. color: #fff;
  547. cursor: pointer;
  548. user-select: none;
  549. }
  550.  
  551.  
  552. .fullscreen-content.video {
  553. pointer-events: none;
  554. }
  555. .gif-lazy {
  556. opacity: 0;
  557. transition: opacity 0.3s ease-in-out;
  558. }
  559. .gif-loaded {
  560. opacity: 1;
  561. }
  562.  
  563. .download_tip{
  564. font-size:1.5em;
  565. opacity:0;
  566.  
  567. transition: opacity 0.2s ease;
  568. position: absolute;
  569. width: 100%;
  570. text-align-last: center;
  571. top: 50%;
  572. transform: translate(0, -50%);
  573. z-index: 9999;
  574. height: 100%;
  575. display: flex;
  576. align-items: center;
  577. justify-content: space-around;
  578.  
  579. background: #ffffff99;
  580. backdrop-filter: blur(1px);
  581.  
  582. }
  583. .download_tip:hover{
  584. opacity:1;
  585. }
  586.  
  587.  
  588. .fileNameP{
  589. position:absolute;
  590. bottom:2em;
  591. left:50%;
  592. transform:translate(-50%,0);
  593. mix-blend-mode: difference;
  594. user-select: none;
  595. }
  596.  
  597. /* 针对 Webkit 内核浏览器(如 Chrome、Edge、Safari) */
  598. ::-webkit-scrollbar {
  599. width: 4px; /* 水平滚动条的高度 */
  600. height: 4px; /* 垂直滚动条的宽度 */
  601. }
  602.  
  603. ::-webkit-scrollbar-track {
  604. background: transparent; /* 滚动条轨道背景 */
  605. }
  606.  
  607. ::-webkit-scrollbar-thumb {
  608. background-color: rgba(0, 0, 0, 0.3); /* 滚动条滑块颜色 */
  609. border-radius: 2px; /* 滚动条滑块圆角 */
  610. }
  611.  
  612. ::-webkit-scrollbar-thumb:hover {
  613. background-color: rgba(0, 0, 0, 0.5); /* 滑块悬停时的颜色 */
  614. }
  615.  
  616. /* 针对 Firefox 浏览器 */
  617. * {
  618. scrollbar-width: thin; /* 设置滚动条为细 */
  619. scrollbar-color: rgba(0, 0, 0, 0.3) transparent; /* 滑块颜色和轨道颜色 */
  620. }
  621.  
  622.  
  623. `;
  624. document.head.appendChild(styleTag);
  625. }
  626.  
  627. // 主逻辑
  628. if (!isTargetPage()) {
  629. console.log('Not a target page, exiting...');
  630. return;
  631. } else {
  632. styling();
  633. createThumbnailView();
  634. }
  635.  
  636. document.body.setAttribute('yiu_nodejsAnywhereBetterPage', true);
  637. }
  638.  
  639. window.onload = () => {
  640. nodejsAnywhereBetterPage();
  641. };
  642.  
  643. nodejsAnywhereBetterPage();