// ==UserScript==
// @name Stream Watch Timer Plus v8.1
// @namespace http://tampermonkey.net/
// @version 8.1
// @description Enhanced Twitch UI: timers, ad muting, filters, dropdown settings, QoL features with fixed dropdown behavior
// @author Void
// @match https://www.twitch.tv/*
// @grant none
// @license All rights reserved
// ==/UserScript==
(function(){
'use strict';
let sessionSec = 0, totalSec = 0, timer = null;
const channel = window.location.pathname.split('/')[1]?.toLowerCase() || 'unknown';
let dropsEnabled = false;
let timerEl, resetBtn, settingsBtn, dropdownBtn, dropdownList, dropdownOptions = [];
let dropdownOpen = false;
const posMap = { 'TL':'top-left','TM':'top-middle','TR':'top-right', 'BL':'bottom-left','BM':'bottom-middle','BR':'bottom-right' };
const posStyles = {
'top-left': {top:'10px', left:'10px'},
'top-middle': {top:'10px', left:'50%', transform:'translateX(-50%)'},
'top-right': {top:'10px', right:'10px'},
'bottom-left': {bottom:'10px', left:'10px'},
'bottom-middle': {bottom:'10px', left:'50%', transform:'translateX(-50%)'},
'bottom-right': {bottom:'10px', right:'10px'}
};
function key(suf) { return `swt_${channel}_${suf}`; }
function loadTotal() { return parseInt(localStorage.getItem(key('tot')) || '0', 10); }
function saveTotal() { localStorage.setItem(key('tot'), totalSec); }
function loadPos() {
const pos = localStorage.getItem(key('pos'));
return (pos && posMap[pos]) ? pos : 'BR';
}
function savePos(val) {
if(val && posMap[val]) localStorage.setItem(key('pos'), val);
}
function fmt(ts) {
const h = Math.floor(ts / 3600), m = Math.floor((ts % 3600) / 60), s = ts % 60;
return h ? `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}` : `${m}:${String(s).padStart(2,'0')}`;
}
function detectDrops() {
dropsEnabled = Array.from(document.querySelectorAll('[data-a-target="stream-tag"]'))
.some(el => /drops enabled/i.test(el.textContent));
}
function autoClaim() {
const btn = document.querySelector('[data-a-target="claim-drop-button"]');
if(btn) btn.click();
}
function muteAds(v) {
const selectors = ['[data-test-selector="ad-banner"]','.player-ad-overlay','[class*="ad-label"]','.sponsored-pill'];
v.muted = selectors.some(sel => document.querySelector(sel));
}
function removeBanners() {
document.querySelectorAll('[data-test-selector="ad-banner"],.top-nav__prime,.video-ad__container,.player-ad-overlay').forEach(e=>e.remove());
}
let lastSidebarFilter = 0;
function filterSidebar() {
const now = performance.now();
if(now - lastSidebarFilter < 1500) return;
lastSidebarFilter = now;
document.querySelectorAll('a[data-a-target="side-nav-card"]').forEach(card => {
const vt = card.querySelector('[data-a-target="side-nav-viewer-count"]')?.textContent?.toLowerCase();
if(!vt) return;
let count;
if(vt.includes('k')) {
const v = vt.replace(/,/g,'.').match(/[\d.]+/);
count = v ? parseFloat(v[0]) * 1000 : 0;
} else {
const v = vt.match(/\d+/);
count = v ? parseInt(v[0],10) : 0;
}
card.style.display = count > 20 ? 'none' : '';
});
}
function autoTheatre() {
const btn = document.querySelector('[data-a-target="player-theatre-mode-button"]');
if(btn && !document.body.classList.contains('theatre-mode')) btn.click();
}
function showFollowerAge() {
const fBtnParent = document.querySelector('[data-a-target="follow-button"]')?.parentElement;
if(!fBtnParent || fBtnParent.querySelector('.fa-age')) return;
fetch(`https://api.ivr.fi/v2/twitch/user/followage/${channel}`)
.then(res => res.json())
.then(data => {
if(data && data.followageHuman){
const span = document.createElement('span');
span.className = 'fa-age';
span.textContent = ` | Following for: ${data.followageHuman}`;
span.style.fontSize = '12px';
span.style.color = '#ccc';
fBtnParent.appendChild(span);
}
}).catch(() => {});
}
function updateTimerText() {
if(!timerEl) return;
let txt = `⏱ Total: ${fmt(totalSec)} | Session: ${fmt(sessionSec)}`;
if(dropsEnabled) {
const remain = Math.max(0, 900 - sessionSec);
txt += ` | 🎁 Drop in ${Math.ceil(remain / 60)}m`;
}
const span = timerEl.querySelector('.swt-text');
if(span) span.textContent = txt;
}
function applyPosition(code) {
if(!timerEl) return;
const style = posStyles[posMap[code]];
timerEl.style.top = timerEl.style.bottom = timerEl.style.left = timerEl.style.right = timerEl.style.transform = '';
Object.assign(timerEl.style, style);
}
function toggleDropdown(forceOpen) {
dropdownOpen = forceOpen !== undefined ? forceOpen : !dropdownOpen;
dropdownList.style.display = dropdownOpen ? 'block' : 'none';
dropdownBtn.setAttribute('aria-expanded', dropdownOpen);
if(dropdownOpen){
// Highlight selected option
const currentVal = loadPos();
dropdownOptions.forEach(opt => opt.classList.toggle('selected', opt.dataset.value === currentVal));
dropdownList.focus();
}
}
function dropdownKeyHandler(e) {
if(!dropdownOpen) return;
const selected = dropdownList.querySelector('.selected');
if(e.key === 'ArrowDown' || e.key === 'Tab'){
e.preventDefault();
let next = selected?.nextElementSibling || dropdownList.firstElementChild;
if(selected) selected.classList.remove('selected');
next.classList.add('selected');
next.focus();
} else if(e.key === 'ArrowUp'){
e.preventDefault();
let prev = selected?.previousElementSibling || dropdownList.lastElementChild;
if(selected) selected.classList.remove('selected');
prev.classList.add('selected');
prev.focus();
} else if(e.key === 'Enter'){
e.preventDefault();
if(selected){
const val = selected.dataset.value;
savePos(val);
applyPosition(val);
dropdownBtn.textContent = val;
toggleDropdown(false);
dropdownBtn.focus();
}
} else if(e.key === 'Escape'){
e.preventDefault();
toggleDropdown(false);
dropdownBtn.focus();
}
}
function createDropdown() {
const container = document.createElement('div');
container.style.position = 'relative';
container.style.display = 'inline-block';
container.style.marginLeft = '6px';
dropdownBtn = document.createElement('button');
dropdownBtn.type = 'button';
dropdownBtn.textContent = loadPos();
dropdownBtn.setAttribute('aria-haspopup', 'listbox');
dropdownBtn.setAttribute('aria-expanded', 'false');
Object.assign(dropdownBtn.style, {
background: '#222',
color: '#eee',
border: '1px solid #555',
borderRadius: '4px',
padding: '3px 8px',
cursor: 'pointer',
fontSize: '12px',
minWidth: '50px',
userSelect: 'none'
});
dropdownBtn.addEventListener('click', e => {
e.stopPropagation();
toggleDropdown();
});
dropdownBtn.addEventListener('keydown', e => {
if(['ArrowDown','ArrowUp','Enter',' '].includes(e.key)){
e.preventDefault();
toggleDropdown(true);
dropdownList.focus();
}
});
dropdownList = document.createElement('ul');
dropdownList.tabIndex = -1;
dropdownList.setAttribute('role', 'listbox');
Object.assign(dropdownList.style, {
position: 'absolute',
top: '100%',
left: '0',
background: '#222',
border: '1px solid #555',
borderRadius: '4px',
marginTop: '2px',
padding: '2px 0',
listStyle: 'none',
width: '60px',
maxHeight: '160px',
overflowY: 'auto',
boxShadow: '0 2px 6px rgba(0,0,0,0.5)',
display: 'none',
zIndex: '10000',
userSelect: 'none'
});
dropdownList.addEventListener('keydown', dropdownKeyHandler);
dropdownList.addEventListener('click', e => {
if(e.target.tagName === 'LI'){
const val = e.target.dataset.value;
if(val){
savePos(val);
applyPosition(val);
dropdownBtn.textContent = val;
toggleDropdown(false);
dropdownBtn.focus();
}
}
});
// Prevent dropdown from closing on clicks inside
dropdownList.addEventListener('mousedown', e => e.preventDefault());
dropdownOptions = [];
Object.entries(posMap).forEach(([code]) => {
const li = document.createElement('li');
li.textContent = code;
li.dataset.value = code;
li.setAttribute('role', 'option');
li.tabIndex = -1;
Object.assign(li.style, {
padding: '4px 10px',
cursor: 'pointer',
color: '#eee',
fontSize: '12px',
userSelect: 'none'
});
li.addEventListener('mouseenter', () => {
dropdownOptions.forEach(o => o.classList.remove('selected'));
li.classList.add('selected');
li.focus();
});
li.addEventListener('mouseleave', () => {
li.classList.remove('selected');
});
dropdownList.appendChild(li);
dropdownOptions.push(li);
});
container.appendChild(dropdownBtn);
container.appendChild(dropdownList);
return container;
}
// Close dropdown if clicking outside
document.addEventListener('click', () => {
if(dropdownOpen) toggleDropdown(false);
});
function createOverlay() {
timerEl = document.createElement('div');
timerEl.id = 'watch-time-overlay';
Object.assign(timerEl.style, {
position:'absolute', background:'rgba(0,0,0,0.85)', color:'#fff',
padding:'8px', borderRadius:'8px', fontSize:'14px', fontFamily:'Segoe UI',
fontWeight:'600', zIndex:'9999', pointerEvents:'auto', display:'flex',
alignItems:'center', gap:'8px', whiteSpace:'nowrap', textShadow:'0 0 4px #000',
userSelect: 'none',
minWidth: '220px',
boxShadow: '0 0 8px rgba(0,0,0,0.7)'
});
const span = document.createElement('span');
span.className = 'swt-text';
span.style.flex = '1';
timerEl.appendChild(span);
resetBtn = document.createElement('button');
resetBtn.textContent = 'Reset';
Object.assign(resetBtn.style,{
cursor:'pointer',
marginLeft:'4px',
fontSize:'12px',
background:'#333',
color:'#eee',
border:'1px solid #555',
borderRadius:'4px',
padding:'2px 8px',
userSelect:'none'
});
resetBtn.onmouseenter = () => resetBtn.style.background = '#444';
resetBtn.onmouseleave = () => resetBtn.style.background = '#333';
resetBtn.onclick = () => {
if(confirm('Reset total and session time?')){
totalSec = sessionSec = 0;
saveTotal();
updateTimerText();
}
};
timerEl.appendChild(resetBtn);
settingsBtn = document.createElement('button');
settingsBtn.textContent = '⚙️';
Object.assign(settingsBtn.style,{
cursor:'pointer',
fontSize:'16px',
marginLeft:'4px',
background:'transparent',
border:'none',
color:'#eee',
userSelect:'none'
});
settingsBtn.onmouseenter = () => settingsBtn.style.color = '#ddd';
settingsBtn.onmouseleave = () => settingsBtn.style.color = '#eee';
settingsBtn.onclick = e => {
e.stopPropagation();
toggleDropdown();
};
timerEl.appendChild(settingsBtn);
const dropdownContainer = createDropdown();
dropdownContainer.style.marginLeft = '8px';
timerEl.appendChild(dropdownContainer);
applyPosition(loadPos());
return timerEl;
}
function startUI(video, container){
if(timer) return;
totalSec = loadTotal();
sessionSec = 0;
detectDrops();
container.appendChild(createOverlay());
updateTimerText();
autoTheatre();
showFollowerAge();
timer = setInterval(() => {
if(video.readyState >= 2 && !video.paused && !video.ended){
totalSec++;
sessionSec++;
updateTimerText();
if(totalSec % 5 === 0) saveTotal();
}
autoClaim();
muteAds(video);
removeBanners();
}, 1000);
setInterval(filterSidebar, 1500);
}
function initWatch(){
new MutationObserver(() => {
const v = document.querySelector('video');
const c = document.querySelector('.video-player__container, .video-player__overlay');
const o = document.getElementById('watch-time-overlay');
if(v && c && !o){
startUI(v,c);
}
}).observe(document.body, {childList:true, subtree:true});
}
window.addEventListener('load', () => setTimeout(initWatch, 1000));
})();