TV+ altyazı büyütücü userjs

ölmedim geri döndüm kaldığımız yerden devam
Süper Üye
Katılım
6 Eki 2019
Mesajlar
744
Çözümler
19
Tepki puanı
155
Ödüller
6
Sosyal
6 HİZMET YILI
TV+ Altyazı Büyütücü (Userscript)

Neden yaptım?


TV+ (tvplus.com.tr) üzerinden yabancı dizi izlerken altyazıların çok küçük kaldığını fark ettim. Player ayarlarında altyazı boyutunu değiştirecek bir seçenek yok. CSS kaynağına baktığımda altyazıların ::cue { font-size: 29px } ile sabitlendiğini gördüm. Özellikle TV'ye cast etmeden büyük ekranda veya biraz uzaktan izleyenler için 29px yetersiz kalıyor. Bu scripti bu sorunu çözmek için yazdım.

Ne yapıyor?

- Altyazı boyutunu 20-80px arası ayarlayabilirsiniz (varsayılan 42px)
- Yazı rengi, kalınlık ve kontur gölge açılıp kapatılabilir
- İsteğe bağlı siyah arkaplan kutusu eklenebilir
- Tüm ayarlar kaydedilir, sayfayı kapatsanız bile kalır
- Klavye kısayolları: Alt+Yukari/Asagi ile boyut, Alt+S ile aç/kapa
- Next.js SPA navigasyonunda çalışmaya devam eder

Capture.PNG


Kurulum

1. Tarayıcınıza Tampermonkey veya Violentmonkey uzantısını kurun
2. Aşağıdaki kodu yeni script olarak ekleyin
3. tvplus.com.tr/izle adresinde herhangi bir içerik açın
4. Sağ altta kırmızı "A" butonuna tıklayıp panelden ayarları yapın

