Il player migliore di sempre — riscritto da zero.
// ==UserScript==
// @name AnimeWorld Better Player
// @namespace aw-better-player
// @version 2.0.0
// @match *://www.animeworld.ac/play/*
// @run-at document-start
// @description Il player migliore di sempre — riscritto da zero.
// @description:it Il player migliore di sempre — riscritto da zero.
// @license MIT
// @grant none
// ==/UserScript==
(() => {
'use strict';
// ── Costanti ──────────────────────────────────────────────────────────────
const HIDE_DELAY_MS = 3000;
const SKIP_SECONDS = 85;
const SAVE_INTERVAL_MS = 5000;
const RESUME_MIN_POS = 5;
const RESUME_END_GAP = 10;
const RESUME_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
const SCRIPT_VERSION = '2.0.0';
const KEY_VOL = 'aw-np-vol';
const KEY_MUTE = 'aw-np-muted';
const KEY_GLOBAL = 'aw-np-global';
const KEY_RESUME_ENABLE = 'aw-np-resume-enabled';
const KEY_RESUME_PFX = 'aw-np-resume:';
const KEY_SEEK_SECS = 'aw-np-seek-secs';
const KEY_AUTOEP_ENABLE = 'aw-np-autoep-enabled';
const KEY_AUTOEP_PFX = 'aw-np-autoep:';
const KEY_AUTOPLAY_ENABLE = 'aw-np-autoplay-enabled';
const KEY_COLOR = 'aw-np-color';
const KEY_COLOR_GLOBAL = 'aw-np-color-global';
const KEY_ICON_COLOR = 'aw-np-icon-color';
const KEY_SPEED = 'aw-np-speed';
const PALETTE = [
{ name: 'Bianco', hex: '#ffffff' },
{ name: 'Rosso', hex: '#f44336' },
{ name: 'Arancio', hex: '#ff9800' },
{ name: 'Giallo', hex: '#ffeb3b' },
{ name: 'Verde', hex: '#4caf50' },
{ name: 'Ciano', hex: '#00bcd4' },
{ name: 'Azzurro', hex: '#42a5f5' },
{ name: 'Blu', hex: '#1565c0' },
{ name: 'Viola', hex: '#9c27b0' },
{ name: 'Rosa', hex: '#e91e8c' },
];
const SEEK_DEFAULT = 5; const SEEK_MIN = 5; const SEEK_MAX = 30; const SEEK_STEP = 5;
const SPEED_DEFAULT = 1; const SPEED_MIN = 0.25; const SPEED_MAX = 3; const SPEED_STEP = 0.25;
// ── Blocca playerServersAndDownloads.js ───────────────────────────────────
const _srcDesc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src');
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
configurable: true, enumerable: true,
get() { return _srcDesc.get.call(this); },
set(val) {
if (typeof val === 'string' && val.includes('playerServersAndDownloads')) return;
_srcDesc.set.call(this, val);
}
});
// ── Storage helpers ───────────────────────────────────────────────────────
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 {} };
// ── Impostazioni ──────────────────────────────────────────────────────────
const isGlobalOn = () => lsGet(KEY_GLOBAL) !== '0';
const pKey = k => isGlobalOn() ? k : k + ':' + (window.animeIdentifier || 'unknown');
const isResumeOn = () => lsGet(pKey(KEY_RESUME_ENABLE)) !== '0';
const isAutoEpOn = () => lsGet(pKey(KEY_AUTOEP_ENABLE)) === '1';
const isAutoPlayOn = () => lsGet(pKey(KEY_AUTOPLAY_ENABLE)) === '1';
const isColorGlobalOn = () => lsGet(KEY_COLOR_GLOBAL) !== '0';
const isIconColorOn = () => lsGet(KEY_ICON_COLOR) === '1';
const colorKey = () => isColorGlobalOn() ? KEY_COLOR : KEY_COLOR + ':' + (window.animeIdentifier || 'unknown');
const loadColor = () => lsGet(colorKey()) || '#ffffff';
const loadSeekSecs = () => { const v = parseInt(lsGet(pKey(KEY_SEEK_SECS)) ?? String(SEEK_DEFAULT), 10); return isNaN(v) ? SEEK_DEFAULT : Math.max(SEEK_MIN, Math.min(SEEK_MAX, v)); };
const loadSpeed = () => { const v = parseFloat(lsGet(pKey(KEY_SPEED)) ?? String(SPEED_DEFAULT)); return isNaN(v) ? SPEED_DEFAULT : Math.max(SPEED_MIN, Math.min(SPEED_MAX, v)); };
const fmtSpeed = v => v.toFixed(2) + 'x';
// ── Colore ────────────────────────────────────────────────────────────────
function hexToRgba(hex, a) {
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return `rgba(255,255,255,${a})`;
const r = parseInt(hex.slice(1,3),16), g = parseInt(hex.slice(3,5),16), b = parseInt(hex.slice(5,7),16);
return `rgba(${r},${g},${b},${a})`;
}
function applyColor(hex, wrap, dotEl) {
if (wrap) {
wrap.style.setProperty('--np-accent', hex);
wrap.style.setProperty('--np-accent-dim', hexToRgba(hex, 0.3));
wrap.style.setProperty('--np-accent-70', hexToRgba(hex, 0.7));
wrap.style.setProperty('--np-accent-60', hexToRgba(hex, 0.6));
}
if (dotEl) dotEl.style.background = hex;
}
// ── Episodio automatico ───────────────────────────────────────────────────
const animeId = () => window.animeIdentifier || '';
const saveLastEpisode = t => { if (animeId()) lsSet(KEY_AUTOEP_PFX + animeId(), t); };
const loadLastEpisode = () => animeId() ? lsGet(KEY_AUTOEP_PFX + animeId()) : null;
// ── Volume ────────────────────────────────────────────────────────────────
function loadVol() {
const v = parseFloat(lsGet(KEY_VOL) ?? '1');
return { vol: isNaN(v) ? 1 : Math.max(0, Math.min(1, v)), muted: lsGet(KEY_MUTE) === 'true' };
}
const saveVol = (vol, muted) => { lsSet(KEY_VOL, String(vol)); lsSet(KEY_MUTE, String(muted)); };
// ── Resume ────────────────────────────────────────────────────────────────
// _activeToken: token dell'episodio attualmente caricato/in riproduzione.
// È il namespace per leggere e scrivere la posizione di resume.
let _activeToken = '';
// _stopSavingFn: ferma il timer di salvataggio dall'esterno di buildPlayer.
let _stopSavingFn = null;
const resumeKey = () => KEY_RESUME_PFX + (_activeToken || location.pathname);
const resumeTs = () => resumeKey() + ':ts';
function saveResumePos(t) {
if (!isResumeOn() || !isFinite(t) || t <= RESUME_MIN_POS) return;
lsSet(resumeKey(), String(t));
lsSet(resumeTs(), String(Date.now()));
}
function clearResumePos() { lsDel(resumeKey()); lsDel(resumeTs()); }
function cleanupResumeStorage() {
try {
const now = Date.now();
const keys = Array.from({ length: localStorage.length }, (_, i) => localStorage.key(i));
keys.forEach(k => {
if (!k?.startsWith(KEY_RESUME_PFX) || k.endsWith(':ts')) return;
const ts = parseFloat(lsGet(k + ':ts') ?? '');
if (isNaN(ts) || now - ts > RESUME_MAX_AGE) { lsDel(k); lsDel(k + ':ts'); }
});
} catch {}
}
// ── Utilities ─────────────────────────────────────────────────────────────
function fmt(s) {
const t = Math.floor(s || 0), h = Math.floor(t / 3600), m = Math.floor((t % 3600) / 60), sec = t % 60;
return h > 0 ? `${h}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}` : `${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`;
}
function mk(tag, id) { const e = document.createElement(tag); if (id) e.id = id; return e; }
function mkBtn(id, html, tip) {
const b = mk('button'); b.className = 'np-btn'; b.id = id; b.innerHTML = html; b.tabIndex = -1;
if (tip) { const t = document.createElement('span'); t.className = 'np-tip'; t.textContent = tip; b.appendChild(t); }
return b;
}
function mkIcon(btn, html) { const s = document.createElement('span'); s.className = 'np-icon'; s.innerHTML = html; btn.prepend(s); return s; }
function setIcon(el, html) { if (el) el.innerHTML = html; }
function setTip(btn, text) { const t = btn.querySelector('.np-tip'); if (t) t.textContent = text; }
function mkRowTip(text) { const t = document.createElement('span'); t.className = 'np-row-tip'; t.textContent = text; return t; }
function mkSwitch(checked) {
const label = document.createElement('label'); label.className = 'np-switch';
const input = document.createElement('input'); input.type = 'checkbox'; input.checked = checked;
const track = document.createElement('span'); track.className = 'np-switch-track';
const thumb = document.createElement('span'); thumb.className = 'np-switch-thumb';
label.append(input, track, thumb); return { label, input };
}
function getAdjacentEpisode(dir) {
const all = Array.from(document.querySelectorAll('.episode a'));
const idx = all.findIndex(a => a.classList.contains('active'));
if (idx === -1) return null;
return dir === 'next' ? (all[idx + 1] ?? null) : (all[idx - 1] ?? null);
}
function getUrlForToken(token) {
return fetch(`/api/episode/serverPlayerAnimeWorld?alt=1&id=${token}`, { credentials: 'same-origin' })
.then(r => { if (!r.ok) throw new Error(); return r.text(); })
.then(html => { const m = html.match(/file:\s*["']([^"']+)/); return m ? m[1] : null; })
.catch(() => null);
}
// ── Stili ─────────────────────────────────────────────────────────────────
function injectStyle() {
if (document.getElementById('aw-np-style')) return;
const s = document.createElement('style'); s.id = 'aw-np-style';
s.textContent = `
#player { background:#000; }
*, *::before, *::after { box-sizing:border-box; }
*:focus { outline:none !important; }
button { -webkit-tap-highlight-color:transparent; }
#aw-np {
position:relative;width:100%;height:100%;background:#000;
display:flex;flex-direction:column;overflow:hidden;
font-family:'Google Sans',Roboto,'Helvetica Neue',sans-serif;
user-select:none;
}
#aw-np-video { flex:1;width:100%;min-height:0;display:block;background:#000;cursor:none; }
#aw-np.ui #aw-np-video { cursor:pointer; }
#aw-np-gradient { position:absolute;bottom:0;left:0;right:0;height:130px;background:linear-gradient(to top,rgba(0,0,0,.88),transparent);pointer-events:none;opacity:0;transition:opacity .3s; }
#aw-np.ui #aw-np-gradient { opacity:1; }
#aw-np-gradient-top { position:absolute;top:0;left:0;right:0;height:130px;background:linear-gradient(to bottom,rgba(0,0,0,.88),transparent);pointer-events:none;opacity:0;transition:opacity .3s; }
#aw-np.ui #aw-np-gradient-top { opacity:1; }
#aw-np-top { position:absolute;top:0;left:0;right:0;height:clamp(64px,8vh,90px);display:flex;align-items:flex-start;justify-content:space-between;padding:clamp(12px,1.8vh,18px) 16px;opacity:0;transition:opacity .3s;pointer-events:none; }
#aw-np.ui #aw-np-top { opacity:1;pointer-events:all; }
#aw-np-top-left { display:flex;flex-direction:column;gap:3px;overflow:hidden; }
#aw-np-title { font-size:clamp(15px,2.1vh,20px);font-weight:700;color:var(--np-accent,#fff);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:clamp(120px,35vw,400px);text-shadow:0 0 8px rgba(0,0,0,1),0 1px 3px rgba(0,0,0,1),0 0 20px rgba(0,0,0,.8); }
#aw-np-epinfo { font-size:clamp(14px,2vh,17px);color:var(--np-accent-70,rgba(255,255,255,.7));font-weight:600;text-shadow:0 0 6px rgba(0,0,0,1),0 1px 3px rgba(0,0,0,1); }
#aw-np-top-right { display:flex;align-items:center;gap:10px;flex-shrink:0; }
#aw-np-brand { font-size:clamp(12px,1.5vh,14px);color:var(--np-accent-60,rgba(255,255,255,.6));font-weight:600;white-space:nowrap;line-height:1;text-shadow:0 0 6px rgba(0,0,0,1),0 1px 3px rgba(0,0,0,1); }
#aw-np-dot { width:12px;height:12px;border-radius:50%;background:var(--np-accent,#fff);cursor:pointer;flex-shrink:0;transition:transform .15s;position:relative;top:-1px;filter:drop-shadow(0 0 4px rgba(0,0,0,.9)); }
#aw-np-dot:hover { transform:scale(1.3); }
#aw-np-dot:hover .np-tip { opacity:1;transition-delay:.3s; }
#aw-np-dot .np-tip { bottom:auto;top:calc(100% + 10px);left:auto;right:0;transform:none; }
#aw-np-dot .np-tip::after { top:auto;bottom:100%;left:auto;right:calc(12px / 2);transform:translateX(50%);border-top-color:transparent;border-bottom-color:rgba(15,15,15,.92); }
#aw-np-color-panel { position:absolute;top:clamp(64px,8vh,90px);right:8px;background:rgba(28,28,28,.97);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:10px;font-size:13px;color:rgba(255,255,255,.9);z-index:11;opacity:0;transform:scale(.95);transform-origin:top right;pointer-events:none;transition:opacity .15s ease,transform .15s ease; }
#aw-np-color-panel.open { opacity:1;transform:scale(1);pointer-events:all; }
#aw-np-color-swatches { display:flex;flex-wrap:wrap;gap:8px;width:152px; }
.np-swatch { width:24px;height:24px;border-radius:50%;cursor:pointer;border:2px solid transparent;transition:transform .15s,border-color .15s;flex-shrink:0; }
.np-swatch:hover { transform:scale(1.2); }
.np-swatch.active { border-color:rgba(255,255,255,.8); }
#aw-np-controls { position:absolute;bottom:0;left:0;right:0;display:flex;flex-direction:column;padding:0 8px 8px;opacity:0;transition:opacity .3s;pointer-events:none; }
#aw-np.ui #aw-np-controls { opacity:1;pointer-events:all; }
#aw-np-seek-wrap { height:40px;display:flex;align-items:center;cursor:pointer;padding:0 2px; }
#aw-np-seek-track { position:relative;width:100%;height:4px;border-radius:2px;background:rgba(255,255,255,.25); }
#aw-np-seek-buf { position:absolute;inset:0;border-radius:2px;background:var(--np-accent-dim,rgba(255,255,255,.3));width:0; }
#aw-np-seek-fill { position:absolute;inset:0;border-radius:2px;background:var(--np-accent,#fff);width:0; }
#aw-np-seek-thumb { position:absolute;top:50%;left:0;width:14px;height:14px;background:var(--np-accent,#fff);border-radius:50%;transform:translate(-50%,-50%);box-shadow:0 0 4px rgba(0,0,0,.6); }
#aw-np-seek-tip { position:absolute;bottom:calc(100% + 8px);left:0;transform:translateX(-50%);background:rgba(0,0,0,.7);color:#fff;font-size:11px;padding:2px 6px;border-radius:4px;pointer-events:none;white-space:nowrap;visibility:hidden; }
#aw-np-bar { display:flex;align-items:center;height:clamp(36px,5vh,52px);gap:0; }
.np-btn { position:relative;display:flex;align-items:center;justify-content:center;width:clamp(36px,4.5vh,52px);height:clamp(36px,4.5vh,52px);background:none;border:none;cursor:pointer;color:rgba(255,255,255,.9);padding:0;flex-shrink:0; }
.np-btn:hover { color:#fff; }
.np-btn svg { display:block;fill:currentColor;flex-shrink:0;width:clamp(22px,3vh,32px);height:clamp(22px,3vh,32px); }
.np-btn svg line { stroke:currentColor; }
.accent-icons .np-btn svg { fill:var(--np-accent,#fff); }
.accent-icons .np-btn svg line { stroke:var(--np-accent,#fff); }
#aw-np-time { font-size:clamp(11px,1.4vh,14px);font-weight:500;color:rgba(255,255,255,.9);letter-spacing:.3px;white-space:nowrap;padding:0 8px;font-variant-numeric:tabular-nums;text-shadow:0 0 6px rgba(0,0,0,1),0 1px 2px rgba(0,0,0,1); }
#aw-np-spacer { flex:1; }
#aw-np-vol-group { position:relative;display:flex;align-items:center; }
#aw-np-vol-popup { position:absolute;bottom:100%;left:50%;transform:translateX(-50%);width:44px;height:0;overflow:hidden;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:6px;transition:height .2s ease,padding .2s ease;padding:0; }
#aw-np-vol-group:hover #aw-np-vol-popup, #aw-np-vol-group:focus-within #aw-np-vol-popup { height:148px;padding:10px 0 12px; }
#aw-np-vol-popup::after { content:'';position:absolute;bottom:0;left:0;right:0;height:12px; }
#aw-np-vol-pct { font-size:12px;font-weight:700;color:rgba(255,255,255,.9);width:32px;text-align:center;font-variant-numeric:tabular-nums;flex-shrink:0;display:block;text-shadow:0 0 6px rgba(0,0,0,1),0 1px 2px rgba(0,0,0,1); }
#aw-np-vol { -webkit-appearance:none;appearance:none;width:4px;height:108px;border-radius:2px;background:rgba(255,255,255,.25);cursor:pointer;outline:none;writing-mode:vertical-lr;direction:rtl;filter:drop-shadow(0 0 4px rgba(0,0,0,.8)); }
#aw-np-vol::-webkit-slider-thumb { -webkit-appearance:none;width:14px;height:14px;background:var(--np-accent,#fff);border-radius:50%;cursor:pointer;box-shadow:0 0 6px rgba(0,0,0,.7); }
#aw-np-vol::-moz-range-thumb { width:14px;height:14px;background:var(--np-accent,#fff);border:none;border-radius:50%;cursor:pointer;box-shadow:0 0 6px rgba(0,0,0,.7); }
#aw-np-spinner { position:absolute;top:50%;left:50%;width:44px;height:44px;margin:-22px 0 0 -22px;border:3px solid rgba(255,255,255,.15);border-top-color:var(--np-accent,#fff);border-radius:50%;animation:np-spin .7s linear infinite;pointer-events:none;display:none; }
#aw-np-spinner.on { display:block; }
@keyframes np-spin { to { transform:rotate(360deg); } }
#aw-np-center { position:absolute;top:50%;left:50%;width:64px;height:64px;margin:-32px 0 0 -32px;background:rgba(0,0,0,.45);border-radius:50%;display:flex;align-items:center;justify-content:center;pointer-events:none;opacity:0;transform:scale(.8);transition:opacity .25s,transform .25s; }
#aw-np-center.on { opacity:1;transform:scale(1); }
#aw-np-center svg { fill:var(--np-accent,#fff);width:32px;height:32px; }
#aw-np-vol-flash { position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45);border-radius:50%;width:64px;height:64px;pointer-events:none;opacity:0;transition:opacity .25s;z-index:12; }
#aw-np-vol-flash.on { opacity:1; }
#aw-np-vol-flash svg { fill:var(--np-accent,#fff);width:32px;height:32px;display:block; }
#aw-np-settings-panel { position:absolute;bottom:calc(clamp(36px,5vh,52px) + 28px + 17px);right:8px;background:rgba(28,28,28,.97);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:12px 16px;min-width:220px;display:flex;flex-direction:column;gap:10px;font-size:13px;color:rgba(255,255,255,.9);z-index:10;opacity:0;transform:scale(.95);transform-origin:bottom right;pointer-events:none;transition:opacity .15s ease,transform .15s ease; }
#aw-np-settings-panel.open { opacity:1;transform:scale(1);pointer-events:all; }
.np-row-tip { position:absolute;top:50%;right:calc(100% + 16px);transform:translateY(-50%);background:rgba(15,15,15,.92);color:#fff;font-size:11px;padding:4px 8px;border-radius:6px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s ease;transition-delay:0s;z-index:20; }
.np-row-tip::after { content:'';position:absolute;top:50%;left:100%;transform:translateY(-50%);border:4px solid transparent;border-left-color:rgba(15,15,15,.92); }
[data-tip]:hover .np-row-tip { opacity:1;transition-delay:.5s; }
.np-switch { position:relative;width:36px;height:20px;flex-shrink:0;cursor:pointer; }
.np-switch input { opacity:0;width:0;height:0;position:absolute; }
.np-switch-track { position:absolute;inset:0;border-radius:20px;background:rgba(255,255,255,.25);transition:background .2s; }
.np-switch input:checked ~ .np-switch-track { background:var(--np-accent,#fff); }
.np-switch-thumb { position:absolute;top:3px;left:3px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .2s,background .2s;box-shadow:0 1px 3px rgba(0,0,0,.4); }
.np-switch input:checked ~ .np-switch-thumb { transform:translateX(16px); }
.np-switch input:not(:checked) ~ .np-switch-thumb { background:rgba(255,255,255,.85); }
.np-tip { position:absolute;bottom:calc(100% + 28px + 12px);left:50%;transform:translateX(-50%);background:rgba(15,15,15,.92);color:#fff;font-size:12px;font-weight:500;padding:5px 10px;border-radius:6px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s ease;transition-delay:0s;z-index:15; }
.np-tip::after { content:'';position:absolute;top:100%;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:rgba(15,15,15,.92); }
.np-btn:hover .np-tip { opacity:1;transition-delay:.3s; }
#aw-np-bar > .np-btn:first-child .np-tip { left:0;transform:none; }
#aw-np-bar > .np-btn:first-child .np-tip::after { left:calc(clamp(36px,4.5vh,52px)/2);transform:translateX(-50%); }
#aw-np-bar > .np-btn:last-child .np-tip { left:auto;right:0;transform:none; }
#aw-np-bar > .np-btn:last-child .np-tip::after { left:auto;right:calc(clamp(36px,4.5vh,52px)/2);transform:translateX(50%); }
#aw-np-toast { position:absolute;top:16px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.55);color:var(--np-accent,rgba(255,255,255,.75));font-size:12px;font-weight:500;padding:6px 14px;border-radius:20px;pointer-events:none;white-space:nowrap;opacity:1;transition:opacity .5s ease;z-index:20; }
`;
document.head.appendChild(s);
}
// ── Icone SVG ─────────────────────────────────────────────────────────────
const svg = p => `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">${p}</svg>`;
const IC = {
play: svg('<path d="M8 5v14l11-7z"/>'),
pause: svg('<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>'),
mute: svg('<path d="M16.5 12A4.5 4.5 0 0 0 14 7.97v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>'),
vol: svg('<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>'),
fsOn: svg('<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>'),
fsOff: svg('<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>'),
restart: 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"/>'),
skip: svg('<path d="M13 3a9 9 0 1 0 9 9h-2a7 7 0 1 1-7-7V3z"/><path d="M13 1v6l4-3z"/><line x1="13" y1="8" x2="13" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/><line x1="13" y1="14" x2="16" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/>'),
undo: svg('<g transform="scale(-1,1) translate(-24,0)"><path d="M13 3a9 9 0 1 0 9 9h-2a7 7 0 1 1-7-7V3z"/><path d="M13 1v6l4-3z"/><line x1="13" y1="8" x2="13" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/><line x1="13" y1="14" x2="16" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></g>'),
prev: svg('<rect x="5" y="5" width="2.5" height="14"/><polygon points="19,5 9,12 19,19"/>'),
next: svg('<polygon points="5,5 15,12 5,19"/><rect x="16.5" y="5" width="2.5" height="14"/>'),
settings: svg('<path d="M19.14 12.94c.04-.3.06-.61.06-.94s-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>'),
};
// ── Build player ──────────────────────────────────────────────────────────
let cleanup = null;
function buildPlayer(videoUrl) {
const { vol, muted } = loadVol();
const wrap = mk('div', 'aw-np');
const video = mk('video', 'aw-np-video');
const grad = mk('div', 'aw-np-gradient');
const gradTop = mk('div', 'aw-np-gradient-top');
const spinner = mk('div', 'aw-np-spinner');
const center = mk('div', 'aw-np-center');
const ctrls = mk('div', 'aw-np-controls');
// Ordine critico: autoplay e preload PRIMA di src
video.autoplay = false;
video.preload = 'metadata';
video.src = videoUrl;
video.volume = vol;
video.muted = muted;
video.playbackRate = loadSpeed();
center.innerHTML = IC.play;
// _play è l'unica via autorizzata per avviare la riproduzione
const _origPlay = video.play.bind(video);
const _play = () => _origPlay().catch(() => {});
// ── Seek bar ──────────────────────────────────────────────────────────
const seekWrap = mk('div', 'aw-np-seek-wrap');
const seekTrack = mk('div', 'aw-np-seek-track');
const seekBuf = mk('div', 'aw-np-seek-buf');
const seekFill = mk('div', 'aw-np-seek-fill');
const seekThumb = mk('div', 'aw-np-seek-thumb');
const seekTip = mk('div', 'aw-np-seek-tip');
seekTrack.append(seekBuf, seekFill, seekThumb, seekTip);
seekWrap.append(seekTrack);
// ── Bottoni barra ─────────────────────────────────────────────────────
const btnPlay = mkBtn('aw-btn-play', '', 'Riproduci');
const btnRestart = mkBtn('aw-btn-restart', IC.restart, 'Ricomincia (R)');
const btnMute = mkBtn('aw-btn-mute', '', muted ? 'Audio (M)' : 'Muto (M)');
const btnUndo = mkBtn('aw-btn-undo', IC.undo, 'Annulla skip (B)');
const btnSkip = mkBtn('aw-btn-skip', IC.skip, 'Skip OP/EP (O)');
const btnPrev = mkBtn('aw-btn-prev', IC.prev, 'Precedente (P)');
const btnNext = mkBtn('aw-btn-next', IC.next, 'Successivo (N)');
const btnSettings = mkBtn('aw-btn-settings', IC.settings, 'Impostazioni');
const btnFs = mkBtn('aw-btn-fs', '', 'Fullscreen (F)');
const icoPlay = mkIcon(btnPlay, IC.play);
const icoMute = mkIcon(btnMute, muted ? IC.mute : IC.vol);
const icoFs = mkIcon(btnFs, IC.fsOn);
// ── Volume ────────────────────────────────────────────────────────────
const volGroup = mk('div', 'aw-np-vol-group');
const volPopup = mk('div', 'aw-np-vol-popup');
const volPctEl = mk('div', 'aw-np-vol-pct');
const volEl = mk('input', 'aw-np-vol');
volEl.type = 'range'; volEl.min = 0; volEl.max = 100;
volEl.value = muted ? 0 : Math.round(vol * 100); volEl.tabIndex = -1;
volPctEl.textContent = (muted ? 0 : Math.round(vol * 100)) + '%';
volPopup.append(volPctEl, volEl);
volGroup.append(volPopup, btnMute);
let lastNonZeroVol = muted ? (vol || 1) : vol;
const updateMuteState = () => {
const m = video.muted || video.volume === 0;
setIcon(icoMute, m ? IC.mute : IC.vol);
setTip(btnMute, m ? 'Audio (M)' : 'Muto (M)');
};
const updateVolUi = () => {
const pct = (video.muted || video.volume === 0) ? 0 : Math.round(video.volume * 100);
volPctEl.textContent = pct + '%';
volEl.style.background = `linear-gradient(to top, var(--np-accent,#fff) ${pct}%, rgba(255,255,255,.25) ${pct}%)`;
};
updateVolUi();
volEl.addEventListener('input', () => {
const v = Number(volEl.value) / 100;
video.volume = v; video.muted = v === 0;
if (v > 0) lastNonZeroVol = v;
updateMuteState(); updateVolUi();
saveVol(v === 0 ? lastNonZeroVol : v, video.muted);
});
volEl.addEventListener('mouseup', () => volEl.blur());
btnMute.addEventListener('click', () => {
if (video.muted || video.volume === 0) {
video.muted = false; video.volume = lastNonZeroVol;
volEl.value = Math.round(video.volume * 100);
} else {
if (video.volume > 0) lastNonZeroVol = video.volume;
video.muted = true; volEl.value = 0;
}
updateMuteState(); updateVolUi();
saveVol(video.muted ? lastNonZeroVol : video.volume, video.muted);
showVolFlash();
});
// ── Time ──────────────────────────────────────────────────────────────
const timeEl = mk('div', 'aw-np-time'); timeEl.textContent = '00:00 / 00:00';
const spacer = mk('div', 'aw-np-spacer');
// ── Pannello impostazioni ─────────────────────────────────────────────
const settingsPanel = mk('div', 'aw-np-settings-panel');
const BTN_CTRL_STYLE = 'background:rgba(255,255,255,.15);border:none;color:#fff;width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center;';
const mkSettingRow = (label, tip, on, onChange) => {
const row = document.createElement('div');
row.style.cssText = 'position:relative;display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;';
row.dataset.tip = '1';
const lbl = document.createElement('span'); lbl.textContent = label;
const { label: sw, input: toggle } = mkSwitch(on);
toggle.addEventListener('change', () => onChange(toggle.checked));
row.addEventListener('click', () => { toggle.checked = !toggle.checked; toggle.dispatchEvent(new Event('change')); });
sw.addEventListener('click', e => e.stopPropagation());
row.append(lbl, sw, mkRowTip(tip));
return { row, toggle };
};
const { row: resumeRow, toggle: resumeToggle } = mkSettingRow('Ripresa automatica', 'Riprende il video dall\'ultimo punto', isResumeOn(), v => lsSet(pKey(KEY_RESUME_ENABLE), v ? '1' : '0'));
const { row: autoEpRow, toggle: autoEpToggle } = mkSettingRow('Episodio automatico', 'Riapre l\'ultimo episodio visto di questa serie', isAutoEpOn(), v => lsSet(pKey(KEY_AUTOEP_ENABLE), v ? '1' : '0'));
const { row: autoPlayRow, toggle: autoPlayToggle } = mkSettingRow('Autoplay', 'In fullscreen, il video parte in automatico al cambio episodio e all\'entrata', isAutoPlayOn(), v => lsSet(pKey(KEY_AUTOPLAY_ENABLE), v ? '1' : '0'));
// Seek
let seekSecs = loadSeekSecs();
const seekRow = document.createElement('div'); seekRow.style.cssText = 'position:relative;display:flex;align-items:center;justify-content:space-between;gap:12px;user-select:none;'; seekRow.dataset.tip = '1';
const seekLabel = document.createElement('span'); seekLabel.textContent = 'Seek';
const seekControls = document.createElement('div'); seekControls.style.cssText = 'display:flex;align-items:center;gap:6px;';
const seekMinus = document.createElement('button'); seekMinus.textContent = '−'; seekMinus.style.cssText = BTN_CTRL_STYLE; seekMinus.tabIndex = -1;
const seekPlus = document.createElement('button'); seekPlus.textContent = '+'; seekPlus.style.cssText = BTN_CTRL_STYLE; seekPlus.tabIndex = -1;
const seekVal = document.createElement('span'); seekVal.style.cssText = 'min-width:40px;text-align:center;font-weight:500;';
const updateSeekVal = () => { seekVal.textContent = String(seekSecs).padStart(2,'0') + ' s'; };
updateSeekVal();
seekMinus.addEventListener('click', e => { e.stopPropagation(); if (seekSecs <= SEEK_MIN) return; seekSecs -= SEEK_STEP; lsSet(pKey(KEY_SEEK_SECS), String(seekSecs)); updateSeekVal(); });
seekPlus.addEventListener('click', e => { e.stopPropagation(); if (seekSecs >= SEEK_MAX) return; seekSecs += SEEK_STEP; lsSet(pKey(KEY_SEEK_SECS), String(seekSecs)); updateSeekVal(); });
seekControls.append(seekMinus, seekVal, seekPlus);
seekRow.append(seekLabel, seekControls, mkRowTip('Regolazione del tempo di avanzamento con le freccette'));
// Speed
let speedVal = loadSpeed();
const speedRow = document.createElement('div'); speedRow.style.cssText = 'position:relative;display:flex;align-items:center;justify-content:space-between;gap:12px;user-select:none;'; speedRow.dataset.tip = '1';
const speedLabel = document.createElement('span'); speedLabel.textContent = 'Velocità';
const speedControls = document.createElement('div'); speedControls.style.cssText = 'display:flex;align-items:center;gap:6px;';
const speedMinus = document.createElement('button'); speedMinus.textContent = '−'; speedMinus.style.cssText = BTN_CTRL_STYLE; speedMinus.tabIndex = -1;
const speedPlus = document.createElement('button'); speedPlus.textContent = '+'; speedPlus.style.cssText = BTN_CTRL_STYLE; speedPlus.tabIndex = -1;
const speedValEl = document.createElement('span'); speedValEl.style.cssText = 'min-width:40px;text-align:center;font-weight:500;';
const updateSpeedVal = () => { speedValEl.textContent = fmtSpeed(speedVal); if (video.readyState > 0) video.playbackRate = speedVal; };
updateSpeedVal();
speedMinus.addEventListener('click', e => { e.stopPropagation(); if (speedVal <= SPEED_MIN) return; speedVal = Math.round((speedVal - SPEED_STEP)*100)/100; lsSet(pKey(KEY_SPEED), String(speedVal)); updateSpeedVal(); });
speedPlus.addEventListener('click', e => { e.stopPropagation(); if (speedVal >= SPEED_MAX) return; speedVal = Math.round((speedVal + SPEED_STEP)*100)/100; lsSet(pKey(KEY_SPEED), String(speedVal)); updateSpeedVal(); });
speedControls.append(speedMinus, speedValEl, speedPlus);
speedRow.append(speedLabel, speedControls, mkRowTip('Regola la velocità di riproduzione dell\'episodio'));
// Globale
const globalRow = document.createElement('div'); globalRow.style.cssText = 'position:relative;display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;border-top:1px solid rgba(255,255,255,.12);padding-top:10px;margin-top:2px;'; globalRow.dataset.tip = '1';
const globalLabel = document.createElement('span'); globalLabel.textContent = 'Globale';
const { label: globalSw, input: globalToggle } = mkSwitch(isGlobalOn());
globalToggle.addEventListener('change', () => {
lsSet(KEY_GLOBAL, globalToggle.checked ? '1' : '0');
resumeToggle.checked = isResumeOn(); autoEpToggle.checked = isAutoEpOn(); autoPlayToggle.checked = isAutoPlayOn();
seekSecs = loadSeekSecs(); updateSeekVal(); speedVal = loadSpeed(); updateSpeedVal();
});
globalRow.addEventListener('click', () => { globalToggle.checked = !globalToggle.checked; globalToggle.dispatchEvent(new Event('change')); });
globalSw.addEventListener('click', e => e.stopPropagation());
globalRow.append(globalLabel, globalSw, mkRowTip('Le impostazioni si applicano a tutte le serie, disattiva per personalizzare ogni serie'));
settingsPanel.append(resumeRow, autoEpRow, autoPlayRow, seekRow, speedRow, globalRow);
// ── Barra controlli ───────────────────────────────────────────────────
const bar = mk('div', 'aw-np-bar');
bar.append(btnPlay, btnRestart, volGroup, timeEl, spacer, btnUndo, btnSkip, btnPrev, btnNext, btnSettings, btnFs);
ctrls.append(seekWrap, bar);
// ── Top bar ───────────────────────────────────────────────────────────
const topBar = mk('div', 'aw-np-top');
const topLeft = mk('div', 'aw-np-top-left');
const topRight = mk('div', 'aw-np-top-right');
const titleEl = mk('div', 'aw-np-title');
const epInfoEl = mk('div', 'aw-np-epinfo');
const brandEl = mk('div', 'aw-np-brand');
const dotEl = mk('div', 'aw-np-dot');
const allEps = Array.from(document.querySelectorAll('.episode a'));
const epIdx = allEps.findIndex(a => a.classList.contains('active'));
const activeEp = epIdx !== -1 ? allEps[epIdx] : null;
const epNum = activeEp ? (activeEp.textContent.trim() || String(epIdx + 1)) : '?';
const epMaxNum = allEps.reduce((m, a) => { const n = parseFloat(a.textContent.trim()); return isNaN(n) ? m : Math.max(m, n); }, 0);
const epTotal = epMaxNum > 0 ? String(epMaxNum) : (allEps.length || '?');
titleEl.textContent = window.animeName || document.title.split(' Episodio')[0] || '';
epInfoEl.textContent = `Episodio ${epNum}/${epTotal}`;
brandEl.textContent = `AW Better Player v${SCRIPT_VERSION}`;
const dotTip = document.createElement('span'); dotTip.className = 'np-tip'; dotTip.textContent = 'Aspetto';
dotEl.style.position = 'relative'; dotEl.appendChild(dotTip);
topLeft.append(titleEl, epInfoEl);
topRight.append(brandEl, dotEl);
topBar.append(topLeft, topRight);
// ── Pannello colori ───────────────────────────────────────────────────
const colorPanel = mk('div', 'aw-np-color-panel');
const swatchWrap = mk('div', 'aw-np-color-swatches');
let currentColor = loadColor();
const customInput = document.createElement('input');
const customPreview = document.createElement('div');
const syncCustomInput = hex => { customInput.value = hex; customPreview.style.background = hex; };
const applyCustomColor = () => {
let val = customInput.value.trim();
if (!val.startsWith('#')) val = '#' + val;
if (!/^#[0-9a-fA-F]{6}$/i.test(val)) { syncCustomInput(currentColor); return; }
val = val.toLowerCase(); currentColor = val; lsSet(colorKey(), val);
applyColor(val, wrap, dotEl); syncCustomInput(val);
swatchWrap.querySelectorAll('.np-swatch').forEach(s => s.classList.toggle('active', s.dataset.hex === val));
};
syncCustomInput(currentColor);
PALETTE.forEach(({ name, hex }) => {
const sw = document.createElement('div');
sw.className = 'np-swatch' + (hex === currentColor ? ' active' : '');
sw.style.background = hex; sw.dataset.hex = hex; sw.title = name;
sw.addEventListener('click', e => {
e.stopPropagation(); currentColor = hex; lsSet(colorKey(), hex);
applyColor(hex, wrap, dotEl); syncCustomInput(hex);
swatchWrap.querySelectorAll('.np-swatch').forEach(s => s.classList.toggle('active', s.dataset.hex === hex));
});
swatchWrap.appendChild(sw);
});
const customRow = document.createElement('div'); customRow.style.cssText = 'position:relative;display:flex;align-items:center;gap:8px;width:100%;'; customRow.dataset.tip = '1'; customRow.addEventListener('click', e => e.stopPropagation());
const customLabel = document.createElement('span'); customLabel.textContent = 'Custom'; customLabel.style.cssText = 'font-size:13px;color:rgba(255,255,255,.9);flex-shrink:0;';
customPreview.style.cssText = 'width:14px;height:14px;border-radius:50%;flex-shrink:0;border:1px solid rgba(255,255,255,.3);';
customInput.type = 'text'; customInput.maxLength = 7; customInput.placeholder = '#ffffff'; customInput.spellcheck = false;
customInput.style.cssText = 'background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);color:#fff;font-size:12px;padding:3px 6px;border-radius:4px;width:76px;outline:none;font-family:monospace;user-select:text;';
customInput.addEventListener('input', () => { let v = customInput.value.trim(); if (!v.startsWith('#')) v = '#' + v; if (/^#[0-9a-fA-F]{6}$/i.test(v)) customPreview.style.background = v; });
let _applyingCustom = false;
customInput.addEventListener('blur', () => { if (!_applyingCustom) applyCustomColor(); });
customInput.addEventListener('keydown', e => { e.stopPropagation(); if (e.key === 'Enter') { _applyingCustom = true; applyCustomColor(); customInput.blur(); _applyingCustom = false; } if (e.key === 'Escape') { syncCustomInput(currentColor); customInput.blur(); } });
customInput.addEventListener('click', e => e.stopPropagation());
customInput.addEventListener('focus', () => customInput.select());
customRow.append(customLabel, customPreview, customInput, mkRowTip('Scegli un colore HEX'));
const iconColorRow = document.createElement('div'); iconColorRow.style.cssText = 'position:relative;display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;border-top:1px solid rgba(255,255,255,.12);padding-top:10px;'; iconColorRow.dataset.tip = '1';
const iconColorLabel = document.createElement('span'); iconColorLabel.textContent = 'Icone colorate';
const { label: iconColorSw, input: iconColorToggle } = mkSwitch(isIconColorOn());
iconColorToggle.addEventListener('change', e => { e.stopPropagation(); lsSet(KEY_ICON_COLOR, iconColorToggle.checked ? '1' : '0'); wrap.classList.toggle('accent-icons', iconColorToggle.checked); });
iconColorRow.addEventListener('click', () => { iconColorToggle.checked = !iconColorToggle.checked; iconColorToggle.dispatchEvent(new Event('change')); });
iconColorSw.addEventListener('click', e => e.stopPropagation());
iconColorRow.append(iconColorLabel, iconColorSw, mkRowTip('Attiva per colorare le icone del player'));
const colorGlobalRow = document.createElement('div'); colorGlobalRow.id = 'aw-np-color-global'; colorGlobalRow.style.cssText = 'position:relative;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid rgba(255,255,255,.12);padding-top:10px;cursor:pointer;'; colorGlobalRow.dataset.tip = '1';
const colorGlobalLabel = document.createElement('span'); colorGlobalLabel.textContent = 'Globale';
const { label: colorGlobalSw, input: colorGlobalToggle } = mkSwitch(isColorGlobalOn());
colorGlobalToggle.addEventListener('change', e => { e.stopPropagation(); lsSet(KEY_COLOR_GLOBAL, colorGlobalToggle.checked ? '1' : '0'); currentColor = loadColor(); applyColor(currentColor, wrap, dotEl); syncCustomInput(currentColor); swatchWrap.querySelectorAll('.np-swatch').forEach(s => s.classList.toggle('active', s.dataset.hex === currentColor)); });
colorGlobalRow.addEventListener('click', () => { colorGlobalToggle.checked = !colorGlobalToggle.checked; colorGlobalToggle.dispatchEvent(new Event('change')); });
colorGlobalSw.addEventListener('click', e => e.stopPropagation());
colorGlobalRow.append(colorGlobalLabel, colorGlobalSw, mkRowTip('Le impostazioni colore si applicano a tutte le serie'));
colorPanel.append(swatchWrap, customRow, iconColorRow, colorGlobalRow);
if (isIconColorOn()) wrap.classList.add('accent-icons');
dotEl.addEventListener('click', e => { e.stopPropagation(); const was = colorPanel.classList.contains('open'); settingsPanel.classList.remove('open'); colorPanel.classList.toggle('open', !was); });
colorPanel.addEventListener('click', e => e.stopPropagation());
btnSettings.addEventListener('click', e => { e.stopPropagation(); const was = settingsPanel.classList.contains('open'); colorPanel.classList.remove('open'); settingsPanel.classList.toggle('open', !was); });
settingsPanel.addEventListener('click', e => e.stopPropagation());
wrap.addEventListener('click', () => { settingsPanel.classList.remove('open'); colorPanel.classList.remove('open'); });
wrap.append(video, grad, gradTop, topBar, colorPanel, spinner, center, settingsPanel, ctrls);
// ── Volume flash ──────────────────────────────────────────────────────
const volFlash = mk('div', 'aw-np-vol-flash');
const volFlashIco = mk('div', 'aw-np-vol-flash-icon');
volFlash.appendChild(volFlashIco);
wrap.appendChild(volFlash);
let volFlashTimer = null;
const showVolFlash = () => { volFlashIco.innerHTML = (video.muted || video.volume === 0) ? IC.mute : IC.vol; volFlash.classList.add('on'); clearTimeout(volFlashTimer); volFlashTimer = setTimeout(() => volFlash.classList.remove('on'), 400); };
applyColor(currentColor, wrap, dotEl);
// ── UI show/hide ──────────────────────────────────────────────────────
let hideTimer = null;
const showUi = () => { wrap.classList.add('ui'); clearTimeout(hideTimer); if (!video.paused) hideTimer = setTimeout(() => wrap.classList.remove('ui'), HIDE_DELAY_MS); };
wrap.addEventListener('mousemove', showUi);
wrap.addEventListener('mouseleave', () => { if (!video.paused) wrap.classList.remove('ui'); });
video.addEventListener('pause', () => { wrap.classList.add('ui'); clearTimeout(hideTimer); });
video.addEventListener('play', showUi);
// ── Flash centrale ────────────────────────────────────────────────────
let cTimer = null;
const flash = html => { center.innerHTML = html; center.classList.add('on'); clearTimeout(cTimer); cTimer = setTimeout(() => center.classList.remove('on'), 700); };
// ── Toggle play/pause ─────────────────────────────────────────────────
let skipFlash = false;
const toggle = () => video.paused ? _play() : video.pause();
video.addEventListener('click', toggle);
video.addEventListener('dblclick', () => btnFs.click());
btnPlay.addEventListener('click', toggle);
video.addEventListener('play', () => { setIcon(icoPlay, IC.pause); setTip(btnPlay, 'Pausa'); if (!skipFlash) flash(IC.pause); });
video.addEventListener('pause', () => { setIcon(icoPlay, IC.play); setTip(btnPlay, 'Riproduci'); if (!skipFlash) flash(IC.play); });
video.addEventListener('ended', () => { setIcon(icoPlay, IC.play); setTip(btnPlay, 'Riproduci'); });
btnRestart.addEventListener('click', () => { video.currentTime = 0; _play(); flash(IC.restart); });
btnSkip.addEventListener('click', () => { video.currentTime = Math.min(video.duration||0, video.currentTime + SKIP_SECONDS); flash(IC.skip); });
btnUndo.addEventListener('click', () => { video.currentTime = Math.max(0, video.currentTime - SKIP_SECONDS); flash(IC.undo); });
btnPrev.addEventListener('click', () => { const t = getAdjacentEpisode('prev'); if (t) loadEpisode(t.dataset.id); });
btnNext.addEventListener('click', () => { const t = getAdjacentEpisode('next'); if (t) loadEpisode(t.dataset.id); });
btnFs.addEventListener('click', () => document.fullscreenElement ? document.exitFullscreen() : wrap.requestFullscreen?.().catch(()=>{}));
// ── Spinner ───────────────────────────────────────────────────────────
video.addEventListener('waiting', () => spinner.classList.add('on'));
video.addEventListener('playing', () => spinner.classList.remove('on'));
video.addEventListener('canplay', () => spinner.classList.remove('on'));
// ── Seek bar interazione ──────────────────────────────────────────────
let seeking = false;
const applySeek = e => { const r = seekTrack.getBoundingClientRect(); const p = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)); if (video.duration) video.currentTime = p * video.duration; };
seekWrap.addEventListener('mousedown', e => { seeking = true; applySeek(e); e.preventDefault(); });
seekWrap.addEventListener('mousemove', e => {
if (seeking) applySeek(e);
const r = seekTrack.getBoundingClientRect();
const p = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
seekTip.textContent = fmt(p * (video.duration || 0));
seekTip.style.left = (p * 100) + '%';
seekTip.style.visibility = 'visible';
});
seekWrap.addEventListener('mouseleave', () => { seekTip.style.visibility = 'hidden'; });
document.addEventListener('mouseup', () => { seeking = false; });
// ── Time update + MAL-Sync ────────────────────────────────────────────
let malSyncTriggered = false;
video.addEventListener('loadedmetadata', () => { malSyncTriggered = false; });
video.addEventListener('timeupdate', () => {
if (seeking || !video.duration) return;
const p = video.currentTime / video.duration * 100;
seekFill.style.width = p + '%';
seekThumb.style.left = p + '%';
timeEl.textContent = `${fmt(video.currentTime)} / ${fmt(video.duration)}`;
if (!malSyncTriggered && p >= 90) { malSyncTriggered = true; history.pushState({}, '', location.href); }
});
video.addEventListener('progress', () => {
if (!video.duration || !video.buffered.length) return;
seekBuf.style.width = (video.buffered.end(video.buffered.length - 1) / video.duration * 100) + '%';
});
// ── Resume ────────────────────────────────────────────────────────────
let episodeEnded = false;
function showResumeToast(seconds) {
wrap.querySelector('#aw-np-toast')?.remove();
const toast = mk('div', 'aw-np-toast'); toast.textContent = `▶ Ripreso da ${fmt(seconds)}`; wrap.appendChild(toast);
setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 500); }, 4000);
}
function attemptResume() {
if (!isResumeOn() || !isFinite(video.duration)) return;
const saved = parseFloat(lsGet(resumeKey()) ?? '');
if (!saved || saved < RESUME_MIN_POS) return;
if (video.duration - saved < RESUME_END_GAP) { clearResumePos(); return; }
video.currentTime = saved; // solo seek, MAI play()
showResumeToast(saved);
}
// Resume su loadedmetadata — seek only, nessun play()
video.addEventListener('loadedmetadata', () => {
if (video.playbackRate !== speedVal) video.playbackRate = speedVal;
attemptResume();
}, { once: true });
// ── Save timer ────────────────────────────────────────────────────────
let saveTimer = null;
const startSaving = () => { if (saveTimer) return; saveTimer = setInterval(() => saveResumePos(video.currentTime), SAVE_INTERVAL_MS); };
const stopSaving = () => { clearInterval(saveTimer); saveTimer = null; };
_stopSavingFn = stopSaving;
video.addEventListener('play', startSaving);
video.addEventListener('pause', stopSaving);
video.addEventListener('ended', () => { stopSaving(); clearResumePos(); episodeEnded = true; });
// ── Fullscreen + Autoplay ─────────────────────────────────────────────
// REGOLA UNICA: isAutoPlayOn() controlla TUTTO il play automatico.
// - Entrata in fullscreen → play se flag ON + video in pausa + episodio non terminato
// - Windowed → MAI play automatico (né qui né altrove in buildPlayer)
// - swapVideoSrc → play se flag ON (vedi swapVideoSrc)
const onFs = () => {
const isFs = !!document.fullscreenElement;
setIcon(icoFs, isFs ? IC.fsOff : IC.fsOn);
setTip(btnFs, isFs ? 'Esci (F)' : 'Fullscreen (F)');
if (isFs && isAutoPlayOn() && video.paused && !episodeEnded) _play();
};
// ── Tastiera ──────────────────────────────────────────────────────────
const onKey = e => {
const tag = document.activeElement?.tagName ?? '', editable = document.activeElement?.isContentEditable;
if (/INPUT|TEXTAREA/.test(tag) || editable) return;
if (e.key === ' ') { e.preventDefault(); toggle(); }
if (e.key === 'ArrowRight') { e.preventDefault(); video.currentTime = Math.min(video.duration||0, video.currentTime + seekSecs); }
if (e.key === 'ArrowLeft') { e.preventDefault(); video.currentTime = Math.max(0, video.currentTime - seekSecs); }
if (e.key === 'ArrowUp') { e.preventDefault(); video.volume = Math.min(1, video.volume+.1); video.muted = false; lastNonZeroVol = video.volume; volEl.value = Math.round(video.volume*100); saveVol(video.volume, false); updateMuteState(); updateVolUi(); showVolFlash(); }
if (e.key === 'ArrowDown') { e.preventDefault(); video.volume = Math.max(0, video.volume-.1); video.muted = video.volume===0; if (video.volume>0) lastNonZeroVol = video.volume; volEl.value = Math.round(video.volume*100); saveVol(video.muted ? lastNonZeroVol : video.volume, video.muted); updateMuteState(); updateVolUi(); showVolFlash(); }
if (e.key === 'f' || e.key === 'F') btnFs.click();
if (e.key === 'm' || e.key === 'M') btnMute.click();
if (e.key === 'r' || e.key === 'R') btnRestart.click();
if (e.key === 'o' || e.key === 'O') btnSkip.click();
if (e.key === 'b' || e.key === 'B') btnUndo.click();
if (e.key === 'p' || e.key === 'P') btnPrev.click();
if (e.key === 'n' || e.key === 'N') btnNext.click();
};
// ── Storage sync ──────────────────────────────────────────────────────
const onStorage = e => {
if (!e.key?.startsWith('aw-np-')) return;
resumeToggle.checked = isResumeOn(); autoEpToggle.checked = isAutoEpOn(); autoPlayToggle.checked = isAutoPlayOn(); globalToggle.checked = isGlobalOn();
seekSecs = loadSeekSecs(); updateSeekVal(); speedVal = loadSpeed(); updateSpeedVal();
const newColor = loadColor();
if (newColor !== currentColor) { currentColor = newColor; applyColor(currentColor, wrap, dotEl); syncCustomInput(currentColor); swatchWrap.querySelectorAll('.np-swatch').forEach(s => s.classList.toggle('active', s.dataset.hex === currentColor)); }
const iconOn = isIconColorOn(); wrap.classList.toggle('accent-icons', iconOn); iconColorToggle.checked = iconOn; colorGlobalToggle.checked = isColorGlobalOn();
};
const onUnload = () => { if (!episodeEnded) saveResumePos(video.currentTime); };
document.addEventListener('fullscreenchange', onFs);
document.addEventListener('keydown', onKey);
window.addEventListener('beforeunload', onUnload);
window.addEventListener('storage', onStorage);
// ── Cleanup ───────────────────────────────────────────────────────────
cleanup = () => {
document.removeEventListener('fullscreenchange', onFs);
document.removeEventListener('keydown', onKey);
window.removeEventListener('beforeunload', onUnload);
window.removeEventListener('storage', onStorage);
stopSaving(); _stopSavingFn = null;
clearTimeout(hideTimer); clearTimeout(cTimer); clearTimeout(volFlashTimer);
skipFlash = true; video.pause(); video.src = '';
cleanup = null;
};
// Metodi esposti per swapVideoSrc
wrap._play = () => _play();
wrap._showResumeTst = s => showResumeToast(s);
wrap._setSkipFlash = v => { skipFlash = v; };
return wrap;
}
// ── Mount (windowed) ──────────────────────────────────────────────────────
// Crea sempre un player nuovo. Non chiama mai play() automaticamente.
function mountPlayer(url) {
if (!url) return;
const container = document.querySelector('#player');
if (!container) return;
const existing = container.querySelector('#aw-np-video');
if (existing && existing.getAttribute('src') === url) return;
if (cleanup) cleanup();
injectStyle();
container.innerHTML = '';
container.appendChild(buildPlayer(url));
}
// ── Swap src (fullscreen) ─────────────────────────────────────────────────
// Chiamata solo quando si è già in fullscreen e si cambia episodio.
// REGOLA: play automatico SOLO se il flag autoplay è ON.
function swapVideoSrc(url) {
const video = document.querySelector('#aw-np-video');
if (!video) { mountPlayer(url); return; }
if (video.getAttribute('src') === url) return;
const wrap = document.querySelector('#aw-np');
wrap?._setSkipFlash(true);
video.pause();
video.src = url;
video.addEventListener('loadedmetadata', () => {
// Velocità
const spd = loadSpeed();
if (video.playbackRate !== spd) video.playbackRate = spd;
// Resume: seek only, MAI play()
if (isResumeOn()) {
const saved = parseFloat(lsGet(resumeKey()) ?? '');
if (saved >= RESUME_MIN_POS && isFinite(video.duration) && video.duration - saved >= RESUME_END_GAP) {
video.currentTime = saved;
wrap?._showResumeTst(saved);
}
}
// Autoplay: SOLO se flag ON
if (isAutoPlayOn()) {
wrap?._play();
video.addEventListener('playing', () => wrap?._setSkipFlash(false), { once: true });
} else {
wrap?._setSkipFlash(false);
}
}, { once: true });
// Reset UI
const sel = { '#aw-np-seek-fill': 'width', '#aw-np-seek-thumb': 'left', '#aw-np-seek-buf': 'width' };
Object.entries(sel).forEach(([id, prop]) => { const el = document.querySelector(id); if (el) el.style[prop] = '0%'; });
const timeEl = document.querySelector('#aw-np-time'); if (timeEl) timeEl.textContent = '00:00 / 00:00';
}
// ── Navigazione ───────────────────────────────────────────────────────────
function setActiveEpisode(token) {
const all = Array.from(document.querySelectorAll('.episode a'));
const idx = all.findIndex(a => a.dataset.id === token);
all.forEach((a, i) => a.classList.toggle('active', i === idx));
const prevBtn = document.querySelector('.prevnext.prev'); if (prevBtn) prevBtn.style.display = idx > 0 ? '' : 'none';
const nextBtn = document.querySelector('.prevnext.next'); if (nextBtn) nextBtn.style.display = idx < all.length-1 ? '' : 'none';
const epInfoEl = document.querySelector('#aw-np-epinfo');
if (epInfoEl && idx !== -1) {
const num = all[idx].textContent.trim() || String(idx + 1);
const maxNum = all.reduce((m, a) => { const n = parseFloat(a.textContent.trim()); return isNaN(n) ? m : Math.max(m, n); }, 0);
epInfoEl.textContent = `Episodio ${num}/${maxNum > 0 ? maxNum : all.length}`;
}
}
// ── loadEpisode ───────────────────────────────────────────────────────────
// Flusso token garantito (nessuna race condition con il save timer):
// 1. _stopSavingFn() → ferma il timer, nessuna scrittura futura
// 2. saveResumePos() → salva con _activeToken (vecchio ep)
// 3. _activeToken = token → aggiorna al nuovo ep
// 4. fetch → swapVideoSrc / mountPlayer → resume legge _activeToken ✓
function loadEpisode(token) {
if (!token) return;
// 1. Ferma il timer immediatamente (PRIMA di cambiare token)
if (_stopSavingFn) { _stopSavingFn(); _stopSavingFn = null; }
// 2. Salva posizione del vecchio episodio
const vid = document.querySelector('#aw-np-video');
if (vid && _activeToken) saveResumePos(vid.currentTime);
// 3. Aggiorna token e UI
_activeToken = token;
saveLastEpisode(token);
setActiveEpisode(token);
// 4. Fetch e mount
const wasFullscreen = !!document.fullscreenElement;
getUrlForToken(token).then(url => {
if (!url) return;
const epLink = document.querySelector(`.episode a[data-id="${token}"]`);
if (epLink?.href) history.pushState({}, '', epLink.href);
if (wasFullscreen && !!document.fullscreenElement) swapVideoSrc(url);
else mountPlayer(url);
});
}
// ── Wire navigazione ──────────────────────────────────────────────────────
function wireControls() {
document.querySelectorAll('.episode a').forEach(a => {
if (a.dataset.npWired) return; a.dataset.npWired = '1';
a.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); loadEpisode(a.dataset.id); });
});
document.querySelectorAll('.prevnext').forEach(btn => {
if (btn.dataset.npWired) return; btn.dataset.npWired = '1';
btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const t = getAdjacentEpisode(btn.dataset.value); if (t) loadEpisode(t.dataset.id); });
});
}
// ── Label "Better Player" ─────────────────────────────────────────────────
function setupPlayerLabel() {
const hide = () => {
['.control[data-value="original"]', '.control[data-value="alternative"]'].forEach(sel =>
document.querySelectorAll(sel).forEach(el => {
if (el.dataset.npBlocked) return; el.dataset.npBlocked = '1';
el.style.setProperty('display', 'none', 'important');
el.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); }, true);
})
);
if (!document.getElementById('aw-bp-label')) {
const ref = document.querySelector('.control[data-value="original"]') || document.querySelector('.control[data-value="alternative"]');
if (!ref) return;
const label = document.createElement('div'); label.id = 'aw-bp-label'; label.className = 'control active'; label.style.pointerEvents = 'none';
label.innerHTML = '<i style="color:#ec4f4f;" class="icon icon-random"></i> <span>Better Player</span>';
ref.insertAdjacentElement('beforebegin', label);
}
};
hide();
new MutationObserver(hide).observe(document.body, { childList: true, subtree: true });
}
// ── Init ──────────────────────────────────────────────────────────────────
function init() {
cleanupResumeStorage();
injectStyle();
const currentToken = document.querySelector('#player')?.dataset?.id;
if (currentToken) _activeToken = currentToken;
const lastToken = isAutoEpOn() ? loadLastEpisode() : null;
if (lastToken && lastToken !== currentToken) {
loadEpisode(lastToken);
} else if (currentToken) {
saveLastEpisode(currentToken);
getUrlForToken(currentToken).then(url => { if (url) mountPlayer(url); });
} else {
const link = document.querySelector('#downloadLink');
const href = link?.getAttribute('href') || '';
const m = href.match(/[?&]id=(.+)/);
const url = m ? decodeURIComponent(m[1]) : (href.startsWith('http') ? href : null);
if (url) mountPlayer(url);
}
wireControls();
setupPlayerLabel();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 200));
} else {
setTimeout(init, 200);
}
})();