Twitch Stream Zoom and Pan

Barre zoom à côté de la recherche Twitch, ZQSD = déplacement (shift pour + rapide), A/E/R = zoom/dézoom/reset

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitch Stream Zoom and Pan
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Barre zoom à côté de la recherche Twitch, ZQSD = déplacement (shift pour + rapide), A/E/R = zoom/dézoom/reset
// @author       Guile93
// @match        *://*.twitch.tv/*
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';
  let zoomLevel = 1;
  const minZoom = 1;
  const maxZoom = 3;
  const zoomStep = 0.1;
  let panX = 0;
  let panY = 0;
  const panStep = 20;
  const $ = (sel, root = document) => root.querySelector(sel);
  const updateTransform = () => {
    const v = $('video');
    if (!v) return;
    v.style.transformOrigin = 'center center';
    v.style.transition = 'transform 0.15s ease-out';
    v.style.transform = `scale(${zoomLevel}) translate(${panX / zoomLevel}px, ${panY / zoomLevel}px)`;
  };
  const makeBtn = (id, label, title, onClick) => {
    const b = document.createElement('button');
    b.id = id;
    b.type = 'button';
    b.textContent = label;
    b.title = title || '';
    b.style.cssText = `
      border: none;
      padding: 4px 8px;
      margin: 0 2px;
      border-radius: 6px;
      background: #3a3a3a;
      color: #fff;
      font: 500 12px/1 system-ui;
      cursor: pointer;
    `;
    b.addEventListener('mouseenter', () => (b.style.background = '#4a4a4a'));
    b.addEventListener('mouseleave', () => (b.style.background = '#3a3a3a'));
    b.addEventListener('click', onClick);
    return b;
  };
  const buildToolbar = () => {
    const bar = document.createElement('div');
    bar.className = 'twitch-zoom-toolbar';
    bar.style.cssText = `
      display: flex;
      align-items: center;
      gap: 4px;
      background: #2b2b2b;
      border: 1px solid #444;
      border-radius: 8px;
      padding: 4px 6px;
      margin-left: 8px;
      height: 32px;
    `;
    const indicator = document.createElement('span');
    indicator.id = 'zoomIndicator';
    indicator.style.cssText = 'margin-left: 6px; font: 600 12px/1 system-ui; color: #ddd; opacity: .9;';
    const updInd = () => (indicator.textContent = `×${zoomLevel.toFixed(1)}`);
    const plus = makeBtn('zoomIn', '+', 'Zoom (+ ou A si zoom actif)', () => {
      zoomLevel = Math.min(maxZoom, zoomLevel + zoomStep);
      updateTransform();
      updInd();
    });
    const minus = makeBtn('zoomOut', '−', 'Dézoom (− ou E si zoom actif)', () => {
      zoomLevel = Math.max(minZoom, zoomLevel - zoomStep);
      updateTransform();
      updInd();
    });
    const reset = makeBtn('resetZoom', 'R', 'Reset (R si zoom actif)', () => {
      if (zoomLevel > 1) {
        zoomLevel = 1;
        panX = 0;
        panY = 0;
        updateTransform();
        updInd();
      }
    });
    updInd();
    bar.append(plus, minus, reset, indicator);
    return { bar, updInd };
  };
  const { bar, updInd } = buildToolbar();
  const mount = () => {
    const tray = $('[data-a-target="tray-search-input"].tw-combo-input');
    if (!tray || !tray.parentElement) return false;
    const parent = tray.parentElement;
    parent.style.display = 'flex';
    parent.style.alignItems = 'center';
    parent.style.gap = '8px';
    tray.style.flex = '0 1 auto';
    parent.insertBefore(bar, tray.nextSibling);
    return true;
  };
  if (!mount()) {
    const obs = new MutationObserver(() => {
      if (mount()) obs.disconnect();
    });
    obs.observe(document.documentElement, { childList: true, subtree: true });
    setTimeout(() => obs.disconnect(), 15000);
  }
  const isTyping = (t) => {
    if (!t) return false;
    const tag = (t.tagName || '').toLowerCase();
    return t.isContentEditable || ['input', 'textarea', 'select'].includes(tag);
  };
  document.addEventListener('keydown', (e) => {
    if (isTyping(e.target)) return;
    const k = e.key.toLowerCase();
    const move = e.shiftKey ? panStep * 3 : panStep;
    if (zoomLevel > 1) {
      if (k === 'z') {
        panY += move;
        updateTransform();
      } else if (k === 's') {
        panY -= move;
        updateTransform();
      } else if (k === 'q') {
        panX += move;
        updateTransform();
      } else if (k === 'd') {
        panX -= move;
        updateTransform();
      } else if (k === 'a') {
        zoomLevel = Math.min(maxZoom, zoomLevel + zoomStep);
        updateTransform();
        updInd();
      } else if (k === 'e') {
        zoomLevel = Math.max(minZoom, zoomLevel - zoomStep);
        updateTransform();
        updInd();
      } else if (k === 'r') {
        zoomLevel = 1;
        panX = 0;
        panY = 0;
        updateTransform();
        updInd();
      }
    }
    if (k === '+' || k === '=') {
      zoomLevel = Math.min(maxZoom, zoomLevel + zoomStep);
      updateTransform();
      updInd();
    } else if (k === '-') {
      zoomLevel = Math.max(minZoom, zoomLevel - zoomStep);
      updateTransform();
      updInd();
    }
  });
  updateTransform();
})();