Twitch MiniPlayer

MiniPlayer for Twitch with draggable functionality

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Twitch MiniPlayer
// @namespace    miniplayer
// @version      1.5.18
// @description  MiniPlayer for Twitch with draggable functionality
// @author       frz
// @icon         https://d2q79iu7y748jz.cloudfront.net/s/_squarelogo/256x256/13750fd9eb41e11a67d2410d47d9e33a
// @match        https://www.twitch.tv/*
// @grant        none
// ==/UserScript==

(function(){
const pad=10,k='miniPlayerPos',sK='miniPlayerSize',dK='miniPlayerDraggable';
let miniPlayer=null,gearBtn=null,menu=null;

const link=document.createElement('link');
link.href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap';
link.rel='stylesheet';
document.head.appendChild(link);

function getHeaderHeight(){
    const sel=['.top-nav','[data-a-target="top-nav"]','.top-nav__menu','header','.tw-header'];
    for(let s of sel){let e=document.querySelector(s);if(e){let r=e.getBoundingClientRect();if(r.height>0)return r.bottom+pad;}}
    return 80+pad;
}


function initializeMiniPlayer(){
    miniPlayer = document.querySelector('.persistent-player__border--mini');
    if(!miniPlayer) return false;
    if(!miniPlayer._originalWidth){
        const currentScale = parseFloat(localStorage.getItem(sK)) || 1;
        miniPlayer._originalWidth = miniPlayer.offsetWidth / currentScale;
        miniPlayer._originalHeight = miniPlayer.offsetHeight / currentScale;
    }
    let saved = localStorage.getItem(k);
    if(saved){
        try{
            let pos = JSON.parse(saved);
            miniPlayer.style.left = pos.left + 'px';
            miniPlayer.style.top = pos.top + 'px';
        }catch{
            miniPlayer.style.left = pad + 'px';
            miniPlayer.style.top = getHeaderHeight() + 'px';
        }
    } else {
        miniPlayer.style.left = pad + 'px';
        miniPlayer.style.top = getHeaderHeight() + 'px';
    }
    let savedSize = parseFloat(localStorage.getItem(sK)) || 1;
    miniPlayer.style.transform = `scale(${savedSize})`;
    if(miniPlayer._dragInitialized) return true;

    Object.assign(miniPlayer.style, {
        position: 'fixed',
        cursor: 'move',
        zIndex: '9999',
        margin: '0',
        transition: 'transform 0.2s ease',
        transformOrigin: 'top left'
    });

    addGearToMiniPlayer();
    let dragging=false, startX=0, startY=0, initLeft=0, initTop=0;

    miniPlayer.addEventListener('mousedown', e => {
        if(!miniPlayer.classList.contains('persistent-player__border--mini')) return;
        const draggable = localStorage.getItem(dK) !== 'false';
        if(!draggable || e.target.closest('.mini-gear')) return;
        dragging = true;
        startX = e.clientX;
        startY = e.clientY;
        initLeft = parseFloat(miniPlayer.style.left);
        initTop = parseFloat(miniPlayer.style.top);
        miniPlayer.style.transition = 'none';
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', stopDrag);
        e.preventDefault();
        e.stopPropagation();
    });

function drag(e) {
    if (!dragging) return;
    let dx = e.clientX - startX;
    let dy = e.clientY - startY;
    const currentScale = parseFloat(localStorage.getItem(sK)) || 1;
    const rect = miniPlayer.getBoundingClientRect();
    const scaledWidth = rect.width;
    const scaledHeight = rect.height;
    const newLeft = initLeft + dx;
    const newTop = initTop + dy;
    const minLeft = pad;
    const minTop = getHeaderHeight();
    const maxLeft = window.innerWidth - scaledWidth - pad;
    const maxTop = window.innerHeight - scaledHeight - pad;
    miniPlayer.style.left = Math.min(Math.max(newLeft, minLeft), maxLeft) + 'px';
    miniPlayer.style.top = Math.min(Math.max(newTop, minTop), maxTop) + 'px';
    e.preventDefault();
}

    function stopDrag(){
        if(!dragging) return;
        dragging = false;
        document.removeEventListener('mousemove', drag);
        document.removeEventListener('mouseup', stopDrag);
        localStorage.setItem(k, JSON.stringify({
            left: Math.round(parseFloat(miniPlayer.style.left)),
            top: Math.round(parseFloat(miniPlayer.style.top))
        }));
        miniPlayer.style.transition = 'transform 0.2s ease';
    }

    let resizeTimeout;
    window.addEventListener('resize', () => {
        clearTimeout(resizeTimeout);
        resizeTimeout = setTimeout(() => {
            const currentScale = parseFloat(localStorage.getItem(sK)) || 1;
            const scaledWidth = miniPlayer._originalWidth * currentScale;
            const scaledHeight = miniPlayer._originalHeight * currentScale;
            let left = parseFloat(miniPlayer.style.left);
            let top = parseFloat(miniPlayer.style.top);
            let newL = Math.max(pad, Math.min(left, window.innerWidth - scaledWidth - pad));
            let newT = Math.max(getHeaderHeight(), Math.min(top, window.innerHeight - scaledHeight - pad));
            if(newL !== left || newT !== top){
                miniPlayer.style.left = newL + 'px';
                miniPlayer.style.top = newT + 'px';
                localStorage.setItem(k, JSON.stringify({left: Math.round(newL), top: Math.round(newT)}));
            }
        }, 100);
    });

    miniPlayer._dragInitialized = true;
    return true;
}


function addGearToMiniPlayer(){
    if(!miniPlayer||!miniPlayer.classList.contains('persistent-player__border--mini')) return;
    document.querySelectorAll('.mini-gear').forEach(e=>e.remove());
    gearBtn=document.createElement('button');
    gearBtn.className='mini-gear';
    Object.assign(gearBtn.style,{position:'absolute',top:'8px',right:'40px',background:'rgba(0,0,0,0.5)',border:'none',borderRadius:'4px',width:'24px',height:'24px',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',zIndex:'99999',opacity:'0',transition:'opacity 0.15s ease-in-out'});
    gearBtn.innerHTML=`<svg width="16" height="16" viewBox="0 0 20 20" fill="#fff"><path d="M10 8a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"></path><path fill-rule="evenodd" d="M9 2h2a2.01 2.01 0 0 0 1.235 1.855l.53.22a2.01 2.01 0 0 0 2.185-.439l1.414 1.414a2.01 2.01 0 0 0-.439 2.185l.22.53A2.01 2.01 0 0 0 18 9v2a2.01 2.01 0 0 0-1.855 1.235l-.22.53a2.01 2.01 0 0 0 .44 2.185l-1.415 1.414a2.01 2.01 0 0 0-2.184-.439l-.531.22A2.01 2.01 0 0 0 11 18H9a2.01 2.01 0 0 0-1.235-1.854l-.53-.22a2.009 2.009 0 0 0-2.185.438L3.636 14.95a2.009 2.009 0 0 0 .438-2.184l-.22-.531A2.01 2.01 0 0 0 2 11V9c.809 0 1.545-.487 1.854-1.235l.22-.53a2.009 2.009 0 0 0-.438-2.185L5.05 3.636a2.01 2.01 0 0 0 2.185.438l.53-.22A2.01 2.01 0 0 0 9 2zm-4 8 1.464 3.536L10 15l3.535-1.464L15 10l-1.465-3.536L10 5 6.464 6.464 5 10z" clip-rule="evenodd"></path></svg>`;
    miniPlayer.appendChild(gearBtn);
    miniPlayer.addEventListener('mouseenter',()=>{gearBtn.style.opacity='1';});
    miniPlayer.addEventListener('mouseleave',()=>{gearBtn.style.opacity='0';});
    gearBtn.addEventListener('click',toggleMenu);
}


function createMenu(){
    if(menu) return;
    menu=document.createElement('div');
    Object.assign(menu.style,{
        position:'fixed',top:'50%',left:'50%',transform:'translate(-50%,-50%) scale(0.9)',
        background:'#18181b',color:'#fff',padding:'20px',borderRadius:'12px',zIndex:'10000',
        display:'none',fontFamily:'Inter,Arial,sans-serif',minWidth:'300px',
        boxShadow:'0 0 15px rgba(0,0,0,0.5)',transition:'transform 0.2s ease, opacity 0.2s ease',opacity:'0',cursor:'default'
    });
    menu.innerHTML=`<div id="menu-header" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;cursor:move;">
<h3 style="margin:0;font-size:16px;">Settings:</h3>
<span id="close-menu" style="cursor:pointer;font-size:18px;font-weight:bold;">✖</span>
</div>
<label style="display:block;margin-bottom:10px;font-size:14px;">MiniPlayer Size:
<input type="range" id="miniplayer-size" min="1" max="2" step="0.05" value="1" style="width:100%;margin-top:5px;">
<span id="size-value" style="margin-left:6px;">100%</span>
</label>
<label style="display:block;margin-bottom:10px;font-size:14px;">Enable MiniPlayer Drag:
<input type="checkbox" id="drag-toggle" style="margin-left:6px;">
</label>
<div style="text-align:right;font-size:12px;margin-top:10px;font-style:italic;">Created by: frz</div>`;
    document.body.appendChild(menu);

    document.getElementById('close-menu').addEventListener('click',()=>{menu.style.opacity='0';menu.style.transform='translate(-50%,-50%) scale(0.9)';setTimeout(()=>{menu.style.display='none';},200);});

    const slider=document.getElementById('miniplayer-size');
    const display=document.getElementById('size-value');
    const savedSize=parseFloat(localStorage.getItem(sK))||1;
    slider.value=savedSize;
    display.textContent=`${Math.round(savedSize*100)}%`;


    slider.style.accentColor = '#9147ff';

    let scaleTimeout;
    slider.addEventListener('input',e=>{
        const scale=parseFloat(e.target.value);
        display.textContent=`${Math.round(scale*100)}%`;
        clearTimeout(scaleTimeout);
        scaleTimeout=setTimeout(()=>{
            if(miniPlayer){
                const r=miniPlayer.getBoundingClientRect();
                localStorage.setItem(k,JSON.stringify({left:Math.round(r.left),top:Math.round(r.top)}));
                miniPlayer.style.transform=`scale(${scale})`;
                localStorage.setItem(sK,scale);
            }
        },150);
    });

    const dragToggle=document.getElementById('drag-toggle');
    const dragSaved=localStorage.getItem(dK);
    dragToggle.checked=dragSaved===null?true:dragSaved==='true';


    dragToggle.style.accentColor = '#9147ff';

    dragToggle.addEventListener('change',()=>{localStorage.setItem(dK,dragToggle.checked);});


    let isMenuDragging=false,offsetX=0,offsetY=0;
    const headerDrag=menu.querySelector('#menu-header');

    headerDrag.addEventListener('mousedown',e=>{isMenuDragging=true;const rect=menu.getBoundingClientRect();offsetX=e.clientX-rect.left;offsetY=e.clientY-rect.top;menu.style.transition='none';e.preventDefault();});
    document.addEventListener('mousemove',e=>{if(!isMenuDragging)return;let newX=e.clientX-offsetX;let newY=e.clientY-offsetY;newX=Math.max(0,Math.min(window.innerWidth-menu.offsetWidth,newX));newY=Math.max(0,Math.min(window.innerHeight-menu.offsetHeight,newY));menu.style.left=newX+'px';menu.style.top=newY+'px';menu.style.transform='translate(0,0)';});
    document.addEventListener('mouseup',()=>{if(!isMenuDragging)return;isMenuDragging=false;menu.style.transition='transform 0.2s ease, opacity 0.2s ease';});
}

function toggleMenu(){createMenu();if(!menu)return;if(menu.style.display==='block'){menu.style.opacity='0';menu.style.transform='translate(-50%,-50%) scale(0.9)';setTimeout(()=>{menu.style.display='none';},200);}else{menu.style.display='block';menu.style.opacity='1';menu.style.transform='translate(-50%,-50%) scale(1)';}}


function checkPlayerState(){
    const mini = document.querySelector('.persistent-player__border--mini');
    const full = document.querySelector('.persistent-player:not(.persistent-player__border--mini)');
    if (full) {
        document.querySelectorAll('.mini-gear').forEach(e => e.remove());
        if(menu && menu.style.display === 'block'){
            menu.style.opacity='0';
            menu.style.transform='translate(-50%,-50%) scale(0.9)';
            setTimeout(()=>{menu.style.display='none';},200);
        }
        if (miniPlayer) {
            miniPlayer._dragInitialized = false;
            miniPlayer.style.cursor = 'default';
            miniPlayer = null;
        }
    } else if (mini) {
        miniPlayer = mini;
        miniPlayer.style.cursor = 'move';
        if (!mini.querySelector('.mini-gear')) {
            addGearToMiniPlayer();
            initializeMiniPlayer();
        }
    }
}

const playerObserver=new MutationObserver(()=>{checkPlayerState();});
playerObserver.observe(document.body,{childList:true,subtree:true,attributes:true,attributeFilter:['class']});
setInterval(checkPlayerState,1000);
setTimeout(checkPlayerState,1000);
})();