Twitch MiniPlayer

MiniPlayer for Twitch with draggable functionality

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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);
})();