Çalıştığı adresler

  • tvplus.com.tr/izle/*
  • tvplus.com.tr/dizi-izle/*
  • tvplus.com.tr/film-izle/*
  • tvplus.com.tr/canli-izle/*
  • tvplus.com.tr/canli-tv/*

Kaynak Kod

JavaScript:
// ==UserScript==
// @name         TV+ Altyazı Büyütücü
// @namespace    https://tvplus.com.tr/
// @version      2.0
// @description  TV+ altyazılarını büyütür, rengini ve stilini özelleştirir. Klavye kısayolları: Alt+↑/↓ boyut, Alt+S aç/kapa.
// @author       durmuk — https://memoryhackers.org/members/durmuk.1871708/
// @match        https://tvplus.com.tr/izle/*
// @match        https://tvplus.com.tr/dizi-izle/*
// @match        https://tvplus.com.tr/film-izle/*
// @match        https://tvplus.com.tr/canli-izle/*
// @match        https://tvplus.com.tr/canli-tv/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const DEFAULTS = {
    fontSize: 42, color: '#FFFFFF', bgColor: 'rgba(0,0,0,0.65)',
    bgEnabled: false, bold: true, stroke: true, enabled: true,
  };

  let cfg = {
    fontSize:  GM_getValue('tvp_fontSize',  DEFAULTS.fontSize),
    color:     GM_getValue('tvp_color',     DEFAULTS.color),
    bgColor:   GM_getValue('tvp_bgColor',   DEFAULTS.bgColor),
    bgEnabled: GM_getValue('tvp_bgEnabled', DEFAULTS.bgEnabled),
    bold:      GM_getValue('tvp_bold',      DEFAULTS.bold),
    stroke:    GM_getValue('tvp_stroke',    DEFAULTS.stroke),
    enabled:   GM_getValue('tvp_enabled',   DEFAULTS.enabled),
  };

  GM_addStyle(`
#tvp-toggle-btn{position:fixed;bottom:20px;right:20px;z-index:2147483647;width:44px;height:44px;background:linear-gradient(135deg,#e50914,#b0060f);border:none;border-radius:50%;color:#fff;font-size:16px;font-weight:800;font-family:'Manrope','Segoe UI',sans-serif;cursor:pointer;box-shadow:0 4px 18px rgba(229,9,20,.45),0 2px 6px rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;transition:transform .2s,box-shadow .2s;letter-spacing:-.5px}
#tvp-toggle-btn:hover{transform:scale(1.12);box-shadow:0 6px 24px rgba(229,9,20,.6),0 3px 8px rgba(0,0,0,.6)}
#tvp-toggle-btn.tvp-panel-open{display:none}
#tvp-sub-panel{position:fixed;bottom:20px;right:20px;z-index:2147483647;background:rgba(10,12,16,.94);border:1px solid rgba(229,9,20,.25);border-radius:16px;padding:16px 20px 18px;width:280px;font-family:'Manrope','Segoe UI',sans-serif;font-size:13px;color:#e0e0e0;box-shadow:0 12px 48px rgba(0,0,0,.75),0 0 0 1px rgba(229,9,20,.1);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);user-select:none;transition:opacity .25s,transform .25s;transform-origin:bottom right}
#tvp-sub-panel.tvp-hidden{opacity:0;transform:scale(.92);pointer-events:none}
#tvp-sub-panel .tvp-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
#tvp-sub-panel .tvp-header h3{margin:0;font-size:14px;font-weight:700;color:#fff;display:flex;align-items:center;gap:6px}
#tvp-sub-panel .tvp-close-x{background:none;border:none;color:#888;font-size:18px;cursor:pointer;padding:2px 6px;border-radius:6px;transition:color .15s,background .15s}
#tvp-sub-panel .tvp-close-x:hover{color:#fff;background:rgba(255,255,255,.08)}
#tvp-sub-panel .tvp-enable-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;padding:6px 10px;background:rgba(255,255,255,.04);border-radius:8px}
#tvp-sub-panel .tvp-enable-row span{font-size:12px;color:#bbb}
.tvp-switch{position:relative;width:40px;height:22px;cursor:pointer}
.tvp-switch input{opacity:0;width:0;height:0}
.tvp-switch .tvp-slider{position:absolute;inset:0;background:rgba(255,255,255,.15);border-radius:22px;transition:background .2s}
.tvp-switch .tvp-slider::before{content:'';position:absolute;height:16px;width:16px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:transform .2s}
.tvp-switch input:checked+.tvp-slider{background:#e50914}
.tvp-switch input:checked+.tvp-slider::before{transform:translateX(18px)}
#tvp-sub-panel .tvp-row{margin-bottom:10px}
#tvp-sub-panel .tvp-row label{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;color:#aaa;font-size:12px}
#tvp-sub-panel .tvp-val{color:#e50914;font-weight:700;font-size:13px}
#tvp-sub-panel input[type=range]{width:100%;accent-color:#e50914;cursor:pointer;height:4px}
#tvp-sub-panel .tvp-color-input{width:100%;height:28px;cursor:pointer;border-radius:6px;border:1px solid rgba(255,255,255,.12);background:transparent;padding:0}
#tvp-sub-panel .tvp-toggles{display:flex;gap:8px;margin-bottom:10px}
#tvp-sub-panel .tvp-btn{flex:1;padding:6px 0;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04);color:#999;font-size:12px;font-weight:600;font-family:inherit;cursor:pointer;transition:all .15s;text-align:center}
#tvp-sub-panel .tvp-btn.tvp-on{background:rgba(229,9,20,.2);border-color:rgba(229,9,20,.5);color:#ff4d58}
#tvp-sub-panel .tvp-btn:hover{background:rgba(229,9,20,.15);color:#fff}
#tvp-sub-panel .tvp-reset{width:100%;margin-top:6px;padding:6px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:8px;color:#777;font-size:11px;font-family:inherit;cursor:pointer;transition:all .15s}
#tvp-sub-panel .tvp-reset:hover{background:rgba(255,255,255,.1);color:#ddd}
#tvp-sub-panel .tvp-info{margin-top:8px;font-size:10px;color:#555;text-align:center;line-height:1.4}
  `);

  const FALLBACK_SEL = [
    '.player-container [class*="subtitle"]',
    '.player-container [class*="caption"]',
    '.shaka-text-container', '.shaka-text-container span', '.shaka-text-container div',
    '.bmpui-subtitle-overlay', '.bmpui-ui-subtitle-overlay', '.bmpui-ui-subtitle-overlay span',
  ];

  let cueStyleEl = null, fallbackStyleEl = null;

  function buildCueCSS() {
    if (!cfg.enabled) return '';
    const w = cfg.bold ? 'bold' : 'normal';
    const s = cfg.stroke ? '1px 1px 4px rgba(0,0,0,1),-1px -1px 4px rgba(0,0,0,1),2px 2px 8px rgba(0,0,0,.9),0 0 3px rgba(0,0,0,.8)' : 'none';
    const bg = cfg.bgEnabled ? cfg.bgColor : 'transparent';
    const rule = `font-size:${cfg.fontSize}px!important;color:${cfg.color}!important;font-weight:${w}!important;text-shadow:${s}!important;background-color:${bg}!important;line-height:1.4!important;font-family:'Manrope','Segoe UI',Arial,sans-serif!important;outline:none!important`;
    return `::cue{${rule}} video::cue{${rule}}`;
  }

  function buildFallbackCSS() {
    if (!cfg.enabled) return '';
    const w = cfg.bold ? '700' : '400';
    const s = cfg.stroke ? '1px 1px 4px rgba(0,0,0,1),-1px -1px 4px rgba(0,0,0,1),2px 2px 8px rgba(0,0,0,.9)' : 'none';
    return FALLBACK_SEL.map(sel =>
      `${sel},${sel}>div,${sel}>span{font-size:${cfg.fontSize}px!important;font-weight:${w}!important;color:${cfg.color}!important;text-shadow:${s}!important;line-height:1.4!important}`
    ).join('\n');
  }

  function applyStyles() {
    if (!cueStyleEl) { cueStyleEl = document.createElement('style'); cueStyleEl.id = 'tvp-cue'; document.head.appendChild(cueStyleEl); }
    if (!fallbackStyleEl) { fallbackStyleEl = document.createElement('style'); fallbackStyleEl.id = 'tvp-fb'; document.head.appendChild(fallbackStyleEl); }
    cueStyleEl.textContent = buildCueCSS();
    fallbackStyleEl.textContent = buildFallbackCSS();
  }

  function saveAll() {
    GM_setValue('tvp_fontSize', cfg.fontSize);
    GM_setValue('tvp_color', cfg.color);
    GM_setValue('tvp_bgColor', cfg.bgColor);
    GM_setValue('tvp_bgEnabled', cfg.bgEnabled);
    GM_setValue('tvp_bold', cfg.bold);
    GM_setValue('tvp_stroke', cfg.stroke);
    GM_setValue('tvp_enabled', cfg.enabled);
  }

  function buildPanel() {
    const toggleBtn = document.createElement('button');
    toggleBtn.id = 'tvp-toggle-btn';
    toggleBtn.title = 'TV+ Altyazı Ayarları';
    toggleBtn.textContent = 'A↑';
    document.body.appendChild(toggleBtn);

    const panel = document.createElement('div');
    panel.id = 'tvp-sub-panel';
    panel.classList.add('tvp-hidden');
    panel.innerHTML = `
      <div class="tvp-header">
        <h3>Altyazı Büyütücü</h3>
        <button class="tvp-close-x" id="tvp-close" title="Kapat">✕</button>
      </div>
      <div class="tvp-enable-row">
        <span>Büyütücü Aktif</span>
        <label class="tvp-switch"><input type="checkbox" id="tvp-enabled" ${cfg.enabled?'checked':''}><span class="tvp-slider"></span></label>
      </div>
      <div class="tvp-row">
        <label>Yazı Boyutu <span class="tvp-val" id="tvp-fs-val">${cfg.fontSize}px</span></label>
        <input type="range" id="tvp-fs" min="20" max="80" step="1" value="${cfg.fontSize}">
      </div>
      <div class="tvp-row">
        <label>Yazı Rengi</label>
        <input type="color" id="tvp-color" class="tvp-color-input" value="${cfg.color}">
      </div>
      <div class="tvp-toggles">
        <button class="tvp-btn ${cfg.bold?'tvp-on':''}" id="tvp-bold">B Kalın</button>
        <button class="tvp-btn ${cfg.stroke?'tvp-on':''}" id="tvp-stroke">Kontur</button>
      </div>
      <div class="tvp-enable-row" style="margin-bottom:8px">
        <span>Arkaplan Kutusu</span>
        <label class="tvp-switch"><input type="checkbox" id="tvp-bg-enabled" ${cfg.bgEnabled?'checked':''}><span class="tvp-slider"></span></label>
      </div>
      <button class="tvp-reset" id="tvp-reset">Varsayılana Sıfırla</button>
      <div class="tvp-info">v2.0 | Alt+Yukari/Asagi boyut | Alt+S aç/kapa</div>`;
    document.body.appendChild(panel);

    toggleBtn.addEventListener('click', () => { panel.classList.remove('tvp-hidden'); toggleBtn.classList.add('tvp-panel-open'); });
    panel.querySelector('#tvp-close').addEventListener('click', () => { panel.classList.add('tvp-hidden'); toggleBtn.classList.remove('tvp-panel-open'); });

    panel.querySelector('#tvp-enabled').addEventListener('change', e => { cfg.enabled = e.target.checked; applyStyles(); saveAll(); });

    const fsSlider = panel.querySelector('#tvp-fs'), fsVal = panel.querySelector('#tvp-fs-val');
    fsSlider.addEventListener('input', () => { cfg.fontSize = +fsSlider.value; fsVal.textContent = cfg.fontSize+'px'; applyStyles(); saveAll(); });

    panel.querySelector('#tvp-color').addEventListener('input', e => { cfg.color = e.target.value; applyStyles(); saveAll(); });

    const boldBtn = panel.querySelector('#tvp-bold');
    boldBtn.addEventListener('click', () => { cfg.bold = !cfg.bold; boldBtn.classList.toggle('tvp-on', cfg.bold); applyStyles(); saveAll(); });

    const strokeBtn = panel.querySelector('#tvp-stroke');
    strokeBtn.addEventListener('click', () => { cfg.stroke = !cfg.stroke; strokeBtn.classList.toggle('tvp-on', cfg.stroke); applyStyles(); saveAll(); });

    panel.querySelector('#tvp-bg-enabled').addEventListener('change', e => { cfg.bgEnabled = e.target.checked; applyStyles(); saveAll(); });

    panel.querySelector('#tvp-reset').addEventListener('click', () => {
      Object.assign(cfg, {...DEFAULTS});
      fsSlider.value = cfg.fontSize; fsVal.textContent = cfg.fontSize+'px';
      panel.querySelector('#tvp-color').value = cfg.color;
      panel.querySelector('#tvp-enabled').checked = cfg.enabled;
      panel.querySelector('#tvp-bg-enabled').checked = cfg.bgEnabled;
      boldBtn.classList.toggle('tvp-on', cfg.bold);
      strokeBtn.classList.toggle('tvp-on', cfg.stroke);
      applyStyles(); saveAll();
    });

    document.addEventListener('keydown', e => {
      if (e.key === 'Escape' && !panel.classList.contains('tvp-hidden')) {
        panel.classList.add('tvp-hidden'); toggleBtn.classList.remove('tvp-panel-open');
      }
    });
  }

  function startObserver() {
    const obs = new MutationObserver(muts => { for (const m of muts) if (m.addedNodes.length) { applyStyles(); break; } });
    const target = document.querySelector('.player-container') || document.querySelector('.player-page') || document.body;
    obs.observe(target, { childList: true, subtree: true });
    if (!document.querySelector('.player-container')) {
      const bObs = new MutationObserver(() => {
        const pc = document.querySelector('.player-container');
        if (pc) { obs.observe(pc, { childList: true, subtree: true }); applyStyles(); bObs.disconnect(); }
      });
      bObs.observe(document.body, { childList: true, subtree: true });
    }
  }

  function watchTextTracks() {
    const hook = v => {
      if (v._tvpH) return; v._tvpH = true;
      v.textTracks.addEventListener('addtrack', () => setTimeout(applyStyles, 200));
      v.textTracks.addEventListener('change', () => setTimeout(applyStyles, 100));
    };
    document.querySelectorAll('video').forEach(hook);
    new MutationObserver(() => document.querySelectorAll('video').forEach(hook)).observe(document.body, { childList: true, subtree: true });
  }

  function watchNavigation() {
    let lastUrl = location.href;
    const check = () => { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(applyStyles, 800); setTimeout(applyStyles, 2000); } };
    const origPush = history.pushState, origReplace = history.replaceState;
    history.pushState = function () { origPush.apply(this, arguments); setTimeout(check, 100); };
    history.replaceState = function () { origReplace.apply(this, arguments); setTimeout(check, 100); };
    window.addEventListener('popstate', () => setTimeout(check, 100));
    setInterval(check, 1000);
  }

  let toastTimer = null;
  function showToast(msg) {
    let t = document.getElementById('tvp-toast');
    if (!t) {
      t = document.createElement('div'); t.id = 'tvp-toast';
      t.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);z-index:2147483647;background:rgba(10,12,16,.92);color:#fff;padding:10px 24px;border-radius:30px;font-family:Manrope,Segoe UI,sans-serif;font-size:14px;font-weight:600;box-shadow:0 4px 20px rgba(0,0,0,.6);border:1px solid rgba(229,9,20,.3);transition:opacity .3s;pointer-events:none';
      document.body.appendChild(t);
    }
    t.textContent = msg; t.style.opacity = '1';
    clearTimeout(toastTimer); toastTimer = setTimeout(() => t.style.opacity = '0', 1500);
  }

  function updatePanelUI() {
    const s = document.querySelector('#tvp-fs'), v = document.querySelector('#tvp-fs-val'), e = document.querySelector('#tvp-enabled');
    if (s) s.value = cfg.fontSize; if (v) v.textContent = cfg.fontSize+'px'; if (e) e.checked = cfg.enabled;
  }

  function setupShortcuts() {
    document.addEventListener('keydown', e => {
      if (!document.querySelector('.player-container')) return;
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
      if (e.altKey && e.key === 'ArrowUp')   { e.preventDefault(); cfg.fontSize = Math.min(80, cfg.fontSize+2); applyStyles(); saveAll(); updatePanelUI(); showToast(`Altyazı: ${cfg.fontSize}px`); }
      if (e.altKey && e.key === 'ArrowDown') { e.preventDefault(); cfg.fontSize = Math.max(20, cfg.fontSize-2); applyStyles(); saveAll(); updatePanelUI(); showToast(`Altyazı: ${cfg.fontSize}px`); }
      if (e.altKey && (e.key === 's' || e.key === 'S')) { e.preventDefault(); cfg.enabled = !cfg.enabled; applyStyles(); saveAll(); updatePanelUI(); showToast(cfg.enabled ? 'Büyütücü: Açık' : 'Büyütücü: Kapalı'); }
    });
  }

  function init() {
    buildPanel();
    applyStyles();
    startObserver();
    watchTextTracks();
    watchNavigation();
    setupShortcuts();
    setInterval(applyStyles, 3000);
  }

  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();
})();

Teknik detay

TV+ player'ı native HTML5 video elementi ve WebVTT track kullanıyor. Altyazılar ::cue pseudo-element ile render ediliyor ve site CSS'inde 29px olarak sabitlenmiş. Bu script ::cue kuralını override ederek boyutu ve stili değiştiriyor. Site Next.js tabanlı SPA olduğu için sayfa geçişlerinde history API hook'ları ve MutationObserver ile altyazı stilinin korunması sağlanıyor.

Bilinen durum

TV+ player altyapısını değiştirirse selektörlerin güncellenmesi gerekebilir. Sorun yaşarsanız konuya yazın.

Tampermonkey / Violentmonkey gerektirir. MIT lisanslıdır.
 

Ekli dosyalar

  • Capture.PNG
    Capture.PNG
    10.7 KB · Görüntüleme: 7
Üst