ölmedim geri döndüm kaldığımız yerden devam
Süper Üye
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
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
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
Kaynak Kod
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.
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
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.