nodejsAnywhereBetterPage

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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