YT Floating Utils

Shared utility functions for YouTube floating player/scripts

Acest script nu ar trebui instalat direct. Aceasta este o bibliotecă pentru alte scripturi care este inclusă prin directiva meta a // @require https://update.greasyfork.org/scripts/544326/1633907/YT%20Floating%20Utils.js

// ==UserScript==
// @name         YT Floating Utils
// @namespace    your.namespace
// @version      1.0
// @description  Shared utility functions for YouTube floating player/scripts
// @author       you
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    function restorePlayerLayoutOrDefault(playerBox) {
        let key = localStorage.getItem('yt_last_layout_key');
        let layout;
        if (key && localStorage.getItem(key)) {
            layout = JSON.parse(localStorage.getItem(key));
        }
        if (!layout && localStorage.getItem('yt_layout_1')) {
            key = 'yt_layout_1';
            layout = JSON.parse(localStorage.getItem('yt_layout_1'));
        }
        if (!layout) {
            layout = { top: 120, left: 140, width: 480, height: 272 };
        }
        playerBox.style.top = layout.top + 'px';
        playerBox.style.left = layout.left + 'px';
        playerBox.style.width = layout.width + 'px';
        playerBox.style.height = layout.height + 'px';
    }

    function getVideoIdFromIframeSrc(src) {
        try {
            const url = new URL(src);
            const match = url.pathname.match(/\/embed\/([^/?]+)/);
            return match?.[1] || null;
        } catch { return null; }
    }

    function extractVideoIdFromUrl(href) {
        try {
            const url = new URL(href, location.origin);
            if (url.pathname.startsWith('/watch')) {
                return url.searchParams.get('v');
            } else if (url.pathname.startsWith('/shorts/')) {
                return url.pathname.split('/shorts/')[1];
            }
        } catch (err) {}
        return null;
    }

    function getNextLayoutNumber() {
        const usedNums = Object.keys(localStorage)
            .filter(k => k.startsWith('yt_layout_'))
            .map(k => parseInt(k.replace('yt_layout_', ''), 10))
            .sort((a, b) => a - b);
        for (let i = 1; i <= 5; i++) {
            if (!usedNums.includes(i)) return i;
        }
        let next = 1;
        while (usedNums.includes(next)) next++;
        return next;
    }

    function saveLayout(box, STORAGE_KEY, layoutInitialized) {
        const rect = box.getBoundingClientRect();
        if (!layoutInitialized) return;
        if (rect.width < 100 || rect.height < 100) return;
        localStorage.setItem(STORAGE_KEY, JSON.stringify({
            width: rect.width,
            height: rect.height,
            top: rect.top,
            left: rect.left,
            bottom: null,
            right: null
        }));
    }

    function applySavedLayout(box, STORAGE_KEY, layoutInitialized) {
        const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
        const validWidth = saved.width && saved.width > 100;
        const validHeight = saved.height && saved.height > 100;
        if (!validWidth || !validHeight) {
            box.style.width = '320px';
            box.style.height = '180px';
            box.style.top = '';
            box.style.left = '';
            box.style.right = '';
            box.style.bottom = '100px';
            layoutInitialized = true;
            return;
        }
        box.style.top = '';
        box.style.left = '';
        box.style.right = '';
        box.style.bottom = '';
        box.style.width = saved.width + 'px';
        box.style.height = saved.height + 'px';
        if (typeof saved.top === 'number') box.style.top = saved.top + 'px';
        else box.style.bottom = '100px';
        if (typeof saved.left === 'number') box.style.left = saved.left + 'px';
        else box.style.right = '30px';
        layoutInitialized = true;
    }

    function saveWatchLaterPanelState(panel) {
        localStorage.setItem('watchLaterPanelPos', JSON.stringify({
            top: panel.style.top,
            left: panel.style.left,
            width: panel.style.width,
            height: panel.style.height
        }));
    }

    function restoreWatchLaterPanelState(panel) {
        const state = JSON.parse(localStorage.getItem('watchLaterPanelPos') || '{}');
        if (state.top) panel.style.top = state.top;
        if (state.left) panel.style.left = state.left;
        if (state.width) panel.style.width = state.width;
        if (state.height) panel.style.height = state.height;
    }

    function extractWatchLaterVideos() {
        const items = document.querySelectorAll('ytd-playlist-video-renderer');
        return Array.from(items).map(el => {
            const titleEl = el.querySelector('#video-title');
            const href = titleEl?.href || '#';
            const title = titleEl?.textContent.trim() || 'Untitled';
            const imgEl = el.querySelector('.yt-core-image img') || el.querySelector('img');
            let thumb = '';
            if (imgEl) {
                if (imgEl.src && !imgEl.src.startsWith('data:')) thumb = imgEl.src;
                else if (imgEl.dataset.src) thumb = imgEl.dataset.src;
                else if (imgEl.getAttribute('data-thumb')) thumb = imgEl.getAttribute('data-thumb');
            }
            return { title, href, thumb };
        });
    }

    function showVolumeAtCursor(volumePercent, x, y, minimal = false) {
        if (!window.volumeIndicator) return;
        window.volumeIndicator.textContent = minimal
            ? `${volumePercent}`
            : (volumePercent === "0" ? '🔇 0%' : `🔊 ${volumePercent}%`);
        window.volumeIndicator.style.top = `${y - 2}px`;
        window.volumeIndicator.style.left = `${x + 20}px`;
        window.volumeIndicator.style.opacity = '1';
        window.volumeIndicator.style.background = 'transparent';
        window.volumeIndicator.style.padding = '0';
        window.volumeIndicator.style.fontSize = '32px';
        window.volumeIndicator.style.fontWeight = '900';
        window.volumeIndicator.style.color = 'white';
        window.volumeIndicator.style.textShadow = '1px 1px 2px black';
        clearTimeout(window.volumeIndicator._hideTimeout);
        window.volumeIndicator._hideTimeout = setTimeout(() => {
            window.volumeIndicator.style.opacity = '0';
        }, 800);
    }

    function showToast(msg) {
        if (!window.toast) return;
        window.toast.textContent = msg;
        window.toast.style.opacity = '1';
        window.toast.style.transform = 'translateX(-50%) scale(1.2)';
        window.toast.style.transition = 'opacity 0.4s ease, transform 0.2s ease';
        setTimeout(() => {
            window.toast.style.opacity = '0';
            window.toast.style.transform = 'translateX(-50%) scale(1)';
        }, 800);
    }

    function attachContextMenu(btn, layoutKey) {
        btn.addEventListener('contextmenu', (e) => {
            e.preventDefault();
            const existing = JSON.parse(localStorage.getItem(layoutKey));
            if (!existing) return;
            const menu = document.getElementById('customLayoutMenu');
            if (!menu) return;
            menu.innerHTML = '';
            const rename = document.createElement('div');
            rename.textContent = '✏️ Rename';
            rename.style.cssText = 'padding:6px 12px;cursor:pointer;';
            rename.onclick = () => {
                const newLabel = prompt('Enter new button label:', btn.textContent);
                if (newLabel) {
                    btn.textContent = newLabel;
                    showToast(`✅ Renamed to ${newLabel}`);
                }
                menu.style.display = 'none';
            };
            const edit = document.createElement('div');
            edit.textContent = 'Edit Position & Size';
            edit.style.cssText = 'padding:6px 12px;cursor:pointer;';
            edit.onclick = () => {
                const newTop = prompt('Top:', existing.top);
                const newLeft = prompt('Left:', existing.left);
                const newWidth = prompt('Width:', existing.width);
                const newHeight = prompt('Height:', existing.height);
                if ([newTop, newLeft, newWidth, newHeight].every(val => !isNaN(val))) {
                    const updated = {
                        top: parseInt(newTop),
                        left: parseInt(newLeft),
                        width: parseInt(newWidth),
                        height: parseInt(newHeight)
                    };
                    localStorage.setItem(layoutKey, JSON.stringify(updated));
                    showToast('✅ Layout updated');
                } else {
                    showToast('❌ Invalid input');
                }
                menu.style.display = 'none';
            };
            const del = document.createElement('div');
            del.textContent = '🗑 Delete';
            del.style.cssText = 'padding:6px 12px;color:red;cursor:pointer;';
            del.onclick = () => {
                if (confirm('Delete this layout?')) {
                    localStorage.removeItem(layoutKey);
                    btn.parentNode?.removeChild(btn);
                    showToast('🗑 Layout deleted');
                }
                menu.style.display = 'none';
            };
            [rename, edit, del].forEach(item => {
                item.addEventListener('mouseover', () => {
                    item.style.background = '#f0f0f0';
                });
                item.addEventListener('mouseout', () => {
                    item.style.background = 'white';
                });
                menu.appendChild(item);
            });
            menu.style.top = e.clientY + 'px';
            menu.style.left = e.clientX + 'px';
            menu.style.display = 'block';
        });
    }

    function setPanelVisibility(isVisible, panelId, commentBtn, comKey, btnOn, btnOff) {
        const panel = document.getElementById(panelId);
        if (!panel) {
            showToast('❌ Floating Comments Panel not found!');
            return false;
        }
        panel.style.display = isVisible ? 'block' : 'none';
        commentBtn.textContent = isVisible ? btnOn : btnOff;
        commentBtn.title = `Toggle Floating Comments Panel (${isVisible ? 'ON' : 'OFF'})`;
        localStorage.setItem(comKey, isVisible ? 'on' : 'off');
        return true;
    }

    function getYouTubeMainVolume() {
        const slider = document.querySelector('.ytp-volume-panel .ytp-volume-slider-handle');
        const val = slider?.getAttribute('aria-valuenow');
        return val ? parseInt(val) : null;
    }

    function getIframeCurrentTime(iframe, cb) {
        const handler = (e) => {
            if (e.origin.includes('youtube')) {
                try {
                    const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
                    if (data.event === 'infoDelivery' && typeof data.info === 'number') {
                        window.removeEventListener('message', handler);
                        cb(data.info);
                    }
                } catch (err) {}
            }
        };
        window.addEventListener('message', handler);
        iframe.contentWindow?.postMessage(JSON.stringify({
            event: "command",
            func: "getCurrentTime",
            args: []
        }), "*");
    }

    async function fetchVideoTitle(videoId) {
        const html = await fetch(`https://corsproxy.io/?https://www.youtube.com/watch?v=${videoId}`).then(r => r.text());
        let m = html.match(/<title>(.*?)<\/title>/i);
        return m ? m[1].replace(' - YouTube', '').trim() : videoId;
    }

    async function fetchChannelNameByVideoId(videoId) {
        const html = await fetch(`https://corsproxy.io/?https://www.youtube.com/watch?v=${videoId}`).then(r => r.text());
        let m = html.match(/"ownerChannelName":"(.*?)"/);
        if (!m) m = html.match(/<link itemprop="name" content="([^"]+)"\/?>/);
        if (!m) {
            m = html.match(/<a[^>]*href="\/@(.*?)"[^>]*>([^<]+)<\/a>/);
            if (m && m[2]) return m[2].trim();
        }
        return m ? m[1].trim() : '';
    }

    async function fetchCommentsHTML(videoId) {
        const url = `https://www.youtube.com/watch?v=${videoId}`;
        const resp = await fetch(url);
        const text = await resp.text();
        const ytInitialDataMatch = text.match(/var ytInitialData = (.*?);<\/script>/s);
        const ytInitialData = ytInitialDataMatch ? JSON.parse(ytInitialDataMatch[1]) : null;
        const threads = [];
        if (ytInitialData) {
            let section = ytInitialData.contents
                ?.twoColumnWatchNextResults
                ?.results
                ?.results
                ?.contents
                ?.find(x => x.itemSectionRenderer && x.itemSectionRenderer.sectionIdentifier === "comment-item-section");
            let items = section?.itemSectionRenderer?.contents || [];
            for (const t of items) {
                if (t.commentThreadRenderer) {
                    threads.push(t.commentThreadRenderer);
                }
            }
        }
        return threads;
    }

    async function updateCommentsPanelForVideo(videoId) {
        const panel = document.getElementById('yt-main-comments-float-panel');
        if (!panel) return;
        panel.innerHTML = '<div style="padding:12px;font-size:14px;">Loading comments...</div>';
        let threads = [];
        try {
            threads = await fetchCommentsHTML(videoId);
        } catch (e) {
            panel.innerHTML = '<div style="padding:12px;font-size:14px;">Failed to load comments.</div>';
            return;
        }
        if (!threads.length) {
            panel.innerHTML = '<div style="padding:12px;font-size:14px;">No comments found (may be disabled, restricted, or private).</div>';
            return;
        }
        panel.innerHTML = '';
        threads.forEach(thread => {
            const c = thread.commentRenderer || thread.commentThreadRenderer?.comment?.commentRenderer;
            if (!c) return;
            const author = c.authorText?.simpleText || '';
            const content = c.contentText?.runs?.map(r => r.text).join('') || '';
            const commentDiv = document.createElement('div');
            commentDiv.style = 'margin-bottom:12px; padding:6px 0; border-bottom:1px solid #eee;';
            commentDiv.innerHTML = `<b>${author}</b><br>${content}`;
            panel.appendChild(commentDiv);
        });
    }

    function isArabic(text) {
        return /[\u0600-\u06FF]/.test(text);
    }

    async function getVideoTitleById(videoId) {
        let url = `https://www.youtube.com/watch?v=${videoId}`;
        let html = await fetch(url).then(r => r.text());
        let m = html.match(/<title>([^<]+)<\/title>/);
        return m ? m[1].replace(' - YouTube', '').trim() : videoId;
    }

    window.YTFloatingUtils = {
        restorePlayerLayoutOrDefault,
        getVideoIdFromIframeSrc,
        extractVideoIdFromUrl,
        getNextLayoutNumber,
        saveLayout,
        applySavedLayout,
        saveWatchLaterPanelState,
        restoreWatchLaterPanelState,
        extractWatchLaterVideos,
        showVolumeAtCursor,
        showToast,
        attachContextMenu,
        setPanelVisibility,
        getYouTubeMainVolume,
        getIframeCurrentTime,
        fetchVideoTitle,
        fetchChannelNameByVideoId,
        updateCommentsPanelForVideo,
        isArabic,
        getVideoTitleById
    };

})();