MEGA - Link Queue with Auto-scan & Metadata

Auto-scan all files in MEGA shared folders. Queue/copy links with file name, size, type, and video duration. Per-link checkboxes for selective copying. Desktop version only.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         MEGA - Link Queue with Auto-scan & Metadata
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  Auto-scan all files in MEGA shared folders. Queue/copy links with file name, size, type, and video duration. Per-link checkboxes for selective copying. Desktop version only.
// @author       xPokerr + adapted + enhanced
// @license      MIT
// @match        *://mega.nz/folder/*
// @match        *://mega.io/folder/*
// @grant        GM_setClipboard
// @grant        GM_openInTab
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    let btn = null;
    let panel = null;
    let listEl = null;
    let queuedLinks = []; // [{ url, handle, name, status, checked, sizeFormatted, ext, timestamp, duration }]

    // ─── URL helpers ──────────────────────────────────────────────────────────

    function getBaseUrl() {
        const match = window.location.href.match(/^(https?:\/\/[^\/]+\/folder\/[^#]+#[^\/]+)/);
        return match ? match[1] : null;
    }

    // ─── Handle normalisation (unchanged from v1) ─────────────────────────────

    function normaliseHandle(value) {
        if (value === null || value === undefined) return null;
        let h = String(value).trim();
        if (!h) return null;

        if (window.M && window.M.d && window.M.d[h]) return h;

        if (window.M && window.M.d) {
            const known = Object.keys(window.M.d).find(k => h.includes(k));
            if (known) return known;
        }

        const tokens = h.match(/[A-Za-z0-9_-]{6,}/g);
        if (tokens && tokens.length) h = tokens[tokens.length - 1];

        if (h.length > 5 && !h.includes(' ')) return h;
        return null;
    }

    function addHandlesFrom(source, out) {
        if (!source) return;

        if (typeof source === 'string') {
            const h = normaliseHandle(source);
            if (h) out.push(h);
            return;
        }

        if (Array.isArray(source) || source instanceof Set) {
            [...source].forEach(v => {
                const h = normaliseHandle(v && (v.h || v.id || v.handle || v.nodeHandle || v));
                if (h) out.push(h);
            });
            return;
        }

        if (typeof source === 'object') {
            ['selected', 'selected_list', 'items'].forEach(k => {
                if (source[k]) addHandlesFrom(source[k], out);
            });

            Object.keys(source).forEach(k => {
                const v = source[k];
                const keyHandle = normaliseHandle(k);
                if (keyHandle && (v === true || v === 1 || typeof v === 'object')) out.push(keyHandle);

                const valueHandle = normaliseHandle(v && (v.h || v.id || v.handle || v.nodeHandle));
                if (valueHandle) out.push(valueHandle);
            });
        }
    }

    function getSelectedHandles() {
        let handles = [];

        if (window.$ && window.$.selected) addHandlesFrom(window.$.selected, handles);

        if (window.selectionManager) {
            if (typeof window.selectionManager.get_selected === 'function')
                addHandlesFrom(window.selectionManager.get_selected() || [], handles);
            addHandlesFrom(window.selectionManager.selected_list, handles);
            addHandlesFrom(window.selectionManager.selected, handles);
        }

        const sels = document.querySelectorAll([
            '.ui-selected', '.data-block-view.selected', 'tr.selected',
            '.grid-node.selected', '.file.selected', '.folder.selected',
            '.megaListItem.selected', '[aria-selected="true"]'
        ].join(','));

        sels.forEach(el => {
            [el.getAttribute('data-id'), el.getAttribute('data-h'),
             el.getAttribute('data-handle'), el.getAttribute('data-node-handle'),
             el.getAttribute('id')].forEach(v => {
                const h = normaliseHandle(v);
                if (h) handles.push(h);
            });
        });

        return [...new Set(handles)];
    }

    // ─── MEGA internal helpers (unchanged from v1) ────────────────────────────

    function isMediaViewerOpen() {
        const v = document.querySelector('.media-viewer-container');
        return !!(v && !v.classList.contains('hidden'));
    }

    function getNode(handle) {
        return (window.M && window.M.d && window.M.d[handle]) ||
               (window.M && window.M.v && window.M.v.find(n => n.h === handle)) || null;
    }

    function getNodeName(handle) {
        const node = getNode(handle);
        return (node && (node.name || node.n)) || handle;
    }

    function getChildrenHandles(parent) {
        const children = [];
        if (window.M && window.M.c && window.M.c[parent])
            Object.keys(window.M.c[parent]).forEach(h => children.push(h));
        if (window.M && window.M.d)
            Object.keys(window.M.d).forEach(h => {
                const node = window.M.d[h];
                if (node && node.p === parent) children.push(node.h || h);
            });
        return [...new Set(children)];
    }

    function getAllDescendantFileHandles(folderHandle) {
        const files = [], seen = new Set(), stack = [folderHandle];
        while (stack.length > 0) {
            const parent = stack.pop();
            if (!parent || seen.has(parent)) continue;
            seen.add(parent);
            getChildrenHandles(parent).forEach(h => {
                const node = getNode(h);
                if (node && node.t === 1) stack.push(node.h || h);
                else files.push((node && node.h) || h);
            });
        }
        return [...new Set(files)];
    }

    // ─── Metadata helpers (new) ────────────────────────────────────────────────

    function formatBytes(bytes) {
        if (bytes === null || bytes === undefined || isNaN(bytes)) return null;
        if (bytes === 0) return '0 B';
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
        if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + ' MB';
        return (bytes / 1073741824).toFixed(2) + ' GB';
    }

    function formatDuration(secs) {
        if (!secs || isNaN(secs)) return null;
        secs = Math.floor(secs);
        const h = Math.floor(secs / 3600);
        const m = Math.floor((secs % 3600) / 60);
        const s = secs % 60;
        return h > 0
            ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
            : `${m}:${String(s).padStart(2, '0')}`;
    }

    function getFileExtension(name) {
        const m = String(name || '').match(/\.([^.]{1,8})$/);
        return m ? m[1].toLowerCase() : '';
    }

    function tryGetVideoDuration(node) {
        if (!node) return null;
        // Direct node properties (set by MEGA after media attribute parsing)
        if (typeof node.playtime === 'number') return node.playtime;
        if (typeof node.duration === 'number') return node.duration;
        // Extended attribute object
        if (node.u && typeof node.u.playtime === 'number') return node.u.playtime;
        // Decoded attribute string (JSON)
        if (node.at) {
            try {
                const at = typeof node.at === 'string' ? JSON.parse(node.at) : node.at;
                if (at && at.c && typeof at.c.playtime === 'number') return at.c.playtime;
                if (at && typeof at.playtime === 'number') return at.playtime;
            } catch (e) {}
        }
        // M.getMediaAttribute (present in some builds)
        try {
            if (window.M && window.M.getMediaAttribute) {
                const a = window.M.getMediaAttribute(node);
                if (a && typeof a.playtime === 'number') return a.playtime;
            }
        } catch (e) {}
        return null;
    }

    function getNodeMeta(handle) {
        const node = getNode(handle);
        if (!node) return { sizeFormatted: null, ext: '', timestamp: null, duration: null };
        const rawDur = tryGetVideoDuration(node);
        return {
            sizeFormatted: formatBytes(node.s),
            ext:           getFileExtension(node.name || node.n),
            timestamp:     node.ts ? new Date(node.ts * 1000).toLocaleDateString() : null,
            duration:      rawDur ? formatDuration(rawDur) : null
        };
    }

    // ─── Auto-scan (new) ───────────────────────────────────────────────────────
    // Reads window.M.d directly — same internal MEGA data structure the manual
    // path uses — and collects every file node (files have a numeric 's'/size
    // property; folders do not).

    function autoScanAndAdd() {
        const baseUrl = getBaseUrl();
        if (!baseUrl) {
            updateStatus('Could not determine base URL. Is MEGA fully loaded?');
            return;
        }
        if (!window.M || !window.M.d) {
            updateStatus('MEGA is still loading — please wait a moment and try again.');
            return;
        }

        // Collect file handles: files have a numeric size and a name; folders don't.
        const fileHandles = Object.keys(window.M.d).filter(h => {
            const n = window.M.d[h];
            return n && typeof n.s === 'number' && (n.name || n.n);
        }).map(h => {
            const n = window.M.d[h];
            return n.h || h; // prefer node's own h field
        });

        if (fileHandles.length === 0) {
            updateStatus('No files found yet — MEGA may still be populating the folder.');
            return;
        }

        const existing = new Set(queuedLinks.map(x => x.url));
        let added = 0;

        fileHandles.forEach(h => {
            const url = `${baseUrl}/file/${h}`;
            if (existing.has(url)) return;
            const meta = getNodeMeta(h);
            queuedLinks.push({
                url, handle: h,
                name: getNodeName(h),
                status: 'Queued',
                checked: true,
                ...meta
            });
            existing.add(url);
            added++;
        });

        renderQueue();
        updateStatus(`Auto-scan complete: added ${added} file${added === 1 ? '' : 's'}. Total in queue: ${queuedLinks.length}.`);
    }

    // ─── Selection-based add — now includes metadata ───────────────────────────

    function buildItemsFromSelection() {
        const baseUrl = getBaseUrl();
        const handles = getSelectedHandles();
        if (!baseUrl || handles.length === 0) return [];

        const fileHandles = [];
        handles.forEach(h => {
            const node = getNode(h);
            if (node && node.t === 1) fileHandles.push(...getAllDescendantFileHandles(h));
            else fileHandles.push(h);
        });

        return [...new Set(fileHandles)].map(h => {
            const meta = getNodeMeta(h);
            return {
                url: `${baseUrl}/file/${h}`,
                handle: h,
                name: getNodeName(h),
                status: 'Queued',
                checked: true,
                ...meta
            };
        });
    }

    // ─── Clipboard ─────────────────────────────────────────────────────────────

    function copyText(text) {
        if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); return Promise.resolve(); }
        return navigator.clipboard.writeText(text);
    }

    // ─── Render ────────────────────────────────────────────────────────────────

    function escapeHtml(value) {
        return String(value).replace(/[&<>'"]/g, c =>
            ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[c]));
    }

    function statusColor(s) {
        if (s.includes('Opened')) return '#63b3ff';
        if (s.includes('Failed')) return '#ff6b6b';
        if (s.includes('Copied')) return '#00cc66';
        return '#ffd166';
    }

    function updateCounter() {
        const el = document.getElementById('mega-extract-counter');
        if (!el) return;
        if (queuedLinks.length === 0) { el.textContent = ''; return; }
        const checked = queuedLinks.filter(x => x.checked).length;
        el.textContent = `${checked} / ${queuedLinks.length} checked`;
    }

    function renderQueue() {
        if (!listEl) return;

        if (queuedLinks.length === 0) {
            listEl.innerHTML = '<div style="color:#666;font-size:12px;padding:14px;text-align:center;">No links queued yet.</div>';
            updateCounter();
            return;
        }

        listEl.innerHTML = queuedLinks.map((item, i) => {
            // Metadata badges
            const ext  = item.ext
                ? `<span style="background:#222;color:#999;padding:1px 5px;border-radius:4px;font-size:10px;font-family:monospace;">${escapeHtml(item.ext.toUpperCase())}</span>`
                : '';
            const size = item.sizeFormatted
                ? `<span style="color:#888;font-size:10px;">${escapeHtml(item.sizeFormatted)}</span>`
                : '';
            const dur  = item.duration
                ? `<span style="background:#0d2b15;color:#5cdb8a;padding:1px 6px;border-radius:4px;font-size:10px;">▶ ${escapeHtml(item.duration)}</span>`
                : '';
            const date = item.timestamp
                ? `<span style="color:#555;font-size:10px;">${escapeHtml(item.timestamp)}</span>`
                : '';
            const metaRow = (ext || size || dur || date)
                ? `<div style="display:flex;align-items:center;flex-wrap:wrap;gap:5px;margin-top:3px;">${ext}${size}${dur}${date}</div>`
                : '';

            return `
            <label style="display:grid;grid-template-columns:18px 1fr;gap:9px;padding:9px 10px;border-bottom:1px solid #1c1c1c;cursor:pointer;align-items:start;">
                <input class="mega-extract-check" data-index="${i}" type="checkbox" ${item.checked ? 'checked' : ''}
                    style="margin-top:3px;width:14px;height:14px;accent-color:#d90000;cursor:pointer;flex-shrink:0;">
                <div style="min-width:0;">
                    <div style="display:flex;justify-content:space-between;align-items:center;gap:6px;">
                        <span title="${escapeHtml(item.name)}"
                            style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600;color:#f0f0f0;">
                            ${escapeHtml(item.name)}
                        </span>
                        <span style="font-size:10px;color:${statusColor(item.status)};white-space:nowrap;flex-shrink:0;">
                            ${escapeHtml(item.status)}
                        </span>
                    </div>
                    ${metaRow}
                    <div title="${escapeHtml(item.url)}"
                        style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;color:#4ecb71;margin-top:3px;opacity:0.8;">
                        ${escapeHtml(item.url)}
                    </div>
                </div>
            </label>`;
        }).join('');

        listEl.querySelectorAll('.mega-extract-check').forEach(cb => {
            cb.onchange = () => {
                const i = Number(cb.getAttribute('data-index'));
                if (queuedLinks[i]) { queuedLinks[i].checked = cb.checked; updateCounter(); }
            };
        });

        updateCounter();
    }

    // ─── Panel ─────────────────────────────────────────────────────────────────

    function openPanel() {
        if (!panel) initPanel();
        panel.style.display = 'block';
        void panel.offsetWidth;
        panel.style.opacity = '1';
        panel.style.transform = 'translateY(0)';
        renderQueue();
    }

    function closePanel() {
        if (!panel) return;
        panel.style.opacity = '0';
        panel.style.transform = 'translateY(10px)';
        setTimeout(() => { if (panel) panel.style.display = 'none'; }, 180);
    }

    function updateStatus(msg) {
        const el = document.getElementById('mega-extract-status');
        if (el) el.textContent = msg;
    }

    function initPanel() {
        if (document.getElementById('mega-extract-panel')) {
            panel  = document.getElementById('mega-extract-panel');
            listEl = document.getElementById('mega-extract-list');
            return;
        }

        panel = document.createElement('div');
        panel.id = 'mega-extract-panel';
        Object.assign(panel.style, {
            position: 'fixed', bottom: '84px', left: '20px', zIndex: '2147483647',
            width: '460px', maxWidth: 'calc(100vw - 40px)',
            background: '#111', color: '#fff',
            border: '1px solid rgba(255,255,255,0.15)', borderRadius: '16px',
            boxShadow: '0 16px 48px rgba(0,0,0,0.75)', fontFamily: 'Arial, sans-serif',
            padding: '14px', display: 'none', opacity: '0', transform: 'translateY(10px)',
            transition: 'all 0.18s ease'
        });

        panel.innerHTML = `
            <!-- Header -->
            <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
                <div style="font-weight:700;font-size:15px;letter-spacing:0.3px;">⬇️ MEGA Link Queue</div>
                <button id="mega-extract-close"
                    style="background:transparent;color:#666;border:0;font-size:22px;cursor:pointer;line-height:1;padding:0 2px;transition:color 0.15s;"
                    onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#666'">×</button>
            </div>

            <!-- Status bar -->
            <div id="mega-extract-status"
                style="font-size:11px;color:#aaa;margin-bottom:8px;line-height:1.45;min-height:30px;
                       background:#1a1a1a;border-radius:7px;padding:6px 9px;border:1px solid #222;">
                Press Auto-scan to queue all files, or manually select files and use "Add selected".
            </div>

            <!-- Queue list -->
            <div id="mega-extract-list"
                style="width:100%;height:255px;overflow-y:auto;overflow-x:hidden;
                       background:#080808;border:1px solid #222;border-radius:10px;box-sizing:border-box;">
            </div>

            <!-- Counter -->
            <div id="mega-extract-counter"
                style="text-align:right;font-size:10px;color:#555;margin-top:4px;height:14px;"></div>

            <!-- Buttons -->
            <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px;">
                <!-- Auto-scan: full-width primary action -->
                <button id="mega-extract-autoscan"
                    style="grid-column:1/-1;padding:11px 8px;border:0;border-radius:10px;
                           background:linear-gradient(135deg,#cc0000,#8a0000);color:#fff;
                           font-weight:700;font-size:13px;cursor:pointer;letter-spacing:0.4px;
                           box-shadow:0 3px 12px rgba(204,0,0,0.4);transition:filter 0.15s;"
                    onmouseover="this.style.filter='brightness(1.15)'"
                    onmouseout="this.style.filter='brightness(1)'">
                    🔍 Auto-scan &amp; Add All Files
                </button>

                <button id="mega-extract-add"
                    style="padding:9px 6px;border:1px solid #3a0000;border-radius:9px;
                           background:#1e0000;color:#ff9999;font-weight:700;font-size:12px;cursor:pointer;
                           transition:background 0.15s;"
                    onmouseover="this.style.background='#2e0000'" onmouseout="this.style.background='#1e0000'">
                    + Add selected
                </button>
                <button id="mega-extract-refresh"
                    style="padding:9px 6px;border:1px solid #2a2a2a;border-radius:9px;
                           background:#1a1a1a;color:#bbb;font-weight:700;font-size:12px;cursor:pointer;
                           transition:background 0.15s;"
                    onmouseover="this.style.background='#242424'" onmouseout="this.style.background='#1a1a1a'">
                    ↻ Refresh sel.
                </button>

                <button id="mega-extract-copy"
                    style="padding:9px 6px;border:1px solid #00401e;border-radius:9px;
                           background:#001e0e;color:#4ecb71;font-weight:700;font-size:12px;cursor:pointer;
                           transition:background 0.15s;"
                    onmouseover="this.style.background='#002a14'" onmouseout="this.style.background='#001e0e'">
                    📋 Copy checked
                </button>
                <button id="mega-extract-download"
                    style="padding:9px 6px;border:1px solid #003366;border-radius:9px;
                           background:#001833;color:#7ab3ff;font-weight:700;font-size:12px;cursor:pointer;
                           transition:background 0.15s;"
                    onmouseover="this.style.background='#002244'" onmouseout="this.style.background='#001833'">
                    ⬇ Open checked
                </button>

                <button id="mega-extract-select-all"
                    style="padding:9px 6px;border:1px solid #2a2a2a;border-radius:9px;
                           background:#1a1a1a;color:#bbb;font-weight:700;font-size:12px;cursor:pointer;
                           transition:background 0.15s;"
                    onmouseover="this.style.background='#242424'" onmouseout="this.style.background='#1a1a1a'">
                    ☑ Toggle all
                </button>
                <button id="mega-extract-clear"
                    style="padding:9px 6px;border:1px solid #2a2a2a;border-radius:9px;
                           background:#1a1a1a;color:#bbb;font-weight:700;font-size:12px;cursor:pointer;
                           transition:background 0.15s;"
                    onmouseover="this.style.background='#242424'" onmouseout="this.style.background='#1a1a1a'">
                    🗑 Clear queue
                </button>
            </div>
        `;

        document.body.appendChild(panel);
        listEl = document.getElementById('mega-extract-list');
        renderQueue();

        // ── Button handlers ──────────────────────────────────────────────────

        document.getElementById('mega-extract-close').onclick = closePanel;

        // AUTO-SCAN: the main new feature
        document.getElementById('mega-extract-autoscan').onclick = autoScanAndAdd;

        // ADD SELECTED (original behaviour, now with metadata)
        document.getElementById('mega-extract-add').onclick = () => {
            const items = buildItemsFromSelection();
            const existing = new Set(queuedLinks.map(x => x.url));
            let added = 0;
            items.forEach(item => {
                if (!existing.has(item.url)) { queuedLinks.push(item); existing.add(item.url); added++; }
            });
            renderQueue();
            const sel = getSelectedHandles().length;
            updateStatus(`Added ${added} file link${added === 1 ? '' : 's'} from ${sel} selected item${sel === 1 ? '' : 's'}. Queue: ${queuedLinks.length}.`);
        };

        // OPEN (download) checked
        document.getElementById('mega-extract-download').onclick = () => {
            const sel = queuedLinks.filter(x => x.checked);
            if (!sel.length) { updateStatus('Nothing checked — tick some checkboxes first.'); return; }
            sel.forEach(item => {
                try {
                    item.status = 'Opened';
                    if (typeof GM_openInTab !== 'undefined')
                        GM_openInTab(item.url, { active: false, insert: true, setParent: true });
                    else
                        window.open(item.url, '_blank', 'noopener,noreferrer');
                } catch (e) { item.status = 'Failed'; }
            });
            renderQueue();
            updateStatus(`Opened ${sel.length} tab${sel.length === 1 ? '' : 's'}. Allow popups in your browser if tabs are blocked.`);
        };

        // COPY checked links
        document.getElementById('mega-extract-copy').onclick = () => {
            const sel = queuedLinks.filter(x => x.checked);
            const text = sel.map(x => x.url).join('\n');
            if (!text) { updateStatus('Nothing checked — tick some checkboxes first.'); return; }
            copyText(text).then(() => {
                sel.forEach(x => { x.status = 'Copied'; });
                renderQueue();
                updateStatus(`✅ Copied ${sel.length} link${sel.length === 1 ? '' : 's'} to clipboard!`);
            });
        };

        // REFRESH selection preview
        document.getElementById('mega-extract-refresh').onclick = () => {
            const n = getSelectedHandles().length;
            const p = buildItemsFromSelection();
            updateStatus(`${n} selected item${n === 1 ? '' : 's'} → ${p.length} file link${p.length === 1 ? '' : 's'} ready to add.`);
        };

        // TOGGLE ALL checkboxes
        document.getElementById('mega-extract-select-all').onclick = () => {
            const allOn = queuedLinks.length > 0 && queuedLinks.every(x => x.checked);
            queuedLinks.forEach(x => { x.checked = !allOn; });
            renderQueue();
            updateStatus(allOn ? 'Unchecked all.' : 'Checked all.');
        };

        // CLEAR queue
        document.getElementById('mega-extract-clear').onclick = () => {
            queuedLinks = [];
            renderQueue();
            updateStatus('Queue cleared.');
        };
    }

    // ─── Floating launcher button ──────────────────────────────────────────────

    function initButton() {
        if (document.getElementById('mega-extract-btn')) {
            btn = document.getElementById('mega-extract-btn');
            return;
        }

        btn = document.createElement('button');
        btn.id = 'mega-extract-btn';
        Object.assign(btn.style, {
            position: 'fixed', bottom: '25px', left: '25px', zIndex: '2147483647',
            padding: '12px 18px', background: '#cc0000', color: '#fff',
            border: '2px solid rgba(255,255,255,0.3)', borderRadius: '50px', cursor: 'pointer',
            fontWeight: '700', fontSize: '14px', letterSpacing: '0.3px',
            boxShadow: '0 8px 22px rgba(0,0,0,0.55)', fontFamily: 'Arial, sans-serif',
            transition: 'all 0.2s ease'
        });
        btn.innerHTML = '⬇️ MEGA Queue';
        btn.onclick = () => {
            initPanel();
            if (panel && panel.style.display === 'block') closePanel();
            else openPanel();
        };
        document.body.appendChild(btn);
    }

    // ─── Main loop ─────────────────────────────────────────────────────────────

    function updateLogic() {
        if (!document.body) return;
        if (!btn)   initButton();
        if (!panel) initPanel();

        if (btn) {
            const count      = getSelectedHandles().length;
            const viewerOpen = isMediaViewerOpen();
            btn.style.opacity = viewerOpen ? '0.4' : '1';
            btn.innerHTML     = (count > 0 && !viewerOpen)
                ? `⬇️ Queue ${count} selected`
                : '⬇️ MEGA Queue';
        }
    }

    setInterval(updateLogic, 500);

})();