nodejsAnywhereBetterPage

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

// ==UserScript==
// @name         nodejsAnywhereBetterPage
// @namespace    http://leizingyiu.net/
// @version      20250305
// @description  Convert nodejs_anywhere file directory into thumbnail view with image/video preview, fullscreen viewer, and keyboard navigation.
// @author       leizingyiu
// @match        http://*.*:8000/*
// @match        https://*.*:8001/*
// @license     GNU AGPLv3
// @grant        none
// ==/UserScript==

const originHtml = document.getElementsByTagName('html')[0].outerHTML;

function globalKeyDown(event) {
    console.log(event);
    if (event.ctrlKey && event.altKey && event.code === 'KeyS') {

        const content = originHtml.replace('</html>', '') + `<script>(${String(nodejsAnywhereBetterPage)})()</script><\/html>`;
        downloadThisHTML(content);

    }
}

function downloadThisHTML(content) {
    const blob = new Blob([content], { type: 'text/html' });

    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = 'index.html';

    link.click();

    URL.revokeObjectURL(link.href);
}


function nodejsAnywhereBetterPage() {
    if (document.body.hasAttribute('yiu_nodejsAnywhereBetterPage')) {
        return;
    }
    'use strict';
    let currentIndex = 0;
    let fileList = [];

    function isTargetPage() {
        const urlPath = window.location.pathname.toLowerCase();
        const isAnyWhereFileView = document.querySelector('#files') !== null;
        return isAnyWhereFileView;
    }

    function setupLazyGifLoading() {
        const observer = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    const gifSrc = img.dataset.gifSrc;

                    if (gifSrc) {
                        img.src = gifSrc;
                        img.classList.remove('gif-lazy');
                        img.classList.add('gif-loaded');
                        observer.unobserve(img);
                    }
                } else {
                    const img = entry.target;
                    if (img.classList.contains('gif-loaded')) {
                        img.src = '';
                        img.classList.remove('gif-loaded');
                        img.classList.add('gif-lazy');
                    }
                }
            });
        }, {
            rootMargin: '50px',
            threshold: 0.01
        });

        return observer;
    }

    function createThumbnailView() {
        const fileListElements = Array.from(document.querySelectorAll('#files li a'));
        fileList = fileListElements.map(file => ({
            url: file.href,
            name: file.querySelector('.name').textContent
        }));

        const container = document.createElement('div');
        container.classList.add('thumbnail-container');

        const lazyLoadObserver = setupLazyGifLoading();

        fileList.forEach((file, index) => {
            const { url, name } = file;

            const wrapper = document.createElement('div');
            wrapper.classList.add('thumbnail-wrapper');

            let hintTxt = '双击预览'
            if (url.match(/\.(png|jpe?g|gif|webp)$/i)) {
                const img = document.createElement('img');
                img.alt = name;
                img.setAttribute('draggable', 'false');

                if (url.match(/\.gif$/i)) {
                    img.classList.add('gif-lazy');
                    img.dataset.gifSrc = url;
                    const gifLabel = document.createElement('div');
                    gifLabel.textContent = 'GIF';
                    gifLabel.classList.add('gif-label');
                    wrapper.appendChild(gifLabel);
                } else {
                    img.src = url;
                }

                wrapper.appendChild(img);
                wrapper.addEventListener('dblclick', () => openFullscreen(index));

                if (url.match(/\.gif$/i)) {
                    lazyLoadObserver.observe(img);
                }

            } else if (url.match(/\.(mp4|webm|ogg)$/i)) {
                const video = document.createElement('video');
                video.src = url;
                video.classList.add('video');
                video.muted = true;
                video.playsInline = true;
                video.preload = 'metadata';
                video.onloadedmetadata = () => video.currentTime = 0.1;
                wrapper.appendChild(video);

                wrapper.addEventListener('dblclick', () => openFullscreen(index));

            } else {

                hintTxt = '双击打开/下载';

                wrapper.addEventListener('dblclick', () => {
                    window.location.href = url;
                });


            }
            const downloadTip = document.createElement('div');
            downloadTip.classList.add('downloadTip');
            downloadTip.textContent = hintTxt;
            downloadTip.classList.add('download_tip');

            wrapper.appendChild(downloadTip);


            const caption = document.createElement('div');
            caption.textContent = name;
            caption.classList.add('thumbnail-caption');
            wrapper.appendChild(caption);

            container.appendChild(wrapper);
        });

        document.body.innerHTML = '';
        document.body.appendChild(container);
    }

    // 打开全屏预览
    function openFullscreen(index) {
        currentIndex = index;

        const overlay = document.createElement('div');
        overlay.classList.add('fullscreen-overlay');

        const content = document.createElement(fileList[currentIndex].url.match(/\.(mp4|webm|ogg)$/i) ? 'video' : 'img');
        content.src = fileList[currentIndex].url;
        content.classList.add('fullscreen-content');
        content.setAttribute('draggable', 'false');
        if (content.tagName === 'VIDEO') {
            content.controls = true;
            content.autoplay = true;
        }


        let backgroundBrightness = 255;
        let accumulatedDelta = 0;
        overlay.addEventListener('wheel', (e) => {
            e.preventDefault();
            if (e.target === overlay) {
                accumulatedDelta += e.deltaY * 0.02;
                backgroundBrightness = Math.floor(
                    127.5 + 127.5 * Math.sin(accumulatedDelta * 0.1)
                );

                overlay.style.backgroundColor = `rgba(${backgroundBrightness}, ${backgroundBrightness}, ${backgroundBrightness}, 1)`;
            }
        });
        overlay.appendChild(content);


        const fileNameP = document.createElement('p');
        let _fileName = fileList[currentIndex].url.split('/');
        fileNameP.classList.add('fileNameP');
        fileNameP.textContent = decodeURIComponent(_fileName[_fileName.length - 1]);
        overlay.appendChild(fileNameP);


        const closeButton = document.createElement('div');
        closeButton.textContent = '×';
        closeButton.classList.add('close-button');
        closeButton.addEventListener('click', () => overlay.remove());
        overlay.appendChild(closeButton);



        const downloadBtn = document.createElement('div');
        downloadBtn.textContent = '⬇️';
        downloadBtn.classList.add('download-button');
        overlay.appendChild(downloadBtn);

        // 绑定点击事件
        downloadBtn.addEventListener('click', function () {
            // 指定要下载的文件链接
            const fileUrl = fileList[currentIndex].url; // 替换为实际文件链接
            let pathGroup = fileUrl.split('/');
            const fileName = decodeURIComponent(pathGroup[pathGroup.length - 1]); // 下载后的文件名

            const a = document.createElement('a');
            a.href = fileUrl;
            a.download = fileName; // 设置下载的文件名
            a.style.display = 'none'; // 隐藏 <a> 标签

            // 将 <a> 标签添加到文档中
            document.body.appendChild(a);

            // 触发点击事件以开始下载
            a.click();

            // 移除 <a> 标签
            document.body.removeChild(a);
        });




        let scale = 1;
        let isDragging = false;
        let startX, startY;

        content.addEventListener('wheel', (e) => {
            e.preventDefault();
            scale += e.deltaY > 0 ? -0.1 : 0.1;
            scale = Math.max(0.1, Math.min(scale, 5));
            content.style.transform = `scale(${scale})`;
        });

        content.addEventListener('mousedown', (e) => {
            isDragging = true;
            startX = e.clientX - content.offsetLeft;
            startY = e.clientY - content.offsetTop;
            content.style.cursor = 'grabbing';
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                const x = e.clientX - startX;
                const y = e.clientY - startY;
                content.style.left = `${x}px`;
                content.style.top = `${y}px`;
            }
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            content.style.cursor = 'grab';
        });

        document.body.appendChild(overlay);


        document.addEventListener('keydown', handleKeyDown);

        overlay.addEventListener('remove', () => {
            document.removeEventListener('keydown', handleKeyDown);
        });

        overlay.addEventListener('dblclick', () => {
            const overlay = document.querySelector('.fullscreen-overlay');
            if (overlay) {
                overlay.remove();
            }
        })
    }



    // 防抖函数
    function debounce(func, delay) {
        let timer;
        return function (...args) {
            clearTimeout(timer); // 清除之前的定时器
            timer = setTimeout(() => func.apply(this, args), delay); // 设置新的定时器
        };
    }

    function toggleBodyClass(cls, t) {
        const body = document.body;
        body.classList.add(cls);
        setTimeout(() => {
            body.classList.remove(cls);
        }, t);
    }

    // 使用防抖包装 toggleBodyClass 函数
    const debouncedToggleBodyClass = debounce(() => { toggleBodyClass('hiliArrow', 3000) }, 500);











    function debounce(func, delay) {
        let timer;
        return function (...args) {
            clearTimeout(timer);
            timer = setTimeout(() => func.apply(this, args), delay);
        };
    }

    function addBodyClass(cls) {
        const body = document.body;
        if (!body.classList.contains(cls)) {
            body.classList.add(cls);
        }
    }

    function removeBodyClass(cls) {
        const body = document.body;
        body.classList.remove(cls);
    }

    function createDebouncedRemoveBodyClass(cls, delay) {
        return debounce(() => removeBodyClass(cls), delay);
    }











    function handleKeyDown(event) {
        if (event.key === 'ArrowLeft') {
            showPrevious();
            const className = 'hiliArrowLeft';
            addBodyClass(className);
            const debouncedRemoveBodyClass = createDebouncedRemoveBodyClass(className, 500);
            debouncedRemoveBodyClass();
        } else if (event.key === 'ArrowRight') {
            showNext();
            const className = 'hiliArrowRight';
            addBodyClass(className);
            const debouncedRemoveBodyClass = createDebouncedRemoveBodyClass(className, 500);
            debouncedRemoveBodyClass();
        } else if (event.key === 'Escape' || event.keyCode === 27) { // 检测 Esc 键
            const overlay = document.querySelector('.fullscreen-overlay');
            if (overlay) {
                overlay.remove();
            }
        }
    }




    document.addEventListener('keydown', typeof globalKeyDown != 'undefined' ? globalKeyDown : () => { });

    // 显示上一张
    function showPrevious() {
        if (currentIndex > 0) {
            currentIndex--;
            updateFullscreenContent();
        }
    }

    // 显示下一张
    function showNext() {
        if (currentIndex < fileList.length - 1) {
            currentIndex++;
            updateFullscreenContent();
        }
    }

    // 更新全屏内容
    function updateFullscreenContent() {
        const overlay = document.querySelector('.fullscreen-overlay');
        if (!overlay) return;

        const content = overlay.querySelector('.fullscreen-content');
        const newUrl = fileList[currentIndex].url;

        if (content.tagName === 'VIDEO' && !newUrl.match(/\.(mp4|webm|ogg)$/i)) {
            // 如果当前是视频但新内容是图片,则替换为图片
            const img = document.createElement('img');
            img.src = newUrl;
            img.classList.add('fullscreen-content');
            img.setAttribute('draggable', 'false');
            overlay.replaceChild(img, content);
        } else if (content.tagName === 'IMG' && newUrl.match(/\.(mp4|webm|ogg)$/i)) {
            // 如果当前是图片但新内容是视频,则替换为视频
            const video = document.createElement('video');
            video.src = newUrl;
            video.classList.add('fullscreen-content');
            video.controls = true;
            video.autoplay = true;
            overlay.replaceChild(video, content);
        } else {
            // 同类型内容更新
            content.src = newUrl;
        }

        let _fileName = newUrl.split('/');
        const fileNameP = overlay.querySelector('.fileNameP');
        fileNameP.textContent = decodeURIComponent(_fileName[_fileName.length - 1])
    }

    // 样式化
    function styling() {
        const styleTag = document.createElement('style');
        styleTag.textContent = `
        .thumbnail-container {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
            gap: 10px;
            padding: 10px;
        }
        .thumbnail-wrapper {
            position: relative;
            overflow: hidden;
            border: 1px solid #ddd;
            border-radius: 5px;
            text-align: center;
            cursor: pointer;
            max-height: 40vh;
            overflow-y: scroll;
            padding: 0 !important;
            margin: 0 !important;

            display: flex;
            flex-direction: column;
            justify-content: space-between;
        }
        .thumbnail-wrapper img,
        .thumbnail-wrapper video {
            width: 100%;
            height: auto;
            flex-grow: 1;
            object-fit: contain;
        }
        .gif-label {
            position: absolute;
            top: 5px;
            right: 5px;
            background: rgba(0, 0, 0, 0.7);
            color: #fff;
            font-size: 12px;
            padding: 2px 5px;
            border-radius: 3px;
        }
        .thumbnail-caption {
            font-size: 12px;
            padding: 5px;
            position: sticky;
            bottom: 0;
            width: 96%;
            overflow: hidden;
            word-break: break-all;
            background: #ffffffaa;
        }
        .fullscreen-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(255, 255, 255, 1);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 999999999;
        }

.fullscreen-overlay:after {
    content: '>';
    right: 2em;
}

.fullscreen-overlay:before {
    content: "<";
    left: 2em;
}
.fullscreen-overlay:before, .fullscreen-overlay:after {
    position: absolute;
    top: 50%;
    opacity: 0.2;
    transform: translate(0, -50%);
    font-size: 2em;
    transition:opacity 0.3s ease;
}

.hiliArrowLeft .fullscreen-overlay:before,
.hiliArrowRight .fullscreen-overlay:after {
    opacity: 1;
}


        .fullscreen-content {
            max-width: 90%;
            max-height: 90%;
            position: absolute;
            user-select: none;
        }
        .close-button {
            position: absolute;
            top: 10px;
            right: 20px;
            font-size: 30px;
            color: #fff;
            cursor: pointer;
            mix-blend-mode: difference;
            user-select: none;
        }

          .download-button {
            position: absolute;
            bottom: 10px;
            right: 20px;
            font-size: 30px;
            color: #fff;
            cursor: pointer;
            user-select: none;
        }


        .fullscreen-content.video {
            pointer-events: none;
        }
        .gif-lazy {
            opacity: 0;
            transition: opacity 0.3s ease-in-out;
        }
        .gif-loaded {
            opacity: 1;
        }

.download_tip{
font-size:1.5em;
opacity:0;

transition: opacity 0.2s ease;
    position: absolute;
    width: 100%;
    text-align-last: center;
    top: 50%;
    transform: translate(0, -50%);
    z-index: 9999;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: space-around;

        background: #ffffff99;
    backdrop-filter: blur(1px);

}
.download_tip:hover{
opacity:1;
}


.fileNameP{
position:absolute;
bottom:2em;
left:50%;
transform:translate(-50%,0);
mix-blend-mode: difference;
user-select: none;
}

        /* 针对 Webkit 内核浏览器(如 Chrome、Edge、Safari) */
::-webkit-scrollbar {
    width: 4px; /* 水平滚动条的高度 */
    height: 4px; /* 垂直滚动条的宽度 */
}

::-webkit-scrollbar-track {
    background: transparent; /* 滚动条轨道背景 */
}

::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.3); /* 滚动条滑块颜色 */
    border-radius: 2px; /* 滚动条滑块圆角 */
}

::-webkit-scrollbar-thumb:hover {
    background-color: rgba(0, 0, 0, 0.5); /* 滑块悬停时的颜色 */
}

/* 针对 Firefox 浏览器 */
* {
    scrollbar-width: thin; /* 设置滚动条为细 */
    scrollbar-color: rgba(0, 0, 0, 0.3) transparent; /* 滑块颜色和轨道颜色 */
}


        `;
        document.head.appendChild(styleTag);
    }

    // 主逻辑
    if (!isTargetPage()) {
        console.log('Not a target page, exiting...');
        return;
    } else {
        styling();
        createThumbnailView();
    }

    document.body.setAttribute('yiu_nodejsAnywhereBetterPage', true);
}

window.onload = () => {
    nodejsAnywhereBetterPage();
};

nodejsAnywhereBetterPage();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址