Kemono图片打包助手

自动获取kemono.su页面中的所有图片并打包下载

// ==UserScript==
// @name         Kemono图片打包助手
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  自动获取kemono.su页面中的所有图片并打包下载
// @author       hoami523
// @match        https://kemono.cr/*/user/*
// @match        https://kemono.su/*/user/*
// @connect      kemono.su
// @connect      kemono.cr
// @connect      cdn.kemono.su
// @connect      img.kemono.su
// @connect      files.kemono.su
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 全局变量
    let isDragging = false;
    let dragOffset = { x: 0, y: 0 };
    let isMenuOpen = false;
    let isDownloading = false;
    let lastZipBlob = null;
    let lastZipName = '图片打包助手.zip';

    // 简化的JSZip实现(核心功能)
    class SimpleZip {
        constructor() {
            this.files = [];
        }

        file(name, blob) {
            this.files.push({ name, blob });
        }

        async generateAsync() {
            // 简化的ZIP格式生成
            const encoder = new TextEncoder();
            let centralDirectory = [];
            let localFiles = [];
            let offset = 0;

            for (let i = 0; i < this.files.length; i++) {
                const file = this.files[i];
                const fileData = await file.blob.arrayBuffer();
                const fileNameBytes = encoder.encode(file.name);
                
                // 本地文件头
                const localHeader = new ArrayBuffer(30 + fileNameBytes.length);
                const localView = new DataView(localHeader);
                
                // 本地文件头签名
                localView.setUint32(0, 0x04034b50, true);
                // 版本
                localView.setUint16(4, 10, true);
                // 标志
                localView.setUint16(6, 0, true);
                // 压缩方法(存储)
                localView.setUint16(8, 0, true);
                // 时间
                localView.setUint16(10, 0, true);
                // 日期
                localView.setUint16(12, 0, true);
                // CRC32(简化,实际应该计算)
                localView.setUint32(14, 0, true);
                // 压缩大小
                localView.setUint32(18, fileData.byteLength, true);
                // 未压缩大小
                localView.setUint32(22, fileData.byteLength, true);
                // 文件名长度
                localView.setUint16(26, fileNameBytes.length, true);
                // 额外字段长度
                localView.setUint16(28, 0, true);
                
                // 文件名
                const localBytes = new Uint8Array(localHeader);
                localBytes.set(fileNameBytes, 30);
                
                localFiles.push(localBytes);
                localFiles.push(new Uint8Array(fileData));
                
                // 中央目录条目
                const centralHeader = new ArrayBuffer(46 + fileNameBytes.length);
                const centralView = new DataView(centralHeader);
                
                // 中央目录文件头签名
                centralView.setUint32(0, 0x02014b50, true);
                // 版本
                centralView.setUint16(4, 10, true);
                // 需要的版本
                centralView.setUint16(6, 10, true);
                // 标志
                centralView.setUint16(8, 0, true);
                // 压缩方法
                centralView.setUint16(10, 0, true);
                // 时间
                centralView.setUint16(12, 0, true);
                // 日期
                centralView.setUint16(14, 0, true);
                // CRC32
                centralView.setUint32(16, 0, true);
                // 压缩大小
                centralView.setUint32(20, fileData.byteLength, true);
                // 未压缩大小
                centralView.setUint32(24, fileData.byteLength, true);
                // 文件名长度
                centralView.setUint16(28, fileNameBytes.length, true);
                // 额外字段长度
                centralView.setUint16(30, 0, true);
                // 注释长度
                centralView.setUint16(32, 0, true);
                // 磁盘号
                centralView.setUint16(34, 0, true);
                // 内部文件属性
                centralView.setUint16(36, 0, true);
                // 外部文件属性
                centralView.setUint32(38, 0, true);
                // 本地文件头偏移
                centralView.setUint32(42, offset, true);
                
                const centralBytes = new Uint8Array(centralHeader);
                centralBytes.set(fileNameBytes, 46);
                
                centralDirectory.push(centralBytes);
                
                offset += localBytes.length + fileData.byteLength;
            }
            
            // 连接所有部分
            const totalLength = localFiles.reduce((sum, arr) => sum + arr.length, 0) +
                              centralDirectory.reduce((sum, arr) => sum + arr.length, 0) +
                              22; // 结束记录长度
            
            const zipData = new Uint8Array(totalLength);
            let pos = 0;
            
            // 写入本地文件
            for (const local of localFiles) {
                zipData.set(local, pos);
                pos += local.length;
            }
            
            // 写入中央目录
            for (const central of centralDirectory) {
                zipData.set(central, pos);
                pos += central.length;
            }
            
            // 结束记录
            const endRecord = new ArrayBuffer(22);
            const endView = new DataView(endRecord);
            
            // 结束记录签名
            endView.setUint32(0, 0x06054b50, true);
            // 磁盘号
            endView.setUint16(4, 0, true);
            // 中央目录起始磁盘号
            endView.setUint16(6, 0, true);
            // 磁盘上的中央目录条目数
            endView.setUint16(8, this.files.length, true);
            // 中央目录条目总数
            endView.setUint16(10, this.files.length, true);
            // 中央目录大小
            endView.setUint32(12, centralDirectory.reduce((sum, arr) => sum + arr.length, 0), true);
            // 中央目录偏移
            endView.setUint32(16, offset, true);
            // 注释长度
            endView.setUint16(20, 0, true);
            
            zipData.set(new Uint8Array(endRecord), pos);
            
            return new Blob([zipData], { type: 'application/zip' });
        }
    }

    // 获取作者和标题
    function getAuthorAndTitle() {
        const author = document.querySelector('.post__user-name')?.textContent?.trim() || '未知作者';
        const titleElement = document.querySelector('.post__title span');
        let title = titleElement?.textContent?.trim() || '未知标题';
        
        // 去除文件名非法字符,包括更多特殊字符
        const safe = s => s.replace(/[\\/:*?"<>|\[\]()]/g, '_');
        return {
            author: safe(author),
            title: safe(title)
        };
    }

    // 创建悬浮面板
    function createFloatingIcon() {
        const container = document.createElement('div');
        container.className = 'image-pack-helper';
        container.innerHTML = `
            <div class="image-pack-icon">
                <svg viewBox="0 0 24 24">
                    <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
                </svg>
            </div>
            <div class="image-pack-menu">
                <div class="menu-title">Kemono图片打包助手</div>
                <button class="download-btn" id="downloadBtn">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
                        <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
                    </svg>
                    打包下载图片
                </button>
                <button class="download-btn" id="manualDownloadBtn" style="margin-top:8px;display:none;">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
                        <path d="M5 20h14v-2H5v2zm7-18C6.48 2 2 6.48 2 12c0 5.52 4.48 10 10 10s10-4.48 10-10C22 6.48 17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/>
                    </svg>
                    手动下载ZIP
                </button>
                <div class="progress-container" id="progressContainer">
                    <div class="progress-bar">
                        <div class="progress-fill" id="progressFill"></div>
                    </div>
                    <div class="progress-text" id="progressText">准备中...</div>
                </div>
                <div id="debugInfo"></div>
            </div>
        `;
        document.body.appendChild(container);
        return container;
    }

    // 拖拽功能
    function initDragAndDrop(container) {
        const icon = container.querySelector('.image-pack-icon');
        let touchStartTime = 0;
        let touchStartX = 0;
        let touchStartY = 0;
        const DRAG_THRESHOLD = 10; // 拖拽阈值
        const TAP_TIMEOUT = 200; // 点击超时时间(毫秒)
        
        // 鼠标事件
        icon.addEventListener('mousedown', startDrag);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', stopDrag);
        
        // 触摸事件处理
        icon.addEventListener('touchstart', (e) => {
            e.preventDefault();
            touchStartTime = Date.now();
            touchStartX = e.touches[0].clientX;
            touchStartY = e.touches[0].clientY;
            startDrag(e);
        }, { passive: false });
        
        document.addEventListener('touchmove', drag, { passive: false });
        
        document.addEventListener('touchend', (e) => {
            const touchEndTime = Date.now();
            const touchEndX = e.changedTouches[0].clientX;
            const touchEndY = e.changedTouches[0].clientY;
            
            // 计算触摸持续时间和移动距离
            const touchDuration = touchEndTime - touchStartTime;
            const moveX = Math.abs(touchEndX - touchStartX);
            const moveY = Math.abs(touchEndY - touchStartY);
            
            // 如果是轻触(时间短且移动距离小),则触发菜单切换
            if (touchDuration < TAP_TIMEOUT && moveX < DRAG_THRESHOLD && moveY < DRAG_THRESHOLD) {
                e.preventDefault();
                toggleMenu(e);
            }
            
            stopDrag(e);
        });
        
        // 鼠标点击事件
        icon.addEventListener('click', toggleMenu);
    }

    function startDrag(e) {
        e.preventDefault();
        isDragging = true;
        const container = document.querySelector('.image-pack-helper');
        container.classList.add('dragging');
        const rect = container.getBoundingClientRect();
        const clientX = e.clientX || (e.touches && e.touches[0].clientX);
        const clientY = e.clientY || (e.touches && e.touches[0].clientY);
        dragOffset.x = clientX - rect.left;
        dragOffset.y = clientY - rect.top;
    }

    function drag(e) {
        if (!isDragging) return;
        e.preventDefault();
        const container = document.querySelector('.image-pack-helper');
        const clientX = e.clientX || (e.touches && e.touches[0].clientX);
        const clientY = e.clientY || (e.touches && e.touches[0].clientY);
        const x = clientX - dragOffset.x;
        const y = clientY - dragOffset.y;
        const maxX = window.innerWidth - container.offsetWidth;
        const maxY = window.innerHeight - container.offsetHeight;
        container.style.left = Math.max(0, Math.min(x, maxX)) + 'px';
        container.style.top = Math.max(0, Math.min(y, maxY)) + 'px';
        container.style.right = 'auto';
    }

    function stopDrag() {
        if (!isDragging) return;
        isDragging = false;
        const container = document.querySelector('.image-pack-helper');
        container.classList.remove('dragging');
    }

    function toggleMenu(e) {
        e.stopPropagation();
        const menu = document.querySelector('.image-pack-menu');
        if (isMenuOpen) {
            menu.classList.remove('show');
            isMenuOpen = false;
        } else {
            menu.classList.add('show');
            isMenuOpen = true;
        }
    }

    // 获取图片(带调试信息)
    function getAllImages(debugInfo) {
        const url = window.location.href;
        const container = document.querySelector('.post__files');
        debugInfo['页面URL'] = url;
        debugInfo['是否找到文件容器'] = !!container;
        if (!container) {
            debugInfo['图片数量'] = 0;
            return [];
        }
        
        // 获取所有图片链接
        const imgLinks = container.querySelectorAll('.fileThumb.image-link');
        debugInfo['图片链接数量'] = imgLinks.length;
        const images = [];
        let filteredCount = 0;
        
        imgLinks.forEach((link, index) => {
            const href = link.getAttribute('href');
            if (href && !href.startsWith('data:')) {
                let src = href;
                if (src && src.startsWith('//')) src = 'https:' + src;
                const urlLower = src.toLowerCase();
                const isImage = urlLower.includes('.jpg') || urlLower.includes('.jpeg') || 
                               urlLower.includes('.png') || urlLower.includes('.gif') || 
                               urlLower.includes('.webp') || urlLower.includes('.bmp') ||
                               urlLower.includes('.svg');
                if (isImage) {
                    images.push({
                        src: src,
                        index: index
                    });
                } else {
                    filteredCount++;
                }
            }
        });
        debugInfo['过滤掉的非图片文件'] = filteredCount;
        debugInfo['有效图片数量'] = images.length;
        return images;
    }

    // 下载图片
    async function downloadImage(url) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                onload: function(response) {
                    const blob = response.response;
                    
                    // 过滤掉小文件(小于50KB的文件)
                    if (blob.size < 50 * 1024) {
                        console.log(`跳过小文件: ${url}, 大小: ${blob.size} bytes`);
                        resolve(null);
                        return;
                    }
                    
                    resolve(blob);
                },
                onerror: function(error) {
                    console.error(`下载失败: ${url}`, error);
                    resolve(null);
                }
            });
        });
    }

    // 自动下载和手动下载
    function triggerDownload(blob, zipName) {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = zipName || 'Kemono图片打包助手.zip';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    // 显示调试信息
    function showDebugInfo(info) {
        const debugDiv = document.getElementById('debugInfo');
        debugDiv.innerHTML = '';
        for (const key in info) {
            const p = document.createElement('p');
            p.style.fontSize = '12px';
            p.style.margin = '2px 0';
            p.textContent = `${key}: ${info[key]}`;
            debugDiv.appendChild(p);
        }
    }

    // 进度UI
    function updateProgressUI(type, data) {
        const progressContainer = document.getElementById('progressContainer');
        const progressFill = document.getElementById('progressFill');
        const progressText = document.getElementById('progressText');
        if (type === 'start') {
            progressContainer.classList.add('show');
            progressFill.style.width = '0%';
            progressText.textContent = `找到 ${data.total} 张图片,开始下载...`;
        } else if (type === 'downloading') {
            progressFill.style.width = `${(data.current / data.total) * 100}%`;
            progressText.textContent = `下载中... (${data.current}/${data.total})`;
        } else if (type === 'zipping') {
            progressFill.style.width = '100%';
            progressText.textContent = '正在生成ZIP文件...';
        } else if (type === 'done') {
            progressFill.style.width = '100%';
            progressText.textContent = `下载完成!共打包 ${data.count} 张图片`;
            progressContainer.classList.add('show');
        } else if (type === 'error') {
            progressText.textContent = data.message;
            progressContainer.classList.add('show');
        }
    }

    // 主打包逻辑
    async function packAndDownloadImages(debugMode) {
        if (isDownloading) return;
        isDownloading = true;
        lastZipBlob = null;
        lastZipName = 'Kemono图片打包助手.zip';
        const downloadBtn = document.getElementById('downloadBtn');
        const manualBtn = document.getElementById('manualDownloadBtn');
        const progressContainer = document.getElementById('progressContainer');
        const progressFill = document.getElementById('progressFill');
        const progressText = document.getElementById('progressText');
        downloadBtn.disabled = true;
        downloadBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>处理中...`;
        progressContainer.classList.add('show');
        progressText.textContent = '准备中...';
        progressFill.style.width = '0%';
        showDebugInfo({ 状态: '正在请求页面信息...' });

        // 获取作者和标题
        const { author, title } = getAuthorAndTitle();
        lastZipName = `${author}_${title}.zip`;

        const debugInfo = {};
        const images = getAllImages(debugInfo);
        if (debugMode) showDebugInfo(debugInfo);
        if (images.length === 0) {
            updateProgressUI('error', { message: '未找到图片' });
            showDebugInfo(debugInfo);
            downloadBtn.disabled = false;
            downloadBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>打包下载图片`;
            manualBtn.style.display = 'none';
            isDownloading = false;
            return;
        }
        updateProgressUI('start', { total: images.length });
        const zip = new SimpleZip();
        let downloadedCount = 0;
        let skippedCount = 0;
        for (let i = 0; i < images.length; i++) {
            updateProgressUI('downloading', { current: i + 1, total: images.length });
            showDebugInfo(debugInfo);
            const blob = await downloadImage(images[i].src);
            if (blob) {
                // 根据blob类型确定扩展名
                let extension = 'jpg'; // 默认扩展名
                if (blob.type.includes('png')) extension = 'png';
                else if (blob.type.includes('gif')) extension = 'gif';
                else if (blob.type.includes('webp')) extension = 'webp';
                else if (blob.type.includes('bmp')) extension = 'bmp';
                else if (blob.type.includes('svg')) extension = 'svg';
                // 只用序号命名
                const filename = `${String(downloadedCount + 1).padStart(3, '0')}.${extension}`;
                zip.file(filename, blob);
                downloadedCount++;
                debugInfo[`文件${downloadedCount}`] = `${filename} (${(blob.size / 1024).toFixed(1)}KB)`;
            } else {
                skippedCount++;
            }
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        debugInfo['跳过的小文件数量'] = skippedCount;
        debugInfo['实际下载文件数量'] = downloadedCount;
        if (downloadedCount === 0) {
            updateProgressUI('error', { message: '下载失败,请检查网络连接' });
            showDebugInfo(debugInfo);
            downloadBtn.disabled = false;
            downloadBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>打包下载图片`;
            manualBtn.style.display = 'none';
            isDownloading = false;
            return;
        }
        updateProgressUI('zipping', {});
        showDebugInfo(debugInfo);
        const zipBlob = await zip.generateAsync();
        lastZipBlob = zipBlob;
        updateProgressUI('done', { count: downloadedCount });
        showDebugInfo(debugInfo);
        // 自动下载
        triggerDownload(zipBlob, lastZipName);
        // 显示手动下载按钮
        manualBtn.style.display = 'block';
        manualBtn.onclick = () => {
            if (lastZipBlob) triggerDownload(lastZipBlob, lastZipName);
        };
        setTimeout(() => {
            downloadBtn.disabled = false;
            downloadBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>打包下载图片`;
            progressContainer.classList.remove('show');
            progressFill.style.width = '0%';
            isDownloading = false;
        }, 3000);
    }

    // 添加CSS样式
    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            /* 悬浮面板容器 */
            .image-pack-helper {
                position: fixed;
                z-index: 999999;
                top: 20px;
                right: 20px;
                cursor: move;
                user-select: none;
                touch-action: none;
            }

            /* 悬浮图标 */
            .image-pack-icon {
                width: 50px;
                height: 50px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
                transition: all 0.3s ease;
                border: 2px solid #fff;
            }

            .image-pack-icon:hover {
                transform: scale(1.1);
                box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
            }

            .image-pack-icon svg {
                width: 24px;
                height: 24px;
                fill: white;
            }

            /* 菜单容器 */
            .image-pack-menu {
                position: absolute;
                top: 60px;
                right: 0;
                background: white;
                border-radius: 12px;
                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
                padding: 16px;
                min-width: 220px;
                opacity: 0;
                visibility: hidden;
                transform: translateY(-10px);
                transition: all 0.3s ease;
                border: 1px solid #e1e5e9;
            }

            .image-pack-menu.show {
                opacity: 1;
                visibility: visible;
                transform: translateY(0);
            }

            .menu-title {
                font-size: 14px;
                font-weight: 600;
                color: #333;
                margin-bottom: 12px;
                text-align: center;
            }

            .download-btn {
                width: 100%;
                padding: 10px 16px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                color: white;
                border: none;
                border-radius: 8px;
                font-size: 14px;
                font-weight: 500;
                cursor: pointer;
                transition: all 0.3s ease;
                display: flex;
                align-items: center;
                justify-content: center;
                gap: 8px;
            }

            .download-btn:hover {
                transform: translateY(-2px);
                box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
            }

            .download-btn:disabled {
                background: #ccc;
                cursor: not-allowed;
                transform: none;
                box-shadow: none;
            }

            .progress-container {
                margin-top: 12px;
                display: none;
            }

            .progress-container.show {
                display: block;
            }

            .progress-bar {
                width: 100%;
                height: 6px;
                background: #f0f0f0;
                border-radius: 3px;
                overflow: hidden;
                margin-bottom: 8px;
            }

            .progress-fill {
                height: 100%;
                background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
                width: 0%;
                transition: width 0.3s ease;
            }

            .progress-text {
                font-size: 12px;
                color: #666;
                text-align: center;
            }

            #debugInfo {
                margin-top: 10px;
                background: #f6f6f6;
                border-radius: 6px;
                padding: 6px;
                color: #666;
                word-break: break-all;
                font-family: monospace;
            }

            @media (max-width: 768px) {
                .image-pack-helper {
                    top: 10px;
                    right: 10px;
                }
                .image-pack-icon {
                    width: 45px;
                    height: 45px;
                }
                .image-pack-icon svg {
                    width: 20px;
                    height: 20px;
                }
                .image-pack-menu {
                    min-width: 180px;
                    padding: 12px;
                }
                .download-btn {
                    padding: 8px 12px;
                    font-size: 13px;
                }
            }

            .image-pack-helper.dragging {
                opacity: 0.8;
            }

            .image-pack-helper.dragging .image-pack-icon {
                transform: scale(0.95);
            }
        `;
        document.head.appendChild(style);
    }

    // 初始化
    function init() {
        if (!window.location.href.includes('kemono.cr/') && !window.location.href.includes('kemono.su/')) return;
        
        // 添加样式
        addStyles();
        
        const container = createFloatingIcon();
        initDragAndDrop(container);
        
        // 为下载按钮添加触摸事件支持
        const downloadBtn = document.getElementById('downloadBtn');
        downloadBtn.addEventListener('click', () => packAndDownloadImages(true));
        downloadBtn.addEventListener('touchend', (e) => {
            e.preventDefault();
            packAndDownloadImages(true);
        });
        
        // 为手动下载按钮添加触摸事件支持
        const manualBtn = document.getElementById('manualDownloadBtn');
        manualBtn.addEventListener('click', () => {
            if (lastZipBlob) triggerDownload(lastZipBlob, lastZipName);
        });
        manualBtn.addEventListener('touchend', (e) => {
            e.preventDefault();
            if (lastZipBlob) triggerDownload(lastZipBlob, lastZipName);
        });
        
        // 菜单常驻:点击菜单不关闭,点击icon才切换
        const menu = document.querySelector('.image-pack-menu');
        menu.addEventListener('mousedown', e => e.stopPropagation());
        menu.addEventListener('touchstart', e => e.stopPropagation());
        
        // 不再监听document的点击关闭菜单
        console.log('Kemono图片打包助手悬浮面板已启动');
        
        // 显示通知
        GM_notification({
            text: 'Kemono图片打包助手已启动!点击右上角图标开始使用。',
            title: 'Kemono图片打包助手',
            timeout: 3000
        });
    }

    // 等待页面加载完成
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();