Music Manager - Universal Sync

Press '/' for menu. Songs stay saved across ALL different websites! Now with search + hover URL scanner that auto-copies.

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.greasyfork.org/scripts/579897/1835463/Music%20Manager%20-%20Universal%20Sync.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Music Manager - Universal Sync
// @namespace    http://tampermonkey.net/
// @version      12.0
// @description  Press '/' for menu. Songs stay saved across ALL different websites! Now with search + hover URL scanner that auto-copies.
// @author       Gemini
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ---- CONFIG ----
    const defaultSongs = [
        { name: "Arctic Monkeys - 505", url: "https://youtu.be/CKI8iQTgZKU?si=O3Z_qmJTGLECGhCa", fav: true, vid: false },
        { name: "The Neighbourhood - Softcore", url: "https://youtu.be/Ssj5ZBuhwIo?si=h4fTut2Vq7IM1QFC", fav: false, vid: false }
    ];

    let savedSongs = GM_getValue('tm_universal_playlist', defaultSongs);
    let currentTab = 'library';
    let searchQuery = '';
    let scannerEnabled = true;

    // ---- UNIVERSAL URL SCANNER (hovers over websites & auto-copies) ----
    const scannerBox = document.createElement('div');
    Object.assign(scannerBox.style, {
        position: 'fixed',
        display: 'none',
        background: '#0d0d0d',
        border: '1px solid #ff3333',
        borderRadius: '8px',
        padding: '8px 14px',
        color: '#00ff88',
        fontFamily: 'monospace',
        fontSize: '12px',
        zIndex: '999999',
        pointerEvents: 'none',
        maxWidth: '600px',
        overflow: 'hidden',
        textOverflow: 'ellipsis',
        whiteSpace: 'nowrap',
        boxShadow: '0 4px 20px rgba(255,0,0,0.3)',
        transition: 'opacity 0.1s'
    });
    document.body.appendChild(scannerBox);

    // Scanner status indicator in corner
    const scannerStatus = document.createElement('div');
    Object.assign(scannerStatus.style, {
        position: 'fixed',
        bottom: '10px',
        right: '10px',
        background: '#0d0d0d',
        border: '1px solid #00ff88',
        borderRadius: '6px',
        padding: '4px 10px',
        color: '#00ff88',
        fontFamily: 'monospace',
        fontSize: '10px',
        zIndex: '999999',
        cursor: 'pointer',
        userSelect: 'none',
        opacity: '0.6'
    });
    scannerStatus.textContent = '🔍 SCANNER ON';
    scannerStatus.title = 'Click to toggle URL scanner on/off';
    scannerStatus.onclick = () => {
        scannerEnabled = !scannerEnabled;
        scannerStatus.textContent = scannerEnabled ? '🔍 SCANNER ON' : '🔍 SCANNER OFF';
        scannerStatus.style.borderColor = scannerEnabled ? '#00ff88' : '#ff3333';
        scannerStatus.style.color = scannerEnabled ? '#00ff88' : '#ff3333';
        if (!scannerEnabled) scannerBox.style.display = 'none';
    };
    document.body.appendChild(scannerStatus);

    // Track last copied URL to avoid spam
    let lastCopiedUrl = '';

    function positionScannerBox(e) {
        let x = e.clientX + 16;
        let y = e.clientY + 16;
        const rect = scannerBox.getBoundingClientRect();
        if (x + rect.width > window.innerWidth - 10) x = e.clientX - rect.width - 16;
        if (y + rect.height > window.innerHeight - 10) y = e.clientY - rect.height - 16;
        if (x < 10) x = 10;
        if (y < 10) y = 10;
        scannerBox.style.left = x + 'px';
        scannerBox.style.top = y + 'px';
    }

    // Hover listener on the whole document
    document.addEventListener('mousemove', (e) => {
        if (!scannerEnabled) return;

        const target = e.target;
        let url = null;

        // Check if hovering over a link
        if (target.tagName === 'A' && target.href) {
            url = target.href;
        }
        // Check if hovering over an image
        else if (target.tagName === 'IMG' && target.src) {
            url = target.src;
        }
        // Check for parent anchors (in case inner elements like spans inside <a>)
        else {
            let parent = target.closest('a');
            if (parent && parent.href) {
                url = parent.href;
            }
            // Check for elements with data-url or similar attributes
            if (!url && target.dataset && target.dataset.url) {
                url = target.dataset.url;
            }
        }

        if (url) {
            // Show the URL
            scannerBox.textContent = '📍 ' + url;
            scannerBox.style.display = 'block';
            positionScannerBox(e);

            // Auto-copy to clipboard (only if it's a new URL)
            if (url !== lastCopiedUrl) {
                lastCopiedUrl = url;
                navigator.clipboard.writeText(url).catch(() => {
                    // Fallback for older browsers
                    const ta = document.createElement('textarea');
                    ta.value = url;
                    ta.style.position = 'fixed';
                    ta.style.opacity = '0';
                    document.body.appendChild(ta);
                    ta.select();
                    document.execCommand('copy');
                    document.body.removeChild(ta);
                });
                // Flash effect
                scannerBox.style.borderColor = '#00ff88';
                setTimeout(() => { scannerBox.style.borderColor = '#ff3333'; }, 200);
            }
        } else {
            scannerBox.style.display = 'none';
        }
    });

    // Clean up when leaving the page
    document.addEventListener('mouseleave', () => {
        if (scannerEnabled) scannerBox.style.display = 'none';
    });

    // ---- INFO MODAL (Draggable) ----
    const infoBox = document.createElement('div');
    Object.assign(infoBox.style, {
        position: 'fixed', top: '50%', left: '50%',
        transform: 'translate(-50%, -50%)',
        background: '#1e1e1e', border: '1px solid #444', borderRadius: '10px',
        zIndex: '10001', display: 'none', flexDirection: 'column',
        width: '300px', padding: '0 0 20px 0',
        boxShadow: '0 0 50px rgba(0,0,0,0.9)', fontFamily: 'Segoe UI, sans-serif',
        color: 'white', textAlign: 'center', overflow: 'hidden'
    });
    document.body.appendChild(infoBox);

    function showInfo(song) {
        infoBox.innerHTML = `
            <div id="infoDragHandle" style="background: #333; color: #ff0000; padding: 8px; cursor: grab; font-weight: bold; font-size: 12px; margin-bottom: 15px; user-select: none;">
                :: DETAILS (Drag) ::
            </div>
            <div style="padding: 0 20px;">
                <div style="font-size: 11px; color: #aaa;">NAME</div>
                <div style="font-size: 13px; margin-bottom: 15px; word-break: break-all;">${song.name}</div>
                <div style="font-size: 11px; color: #aaa;">URL</div>
                <div style="font-size: 12px; margin-bottom: 20px; word-break: break-all; color: #00abff;">${song.url}</div>
                <button id="closeInfo" style="padding: 8px 20px; background: #ff0000; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">CLOSE</button>
            </div>
        `;
        infoBox.style.display = 'flex';
        document.getElementById('closeInfo').onclick = () => infoBox.style.display = 'none';
        setupDragging(document.getElementById('infoDragHandle'), infoBox, true);
    }

    // ---- MAIN MENU ----
    const menu = document.createElement('div');
    Object.assign(menu.style, {
        position: 'fixed', top: '15%', left: '35%',
        background: '#121212', border: '2px solid #ff0000', borderRadius: '12px',
        zIndex: '10000', display: 'none', flexDirection: 'column',
        width: '450px', maxHeight: '88vh',
        boxShadow: '0 10px 40px rgba(0,0,0,0.8)', fontFamily: 'Segoe UI, sans-serif',
        color: 'white', overflow: 'hidden'
    });

    // -- Drag Handle --
    const dragHandle = document.createElement('div');
    dragHandle.innerText = ":: Music Menu (Drag) ::";
    Object.assign(dragHandle.style, {
        background: '#ff0000', color: 'white', padding: '8px', cursor: 'grab',
        textAlign: 'center', fontWeight: 'bold', fontSize: '11px', userSelect: 'none'
    });
    menu.appendChild(dragHandle);

    // -- Nav Tabs --
    const nav = document.createElement('div');
    nav.style.display = 'flex';
    nav.style.background = '#1a1a1a';

    const libBtn = document.createElement('button');
    const favBtn = document.createElement('button');
    const vidBtn = document.createElement('button');

    [libBtn, favBtn, vidBtn].forEach(b => {
        Object.assign(b.style, {
            flex: '1', padding: '10px', cursor: 'pointer', border: 'none',
            background: 'none', color: 'white', fontWeight: 'bold', fontSize: '12px'
        });
    });

    libBtn.innerText = "📁 Movies/songs"; favBtn.innerText = "⭐ Music"; vidBtn.innerText = "⚪ Movies";

    libBtn.onclick = () => { currentTab = 'library'; searchQuery = ''; updateTabStyles(); renderPlaylist(); };
    favBtn.onclick = () => { currentTab = 'favorites'; searchQuery = ''; updateTabStyles(); renderPlaylist(); };
    vidBtn.onclick = () => { currentTab = 'videos'; searchQuery = ''; updateTabStyles(); renderPlaylist(); };

    function updateTabStyles() {
        libBtn.style.borderBottom = currentTab === 'library' ? '3px solid #ff0000' : 'none';
        favBtn.style.borderBottom = currentTab === 'favorites' ? '3px solid #ffcc00' : 'none';
        vidBtn.style.borderBottom = currentTab === 'videos' ? '3px solid #00abff' : 'none';
    }

    nav.append(libBtn, favBtn, vidBtn);
    menu.appendChild(nav);

    // -- SEARCH BAR --
    const searchBar = document.createElement('input');
    searchBar.placeholder = "🔍 Search by name or keyword...";
    Object.assign(searchBar.style, {
        margin: '10px 15px 0 15px',
        padding: '9px 12px',
        background: '#1a1a1a',
        color: 'white',
        border: '1px solid #444',
        borderRadius: '6px',
        fontFamily: 'Segoe UI, sans-serif',
        fontSize: '13px',
        outline: 'none'
    });
    searchBar.addEventListener('input', () => {
        searchQuery = searchBar.value.toLowerCase().trim();
        renderPlaylist();
    });
    searchBar.addEventListener('keydown', (e) => e.stopPropagation());
    menu.appendChild(searchBar);

    // -- SCROLLABLE LIST --
    const listContainer = document.createElement('div');
    Object.assign(listContainer.style, {
        padding: '15px',
        overflowY: 'auto',
        flexGrow: '1',
        maxHeight: '350px',
        minHeight: '120px'
    });
    menu.appendChild(listContainer);

    function renderPlaylist() {
        listContainer.innerHTML = '';

        let filtered = savedSongs.filter(s => {
            if (currentTab === 'favorites') return s.fav;
            if (currentTab === 'videos') return s.vid;
            return !s.fav && !s.vid;
        });

        if (searchQuery) {
            const q = searchQuery.toLowerCase();
            filtered = filtered.filter(s =>
                s.name.toLowerCase().includes(q) ||
                s.url.toLowerCase().includes(q)
            );
        }

        const count = document.createElement('div');
        count.style = 'font-size: 11px; color: #666; margin-bottom: 8px;';
        count.textContent = `${filtered.length} item${filtered.length !== 1 ? 's' : ''}`;
        listContainer.appendChild(count);

        if (filtered.length === 0) {
            const empty = document.createElement('div');
            empty.style = 'color: #555; text-align: center; padding: 30px 0; font-size: 13px;';
            empty.textContent = searchQuery ? 'No matches found.' : 'This folder is empty.';
            listContainer.appendChild(empty);
            return;
        }

        filtered.forEach(song => {
            const row = document.createElement('div');
            row.style = 'display: flex; gap: 8px; margin-bottom: 8px; align-items: center;';

            const star = document.createElement('button');
            star.innerText = song.fav ? "★" : "☆";
            star.style = `background:none; border:none; color:${song.fav ? '#ffcc00':'#444'}; cursor:pointer; font-size:18px;`;
            star.onclick = () => { song.fav = !song.fav; if(song.fav) song.vid = false; save(); };

            const circ = document.createElement('button');
            circ.innerText = song.vid ? "●" : "○";
            circ.style = `background:none; border:none; color:${song.vid ? '#00abff':'#444'}; cursor:pointer; font-size:18px;`;
            circ.onclick = () => { song.vid = !song.vid; if(song.vid) song.fav = false; save(); };

            const mainBtn = document.createElement('button');
            mainBtn.innerText = song.name;
            mainBtn.style = 'flex-grow:1; padding:8px; background:#222; color:white; border:1px solid #333; border-radius:4px; text-align:left; font-size:13px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:pointer;';
            mainBtn.onclick = () => window.open(song.url, '_blank');

            const infoBtn = document.createElement('button');
            infoBtn.innerText = "⋮";
            infoBtn.style = 'background:none; border:none; color:#888; cursor:pointer; font-size:18px; padding:0 5px;';
            infoBtn.onclick = (e) => { e.stopPropagation(); showInfo(song); };

            const del = document.createElement('button');
            del.innerText = "✕";
            del.style = 'background:none; border:none; color:#444; cursor:pointer; font-size:14px;';
            del.onclick = () => { savedSongs = savedSongs.filter(s => s !== song); save(); };

            row.append(star, circ, mainBtn, infoBtn, del);
            listContainer.appendChild(row);
        });
    }

    // -- FOOTER --
    const footer = document.createElement('div');
    footer.style = 'padding: 15px; border-top: 1px solid #333; display: flex; flex-direction: column; gap: 8px;';

    const iName = document.createElement('input');
    iName.placeholder = "Name...";
    iName.style = 'padding:8px; background:#1a1a1a; color:white; border:1px solid #444; border-radius:4px;';

    const iUrl = document.createElement('input');
    iUrl.placeholder = "URL...";
    iUrl.style = 'padding:8px; background:#1a1a1a; color:white; border:1px solid #444; border-radius:4px;';

    const addBtn = document.createElement('button');
    addBtn.innerText = "Add to Library";
    addBtn.style = 'padding:10px; background:#ff0000; color:white; border:none; border-radius:5px; cursor:pointer; font-weight:bold;';

    addBtn.onclick = () => {
        if(iName.value && iUrl.value) {
            savedSongs.push({ name: iName.value, url: iUrl.value, fav: false, vid: false });
            iName.value = '';
            iUrl.value = '';
            save();
        }
    };

    footer.append(iName, iUrl, addBtn);
    menu.appendChild(footer);

    // ---- UNIVERSAL SAVE ----
    function save() {
        GM_setValue('tm_universal_playlist', savedSongs);
        renderPlaylist();
    }

    // ---- DRAG LOGIC ----
    function setupDragging(handle, target, isCentered = false) {
        let dragging = false, ox, oy;
        handle.onmousedown = (e) => {
            dragging = true;
            if (isCentered) {
                const rect = target.getBoundingClientRect();
                target.style.transform = 'none';
                target.style.left = rect.left + 'px';
                target.style.top = rect.top + 'px';
            }
            ox = e.clientX - target.offsetLeft;
            oy = e.clientY - target.offsetTop;
        };
        document.addEventListener('mousemove', (e) => {
            if (dragging) {
                target.style.left = (e.clientX - ox) + 'px';
                target.style.top = (e.clientY - oy) + 'px';
            }
        });
        document.addEventListener('mouseup', () => dragging = false);
    }

    setupDragging(dragHandle, menu);

    // ---- KEYBOARD TOGGLE ----
    window.addEventListener('keydown', (e) => {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
        if (e.key === '/') {
            e.preventDefault();
            const isHidden = menu.style.display === 'none';
            menu.style.display = isHidden ? 'flex' : 'none';
            if (!isHidden) infoBox.style.display = 'none';
            else searchBar.focus();
        }
    });

    // ---- FINAL APPEND & INIT ----
    document.body.appendChild(menu);
    updateTabStyles();
    renderPlaylist();

})();