Il player migliore di sempre.
// ==UserScript==
// @name AnimeWorld Better Player
// @namespace aw-better-player
// @version 1.4
// @include *animeworld*/*
// @run-at document-end
// @description Il player migliore di sempre.
// @description:it Il player migliore di sempre.
// @license MIT
// @grant none
// ==/UserScript==
(() => {
'use strict';
const SKIP_SECONDS = 85;
const SAVE_INTERVAL_MS = 5000;
const RESUME_THRESHOLD = 10;
const STORAGE_PREFIX = 'aw-resume:';
const STORAGE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
const BTN_RATIO = 0.10;
const BTN_MIN = 28;
const BTN_MAX = 52;
const STOCK_DELAY_MS = 800;
const FS_FLAG_KEY = 'aw-autofs';
const AUTOPLAY_FLAG_KEY = 'aw-autoplay-flag';
const AUTOFS_PREF_KEY = 'aw-autofs-enabled';
const AUTOPLAY_PREF_KEY = 'aw-autoplay-enabled';
const FLAG_TTL = 3 * 60 * 1000;
const ICONS = {
restart: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>`,
skip: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13 3a9 9 0 1 0 9 9h-2a7 7 0 1 1-7-7V3z"/><path d="M13 1v6l4-3z"/><polyline points="13,8 13,13 16,15" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`,
skipBack: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M11 3a9 9 0 1 1-9 9h2a7 7 0 1 0 7-7V3z"/><path d="M11 1v6l-4-3z"/><polyline points="11,8 11,13 8,15" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`,
prev: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect x="5" y="5" width="2.5" height="14" rx="1"/><polygon points="19,5 9,12 19,19"/></svg>`,
next: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><polygon points="5,5 15,12 5,19"/><rect x="16.5" y="5" width="2.5" height="14" rx="1"/></svg>`,
autofs: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><polyline points="3,9 3,3 9,3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><polyline points="21,9 21,3 15,3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><polyline points="3,15 3,21 9,21" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><polyline points="21,15 21,21 15,21" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><text x="12" y="12" text-anchor="middle" dominant-baseline="central" font-size="9" font-weight="600" font-family="system-ui,sans-serif" fill="currentColor" stroke="none">A</text></svg>`,
autoplay: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.8"/><polygon points="10,8 17,12 10,16" fill="currentColor"/></svg>`,
};
const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
function injectStyle() {
if (document.getElementById('aw-skip-style')) return;
const s = document.createElement('style');
s.id = 'aw-skip-style';
s.textContent = `
.aw-btn{display:inline-flex;align-items:center;justify-content:center;padding:0;margin:0;
background:transparent;border:none;color:#fff;cursor:pointer;opacity:.8;
transition:opacity .15s;flex-shrink:0;box-sizing:border-box;
-webkit-tap-highlight-color:transparent;user-select:none}
.aw-btn:hover{opacity:1}
.aw-btn.aw-off{opacity:.25;cursor:default;pointer-events:none}
.aw-btn svg{fill:currentColor;flex-shrink:0;display:block;
filter:drop-shadow(0 0 2px rgba(0,0,0,.85)) drop-shadow(0 1px 3px rgba(0,0,0,.6))}
.aw-btn.aw-active{opacity:1;color:#f5c518}
#aw-toast{
position:absolute;top:50%;left:50%;
transform:translate(-50%,-50%);
background:rgba(0,0,0,.45);color:rgba(255,255,255,.6);
font-weight:600;line-height:1.2;font-family:system-ui,sans-serif;
pointer-events:none;white-space:nowrap;
opacity:1;transition:opacity .4s ease;z-index:999999}
#aw-toast.aw-toast-hide{opacity:0}
.aw-btn{position:relative}
.aw-btn .aw-tip{
display:none;position:absolute;bottom:calc(100% + 18px);left:50%;
transform:translateX(-50%);
background:#fff;color:#000;
font:400 12px/1.3 Arial,Helvetica,sans-serif;
white-space:nowrap;padding:5px 9px;border-radius:3px;
pointer-events:none;z-index:99999;
box-shadow:0 1px 4px rgba(0,0,0,.35)}
.aw-btn .aw-tip::after{
content:'';position:absolute;top:100%;left:50%;
transform:translateX(-50%);
border:5px solid transparent;
border-top-color:#fff}
.aw-btn:hover .aw-tip{display:block}
`;
document.head.appendChild(s);
}
function makeBtn(icon, title) {
const el = document.createElement('div');
el.className = 'aw-btn';
el.setAttribute('role', 'button');
el.setAttribute('tabindex', '0');
el.setAttribute('aria-label', title);
el.innerHTML = icon + `<span class="aw-tip">${title}</span>`;
return el;
}
function setBtnTip(btn, text) {
btn.setAttribute('aria-label', text);
const tip = btn.querySelector('.aw-tip');
if (tip) tip.textContent = text;
}
function applySize(btns) {
const size = clamp(Math.round(document.documentElement.clientHeight * BTN_RATIO), BTN_MIN, BTN_MAX);
const icon = Math.round(size * 0.5);
btns.forEach(b => {
b.style.width = `${size}px`; b.style.height = `${size}px`;
const svg = b.querySelector('svg');
if (svg) { svg.style.width = `${icon}px`; svg.style.height = `${icon}px`; }
});
}
function episodeId() {
const src = document.referrer || location.href;
const m = src.match(/\/play\/([^?#]+)/);
const fallback = src.replace(/https?:\/\/[^/]+/, '').replace(/[?#].*$/, '').slice(0, 200);
return m ? m[1] : (fallback || 'unknown');
}
const key = () => STORAGE_PREFIX + episodeId();
const keyTs = () => key() + ':ts';
const lsGet = k => { try { return localStorage.getItem(k); } catch (_) { return null; } };
const lsSet = (k, v) => { try { localStorage.setItem(k, v); } catch (_) {} };
const lsDel = k => { try { localStorage.removeItem(k); } catch (_) {} };
const savePos = t => { lsSet(key(), String(t)); lsSet(keyTs(), String(Date.now())); };
const loadPos = () => { const v = lsGet(key()); return v !== null ? parseFloat(v) : null; };
const clearPos = () => { lsDel(key()); lsDel(keyTs()); };
function cleanupStorage() {
try {
const now = Date.now();
const dead = [];
const allKeys = Array.from({ length: localStorage.length }, (_, i) => localStorage.key(i));
for (const k of allKeys) {
if (!k?.startsWith(STORAGE_PREFIX)) continue;
if (k.endsWith(':ts')) continue;
const tsRaw = lsGet(k + ':ts');
const ts = tsRaw !== null ? parseFloat(tsRaw) : NaN;
if (isNaN(ts) || now - ts > STORAGE_MAX_AGE) dead.push(k, k + ':ts');
}
dead.forEach(lsDel);
} catch (_) {}
}
let _toastTimer = null;
function formatTime(seconds) {
const s = Math.floor(seconds);
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
const mm = String(m).padStart(2, '0');
const ss = String(sec).padStart(2, '0');
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
}
function applyToastSize(el) {
const wrapper = document.querySelector('.jw-wrapper');
const wrapperH = wrapper ? wrapper.offsetHeight : 0;
const h = wrapperH > 0 ? wrapperH : window.innerHeight;
const fs = Math.round(h * 0.038);
const pad = Math.round(fs * 0.53);
el.style.fontSize = `${fs}px`;
el.style.padding = `${pad}px ${Math.round(pad * 2.1)}px`;
el.style.borderRadius = `${Math.round(fs * 1.2)}px`;
}
function showResumeToast(seconds) {
document.getElementById('aw-toast')?.remove();
if (_toastTimer) { clearTimeout(_toastTimer); _toastTimer = null; }
const parent = document.querySelector('.jw-wrapper') ?? document.body;
const el = document.createElement('div');
el.id = 'aw-toast';
el.textContent = `▶ Ripreso da ${formatTime(seconds)}`;
parent.appendChild(el);
applyToastSize(el);
const onFs = () => { if (document.getElementById('aw-toast')) applyToastSize(el); };
document.addEventListener('fullscreenchange', onFs);
document.addEventListener('webkitfullscreenchange', onFs);
const cleanup = () => {
document.removeEventListener('fullscreenchange', onFs);
document.removeEventListener('webkitfullscreenchange', onFs);
};
_toastTimer = setTimeout(() => {
el.classList.add('aw-toast-hide');
el.addEventListener('transitionend', () => { el.remove(); cleanup(); }, { once: true });
setTimeout(() => { el.remove(); cleanup(); }, 600);
_toastTimer = null;
}, 5000);
}
function parentBtn(val) {
try { return window.parent.document.querySelector(`.prevnext[data-value="${val}"]`); }
catch (_) { return null; }
}
function navEpisode(val, video) {
const el = parentBtn(val);
if (!el) return;
if (video?.currentTime > 5) savePos(video.currentTime);
if (lsGet(AUTOFS_PREF_KEY) === '1') lsSet(FS_FLAG_KEY, String(Date.now()));
if (lsGet(AUTOPLAY_PREF_KEY) === '1') lsSet(AUTOPLAY_FLAG_KEY, String(Date.now()));
el.click();
}
function syncNavState(prev, next) {
[['prev', prev], ['next', next]].forEach(([val, btn]) => {
const has = !!parentBtn(val);
btn.classList.toggle('aw-off', !has);
btn.setAttribute('tabindex', has ? '0' : '-1');
});
}
function tryAltSwitch() {
const isStock = !!document.querySelector('video#video-player[controls]') &&
!document.querySelector('.jw-button-container');
if (!isStock) return false;
try {
const altBtn = window.parent.document.querySelector('#alternative');
if (altBtn) { altBtn.click(); return true; }
} catch (_) {}
return false;
}
function requestFs() {
return new Promise(resolve => {
const isFs = () => !!(document.fullscreenElement || document.webkitFullscreenElement);
if (isFs()) { resolve(true); return; }
function tryClick() {
const jwBtn = document.querySelector('.jw-icon-fullscreen:not(.jw-fullscreen-ima)');
if (!jwBtn || jwBtn.offsetParent === null) return false;
jwBtn.click();
let resolved = false;
const onFsChange = () => { if (!resolved) { resolved = true; resolve(true); } };
document.addEventListener('fullscreenchange', onFsChange, { once: true });
document.addEventListener('webkitfullscreenchange', onFsChange, { once: true });
setTimeout(() => { if (!resolved) { resolved = true; resolve(false); } }, 800);
return true;
}
if (tryClick()) return;
const timeout = setTimeout(() => {
observer.disconnect();
const el = document.documentElement;
const fn = el.requestFullscreen ?? el.webkitRequestFullscreen ?? el.mozRequestFullScreen ?? el.msRequestFullscreen;
fn?.call(el).then(() => resolve(true)).catch(() => resolve(false)) ?? resolve(false);
}, 3000);
const observer = new MutationObserver(() => {
if (tryClick()) {
clearTimeout(timeout);
observer.disconnect();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
});
}
function insertButtons(video, container) {
if (container.querySelector('.aw-btn')) return;
const btnRestart = makeBtn(ICONS.restart, "Ricomincia dall'inizio (R)");
const btnSkip = makeBtn(ICONS.skip, 'Salta opening/ending (O)');
const btnSkipBack = makeBtn(ICONS.skipBack, 'Annulla skip (B)');
const btnPrev = makeBtn(ICONS.prev, 'Episodio precedente (P)');
const btnNext = makeBtn(ICONS.next, 'Episodio successivo (N)');
const btnAutoFs = makeBtn(ICONS.autofs, 'Full screen automatico (S)');
const btnAutoPlay = makeBtn(ICONS.autoplay, 'Autoplay al cambio episodio (A)');
const isAutoFsOn = () => lsGet(AUTOFS_PREF_KEY) === '1';
const isAutoPlayOn = () => lsGet(AUTOPLAY_PREF_KEY) === '1';
const syncAutoFsBtn = () => {
const on = isAutoFsOn();
btnAutoFs.classList.toggle('aw-active', on);
btnAutoFs.setAttribute('aria-pressed', String(on));
setBtnTip(btnAutoFs, `Full screen automatico: ${on ? 'ON' : 'OFF'} (S)`);
};
const syncAutoPlayBtn = () => {
const on = isAutoPlayOn();
btnAutoPlay.classList.toggle('aw-active', on);
btnAutoPlay.setAttribute('aria-pressed', String(on));
setBtnTip(btnAutoPlay, `Autoplay al cambio episodio: ${on ? 'ON' : 'OFF'} (A)`);
};
btnAutoFs.addEventListener('click', () => {
lsSet(AUTOFS_PREF_KEY, isAutoFsOn() ? '0' : '1');
syncAutoFsBtn();
});
btnAutoPlay.addEventListener('click', () => {
lsSet(AUTOPLAY_PREF_KEY, isAutoPlayOn() ? '0' : '1');
syncAutoPlayBtn();
});
syncAutoFsBtn();
syncAutoPlayBtn();
window.addEventListener('storage', e => {
if (e.key === AUTOFS_PREF_KEY) syncAutoFsBtn();
if (e.key === AUTOPLAY_PREF_KEY) syncAutoPlayBtn();
});
const fsFlag = lsGet(FS_FLAG_KEY);
const autoplayFlag = lsGet(AUTOPLAY_FLAG_KEY);
const shouldFs = fsFlag !== null && (Date.now() - parseFloat(fsFlag)) < FLAG_TTL;
const shouldAutoPlay = autoplayFlag !== null && (Date.now() - parseFloat(autoplayFlag)) < FLAG_TTL;
if (fsFlag !== null) lsDel(FS_FLAG_KEY);
if (autoplayFlag !== null) lsDel(AUTOPLAY_FLAG_KEY);
if (shouldFs || shouldAutoPlay) {
const runAutoActions = () => {
if (shouldFs) requestFs();
if (shouldAutoPlay) video.play().catch(() => {});
};
if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
runAutoActions();
} else {
video.addEventListener('canplay', runAutoActions, { once: true });
}
}
btnRestart.addEventListener('click', () => { video.currentTime = 0; clearPos(); });
btnSkipBack.addEventListener('click', () => {
if (!isFinite(video.duration)) return;
const playing = !video.paused && !video.ended;
video.currentTime = Math.max(0, video.currentTime - SKIP_SECONDS);
if (playing) video.play().catch(() => {});
});
btnSkip.addEventListener('click', () => {
if (!isFinite(video.duration)) return;
const playing = !video.paused && !video.ended;
video.currentTime = Math.min(video.duration, video.currentTime + SKIP_SECONDS);
if (playing) video.play().catch(() => {});
});
btnPrev.addEventListener('click', () => navEpisode('prev', video));
btnNext.addEventListener('click', () => navEpisode('next', video));
[btnPrev, btnNext].forEach(btn =>
btn.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); btn.click(); }
})
);
const rewind = container.querySelector('.jw-icon-rewind');
const spacer = container.querySelector('.jw-spacer');
(rewind ?? container).insertAdjacentElement(rewind ? 'afterend' : 'afterbegin', btnRestart);
if (spacer) {
[btnAutoFs, btnAutoPlay, btnSkipBack, btnSkip, btnPrev, btnNext].reduce((ref, btn) => {
ref.insertAdjacentElement('afterend', btn);
return btn;
}, spacer);
} else {
container.append(btnAutoFs, btnAutoPlay, btnSkipBack, btnSkip, btnPrev, btnNext);
}
const allBtns = [btnRestart, btnSkipBack, btnSkip, btnPrev, btnNext, btnAutoFs, btnAutoPlay];
syncNavState(btnPrev, btnNext);
const navSyncTimer = setInterval(() => syncNavState(btnPrev, btnNext), 2000);
window.addEventListener('beforeunload', () => clearInterval(navSyncTimer), { once: true });
applySize(allBtns);
const ro = new ResizeObserver(() => applySize(allBtns));
ro.observe(document.documentElement);
['fullscreenchange', 'webkitfullscreenchange'].forEach(ev =>
document.addEventListener(ev, () => applySize(allBtns))
);
document.addEventListener('keydown', e => {
const tag = document.activeElement?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || document.activeElement?.isContentEditable) return;
const map = { o: btnSkip, b: btnSkipBack, r: btnRestart, p: btnPrev, n: btnNext, s: btnAutoFs, a: btnAutoPlay };
map[e.key.toLowerCase()]?.click();
});
function attemptResume() {
if (!isFinite(video.duration)) return;
const saved = loadPos();
if (!saved || saved < 5) return;
if (video.duration - saved < RESUME_THRESHOLD) { clearPos(); return; }
video.currentTime = saved;
showResumeToast(saved);
}
if (isFinite(video.duration)) attemptResume();
else video.addEventListener('loadedmetadata', attemptResume, { once: true });
let saveTimer = null;
const startSaving = () => {
if (saveTimer) return;
saveTimer = setInterval(() => { if (video.currentTime > 5) savePos(video.currentTime); }, SAVE_INTERVAL_MS);
};
const stopSaving = () => { clearInterval(saveTimer); saveTimer = null; };
video.addEventListener('play', startSaving);
video.addEventListener('pause', stopSaving);
video.addEventListener('ended', () => { stopSaving(); clearPos(); });
window.addEventListener('beforeunload', () => { if (video.currentTime > 5) savePos(video.currentTime); });
}
cleanupStorage();
const seen = new WeakSet();
function tryInit() {
const video = document.querySelector('video.jw-video, .jwplayer video, video');
const container = document.querySelector('.jw-button-container');
if (!video || !container || seen.has(video)) return;
seen.add(video);
injectStyle();
insertButtons(video, container);
}
setTimeout(() => {
if (!tryAltSwitch()) tryInit();
}, STOCK_DELAY_MS);
let rafPending = false;
new MutationObserver(() => {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(() => { rafPending = false; tryInit(); });
}).observe(document.documentElement, { childList: true, subtree: true });
})();