Sauvegarde/restauration chiffrée du profil (.sama) + Next/Prev auto & contrôles clavier adaptatifs
// ==UserScript==
// @name anime-sama Plus
// @namespace http://tampermonkey.net/
// @version 0.1.7.2
// @description Sauvegarde/restauration chiffrée du profil (.sama) + Next/Prev auto & contrôles clavier adaptatifs
// @author MASTERD
// @include /^https?\:\/\/.*\.anime-sama\..*\/.*$/
// @include /^https?\:\/\/.*\anime-sama\..*\/.*$/
// @match *://*.callistanise.com/*
// @match *://*.dingtezuni.com/*
// @match *://*.embed4me.com/*
// @match *://*.oneupload.to/*
// @match *://*.oneupload.net/*
// @match *://*.sendvid.com/*
// @match *://*.sibnet.ru/*
// @match *://*.smoothpre.com/*
// @match *://*.vk.com/*
// @match *://*.vkvideo.ru/*
// @include /^https?\:\/\/.*vidmoly\..*\/.*$/
// @icon https://www.google.com/s2/favicons?sz=64&domain=anime-sama.org
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --------------------------------------------------------------------------
//************** CONFIGURATION GÉNÉRALE **************/
const PREFIX = 'ASP';
const SCRIPT_CONFIG = {
DEBUG: false,
VERSION: '0.1.7.2',
P_DOMAINS: ['anime-sama'],
C_DOMAINS: ['sendvid.com', 'exemple.com']
};
//************** UTILITAIRES **************/
const Utils = {
log: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:LOG]`, 'color:#F47521;font-weight:bold', ...args),
error: (...args) => console.error(`%c[${PREFIX}:ERROR]`, 'color:red;font-weight:bold', ...args),
warn: (...args) => SCRIPT_CONFIG.DEBUG && console.warn(`%c[${PREFIX}:WARN]`, 'color:orange;font-weight:bold', ...args),
info: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:INFO]`, 'color:#2196F3;font-weight:bold', ...args),
skip: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:SKIP]`, 'color:#4CAF50;font-weight:bold', ...args),
msg: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:MSG]`, 'color:#9C27B0;font-weight:bold', ...args),
dom: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:DOM]`, 'color:#FF5722;font-weight:bold', ...args),
key: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:KEY]`, 'color:#00BCD4;font-weight:bold', ...args),
nav: (...args) => SCRIPT_CONFIG.DEBUG && console.log(`%c[${PREFIX}:NAV]`, 'color:#795548;font-weight:bold', ...args),
debounce: (func, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
},
waitForElement: (selector, timeout = 10000) => {
return new Promise((resolve, reject) => {
const el = document.querySelector(selector);
if (el) return resolve(el);
const observer = new MutationObserver((_, obs) => {
const found = document.querySelector(selector);
if (found) {
obs.disconnect();
resolve(found);
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => { observer.disconnect(); reject(new Error(`Timeout: ${selector}`)); }, timeout);
});
},
keyToSymbol: (key) => {
const KEY_SYMBOLS = {
ArrowLeft: '←', ArrowRight: '→', ArrowUp: '↑', ArrowDown: '↓',
Enter: '⏎', Escape: 'Esc', Tab: '⇥', Shift: '⇧',
Control: 'Ctrl', Alt: 'Alt',
Meta: navigator.platform.toUpperCase().includes('MAC') ? '⌘' : '❖',
' ': '────────', Space: '────────',
Backspace: '⌫', Delete: '⌦', Insert: 'Ins',
Home: 'Home', End: 'End', PageUp: 'Pg↑', PageDown: 'Pg↓',
CapsLock: '⇪', Dead: '◌',
AudioVolumeUp: '🔊', AudioVolumeDown: '🔉', AudioVolumeMute: '🔇',
MediaPlayPause: '⏯', MediaTrackNext: '⏭', MediaTrackPrevious: '⏮'
};
if (KEY_SYMBOLS[key]) return KEY_SYMBOLS[key];
if (key.length === 1 && key.match(/[a-z]/i)) return key.toUpperCase();
return key;
}
};
// --------------------------------------------------------------------------
// UI - Choix restauration (Remplacer/Annuler)
function showChoiceDialog() {
return new Promise(resolve => {
const overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000
});
const box = document.createElement('div');
Object.assign(box.style, {
background: '#111', color: '#fff', padding: '20px', borderRadius: '10px',
width: 'min(92vw, 360px)', textAlign: 'center', fontFamily: 'sans-serif',
boxShadow: '0 10px 30px rgba(0,0,0,.4)'
});
box.innerHTML = '<p style="margin-bottom:12px;font-weight:700">Comment voulez-vous restaurer ?</p>';
const mk = (label, code, bg) => {
const b = document.createElement('button');
b.textContent = label;
Object.assign(b.style, {
margin: '0 8px', padding: '8px 12px', border: 'none', borderRadius: '6px',
cursor: 'pointer', fontWeight: 700, background: bg, color: '#fff'
});
b.onclick = () => { document.body.removeChild(overlay); resolve(code); };
return b;
};
box.appendChild(mk('Restaurer (Remplacer)', 'replace', '#e53935'));
box.appendChild(mk('Annuler', 'cancel', '#555'));
overlay.appendChild(box);
document.body.appendChild(overlay);
});
}
// --------------------------------------------------------------------------
// UI - Mot de passe
function showPasswordDialog(mode) {
return new Promise(resolve => {
const overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.55)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10001
});
const box = document.createElement('div');
Object.assign(box.style, {
background: '#111', color: '#fff', padding: '24px', borderRadius: '12px',
width: 'min(92vw, 380px)', fontFamily: 'sans-serif',
boxShadow: '0 10px 30px rgba(0,0,0,.5)'
});
const title = mode === 'backup' ? 'Mot de passe de sauvegarde' : 'Mot de passe de restauration';
box.innerHTML = `
<p style="font-weight:700;margin-bottom:10px;text-align:center">${title}</p>
<input id="asplus-pass" type="password" autocomplete="current-password"
placeholder="(vide = SAMA)"
style="width:100%;padding:8px;border-radius:6px;border:1px solid #333;background:#0b0b0b;color:#fff;margin-bottom:10px"/>
<div id="asplus-risk-banner" style="
display:block;margin:10px 0 12px 0;padding:10px 12px;border-radius:10px;
border:2px solid #ff5252;background:linear-gradient(90deg,#3a0000,#180000);
box-shadow:0 0 0 2px rgba(255,82,82,.25) inset, 0 0 18px rgba(255,82,82,.2);">
<label for="asplus-remember" style="display:flex;gap:12px;align-items:flex-start;cursor:pointer;">
<input id="asplus-remember" type="checkbox" style="transform:scale(1.35);margin-top:2px"/>
<div>
<div style="color:#ff5252;font-weight:900;letter-spacing:.3px;text-transform:uppercase;font-size:14px;">
⚠️ MÉMORISER (LOCAL SANS CHIFFREMENT)
</div>
<div style="color:#ffb3b3;font-size:12px;margin-top:2px;line-height:1.25;">
Le mot de passe sera stocké tel quel dans ce navigateur.
N'activez que si vous comprenez le risque.
</div>
</div>
</label>
</div>`;
const btnRow = document.createElement('div');
btnRow.style.cssText = 'display:flex;gap:10px;justify-content:center;margin-top:8px';
const mkBtn = (label, bg, cb) => {
const b = document.createElement('button');
b.textContent = label;
Object.assign(b.style, {
padding: '8px 16px', border: 'none', borderRadius: '6px',
cursor: 'pointer', fontWeight: 700, background: bg, color: '#fff'
});
b.onclick = cb;
return b;
};
const submit = () => {
const pass = box.querySelector('#asplus-pass').value;
const remember = box.querySelector('#asplus-remember').checked;
document.body.removeChild(overlay);
resolve({ pass, remember });
};
btnRow.appendChild(mkBtn('Valider', '#4caf50', submit));
btnRow.appendChild(mkBtn('Annuler', '#555', () => {
document.body.removeChild(overlay);
resolve({ pass: null, remember: false });
}));
box.appendChild(btnRow);
overlay.appendChild(box);
document.body.appendChild(overlay);
box.querySelector('#asplus-pass').addEventListener('keydown', e => { if (e.key === 'Enter') submit(); });
box.querySelector('#asplus-pass').focus();
});
}
async function getPassphrase(mode) {
const sess = localStorage.getItem('asplus.passphrase');
if (sess && sess.length) return sess;
const { pass, remember } = await showPasswordDialog(mode);
const chosen = (pass && pass.length) ? pass : 'SAMA';
if (remember) localStorage.setItem('asplus.passphrase', chosen);
return chosen;
}
// --------------------------------------------------------------------------
// Sauvegarde / Restauration (AES-GCM 256)
async function backupProfile() {
try {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
data[key] = localStorage.getItem(key);
}
const json = JSON.stringify(data);
const encoder = new TextEncoder();
const passphrase = await getPassphrase('backup');
const iv = crypto.getRandomValues(new Uint8Array(12));
const baseKey = await crypto.subtle.importKey('raw', encoder.encode(passphrase), { name: 'PBKDF2' }, false, ['deriveKey']);
const aesKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: iv, iterations: 100000, hash: 'SHA-256' },
baseKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt']
);
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, encoder.encode(json));
const payload = new Uint8Array(iv.byteLength + encrypted.byteLength);
payload.set(iv, 0);
payload.set(new Uint8Array(encrypted), iv.byteLength);
const blob = new Blob([payload], { type: 'application/vnd.animesama.backup' });
await pickFileToSave(blob);
} catch (e) {
Utils.error('Backup failed:', e);
}
}
async function pickFileToSave(blob) {
if (window.showSaveFilePicker) {
const handle = await window.showSaveFilePicker({
suggestedName: `anime-sama_${new Date().toISOString().slice(0, 10)}.sama`,
types: [{ description: 'Backup Anime-Sama', accept: { 'application/vnd.animesama.backup': ['.sama'] } }]
});
const w = await handle.createWritable();
await w.write(blob);
await w.close();
} else {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `anime-sama_${new Date().toISOString().slice(0, 10)}.sama`;
a.click();
URL.revokeObjectURL(a.href);
}
}
async function restoreProfile() {
try {
let file;
if (window.showOpenFilePicker) {
const [handle] = await window.showOpenFilePicker({
types: [{ description: 'Backup Anime-Sama', accept: { 'application/vnd.animesama.backup': ['.sama'] } }]
});
file = await handle.getFile();
} else {
file = await new Promise(resolve => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.sama';
input.onchange = () => resolve(input.files[0]);
input.click();
});
}
if (!file) return;
const buf = await file.arrayBuffer();
const arr = new Uint8Array(buf);
const iv = arr.slice(0, 12);
const encrypted = arr.slice(12);
const passphrase = await getPassphrase('restore');
const encoder = new TextEncoder();
const baseKey = await crypto.subtle.importKey('raw', encoder.encode(passphrase), { name: 'PBKDF2' }, false, ['deriveKey']);
const aesKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: iv, iterations: 100000, hash: 'SHA-256' },
baseKey, { name: 'AES-GCM', length: 256 }, false, ['decrypt']
);
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, encrypted);
const json = new TextDecoder().decode(decrypted);
const data = JSON.parse(json);
const choice = await showChoiceDialog();
if (choice === 'cancel') return;
if (choice === 'replace') localStorage.clear();
for (const [k, v] of Object.entries(data)) localStorage.setItem(k, v);
location.reload();
} catch (e) {
Utils.error('Restore failed:', e);
alert('Échec de la restauration. Vérifiez le mot de passe.');
}
}
// --------------------------------------------------------------------------
// Profil dropdown (anime-sama uniquement)
function createProfileDropdown() {
const nav = document.querySelector('.asn-nav-desktop');
if (!nav || document.getElementById('tampered-dropdown')) return;
const profileLink = nav.querySelector('a[href="/profil"]');
if (!profileLink) return;
// Créer le wrapper
const wrapper = document.createElement('div');
wrapper.id = 'tampered-dropdown';
wrapper.className = 'relative inline-block text-left';
// Créer le bouton en copiant le contenu du lien Profil
const btn = document.createElement('button');
btn.type = 'button';
// Copier les classes du lien original + ajouts
btn.className = profileLink.className + ' inline-flex items-center cursor-pointer';
// Transférer le contenu HTML du lien (SVG, span, etc.)
btn.innerHTML = profileLink.innerHTML;
// Ajouter la flèche dropdown
btn.insertAdjacentHTML('beforeend', `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1 text-white transform transition-transform duration-200" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.293l3.71-4.06a.75.75 0 111.08 1.04l-4.25 4.656a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
</svg>`);
wrapper.appendChild(btn);
// Menu dropdown
const menu = document.createElement('div');
menu.className = 'absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50';
menu.style.display = 'none';
const mkItem = (icon, label, action) => {
const a = document.createElement('a');
a.href = '#';
a.className = 'block px-4 py-2 text-sm text-white hover:bg-gray-700';
a.textContent = `${icon} ${label}`;
a.addEventListener('click', (e) => { e.preventDefault(); menu.style.display = 'none'; action(); });
return a;
};
// Lien vers profil original
const profItem = document.createElement('a');
profItem.href = '/profil';
profItem.className = 'block px-4 py-2 text-sm text-white hover:bg-gray-700';
profItem.textContent = '👤 Profil';
menu.appendChild(profItem);
menu.appendChild(mkItem('💾', 'Sauvegarder', backupProfile));
menu.appendChild(mkItem('📂', 'Restaurer', restoreProfile));
wrapper.appendChild(menu);
// Toggle
const arrow = btn.querySelector('svg:last-child');
btn.addEventListener('click', (e) => {
e.stopPropagation();
const open = menu.style.display !== 'none';
menu.style.display = open ? 'none' : 'block';
if (arrow) arrow.style.transform = open ? '' : 'rotate(180deg)';
});
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
menu.style.display = 'none';
if (arrow) arrow.style.transform = '';
}
});
// Remplacer le lien par le wrapper
profileLink.replaceWith(wrapper);
}
function ensureProfileDropdown() {
const nav = document.querySelector('.asn-nav-desktop');
if (nav && !document.querySelector('#tampered-dropdown')) createProfileDropdown();
}
let _ensureTimer = null;
function scheduleEnsure() {
if (_ensureTimer) return;
_ensureTimer = setTimeout(() => { _ensureTimer = null; ensureProfileDropdown(); }, 100);
}
if (document.readyState !== 'loading') ensureProfileDropdown();
else window.addEventListener('DOMContentLoaded', ensureProfileDropdown);
const domObserver = new MutationObserver(scheduleEnsure);
domObserver.observe(document.documentElement, { childList: true, subtree: true });
(function hookHistory() {
const fire = () => window.dispatchEvent(new Event('asplus:navigation'));
const _push = history.pushState, _replace = history.replaceState;
history.pushState = function (...a) { const r = _push.apply(this, a); fire(); return r; };
history.replaceState = function (...a) { const r = _replace.apply(this, a); fire(); return r; };
window.addEventListener('popstate', fire);
window.addEventListener('asplus:navigation', scheduleEnsure);
})();
document.addEventListener('visibilitychange', () => { if (!document.hidden) scheduleEnsure(); });
// --------------------------------------------------------------------------
// Réactiver la sélection de texte
(function enableSelection() {
const css = `html, body, * {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
-webkit-touch-callout: default !important;
}`;
const style = document.createElement('style');
style.id = 'asplus-enable-selection';
style.appendChild(document.createTextNode(css));
(document.head || document.documentElement).appendChild(style);
const unblock = e => { e.stopImmediatePropagation(); };
['copy', 'cut', 'paste', 'contextmenu', 'selectstart', 'dragstart']
.forEach(t => document.addEventListener(t, unblock, true));
const fixInline = el => {
if (!el || !el.style) return;
el.style.setProperty('user-select', 'text', 'important');
el.style.setProperty('-webkit-user-select', 'text', 'important');
el.style.setProperty('-moz-user-select', 'text', 'important');
el.style.setProperty('-ms-user-select', 'text', 'important');
el.style.setProperty('-webkit-touch-callout', 'default', 'important');
};
fixInline(document.body);
new MutationObserver(muts => {
for (const m of muts) {
if (m.type === 'attributes' && m.attributeName === 'style') fixInline(m.target);
if (m.addedNodes) m.addedNodes.forEach(n => { if (n.nodeType === 1) fixInline(n); });
}
}).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
})();
// --------------------------------------------------------------------------
//************** INJECTION LECTEUR (parent/iframe) + auto-next + raccourcis **************/
const injectedCode =`
(function () {
//************** LOGGER (miroir de Utils) **************/
var _DEBUG = ${SCRIPT_CONFIG.DEBUG};
var _log = function() { if (!_DEBUG) return; var a = ['%c[ASP]','color:#F47521;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.log.apply(console, a); };
var _warn = function() { if (!_DEBUG) return; var a = ['%c[ASP:WARN]','color:orange;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.warn.apply(console, a); };
var _err = function() { var a = ['%c[ASP:ERROR]','color:red;font-weight:bold'].concat(Array.prototype.slice.call(arguments)); console.error.apply(console, a); };
var CONTROL_DOMAINS = ${JSON.stringify(SCRIPT_CONFIG.C_DOMAINS)};
var PARENT_DOMAINS = ${JSON.stringify(SCRIPT_CONFIG.P_DOMAINS)};
var SITE = location.hostname;
var isTop = (window.self === window.top);
function matchHost(host, pattern) {
if (!host || !pattern) return false;
if (pattern.indexOf('.') !== -1) {
return host === pattern || host.endsWith('.' + pattern);
}
var esc = pattern.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&');
return new RegExp('(?:^|\\\\.)' + esc + '\\\\.', 'i').test(host);
}
var isControlHost = CONTROL_DOMAINS.some(function(p){ return matchHost(SITE, p); });
var isParentHost = PARENT_DOMAINS.some(function(p){ return matchHost(SITE, p); });
var ref = document.referrer || '';
var refHost = '';
try { refHost = new URL(ref).hostname; } catch (_) {}
var refIsParent = PARENT_DOMAINS.some(function(p){ return matchHost(refHost, p); });
var fromAnimeParent = isTop && (isParentHost || !!document.getElementById('playerDF'));
var fromAnimeIframe = !isTop && refIsParent;
_log('[init]', { host:SITE, isTop:isTop, fromAnimeParent:fromAnimeParent, fromAnimeIframe:fromAnimeIframe, refHost:refHost });
var pendingToggle = false;
try {
if (sessionStorage.getItem('asp_pendingToggle') === '1') {
pendingToggle = true;
sessionStorage.removeItem('asp_pendingToggle');
_log('[parent] pendingToggle restauré depuis sessionStorage');
}
} catch(_) {}
var prevEp = window.prevEp || function() { _warn('prevEp non défini'); };
var nextEp = window.nextEp || function() { _warn('nextEp non défini'); };
var EPS = 3;
//**** PARENT: raccourcis clavier ****
function parentKeyHandler(e) {
if (/input|textarea|select/i.test(e.target.tagName)) return;
var iframe = document.getElementById('playerDF');
// Touches navigation : toujours actives
switch (e.key) {
case 'n': case 'N':
e.preventDefault();
pendingToggle = true;
try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(_) {}
nextEp();
return;
case 'p': case 'P':
e.preventDefault();
pendingToggle = true;
try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(_) {}
prevEp();
return;
}
// Touches de contrôle : soumises à CONTROL_DOMAINS
if (!isControlHost) return;
switch (e.key) {
case ' ':
e.preventDefault();
pendingToggle = true;
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'togglePlay' }, '*');
break;
case 'ArrowRight':
e.preventDefault();
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'seekForward', value: 10 }, '*');
break;
case 'ArrowLeft':
e.preventDefault();
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'seekBackward', value: 10 }, '*');
break;
case 'ArrowUp':
e.preventDefault();
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'volumeUp', value: 10 }, '*');
break;
case 'ArrowDown':
e.preventDefault();
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'volumeDown', value: 10 }, '*');
break;
case 'f': case 'F':
e.preventDefault();
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'toggleFullscreen' }, '*');
break;
}
}
//**** PARENT: message handler ****
function messageHandler(e) {
if (!e.data || !e.data.action) return;
_log('[parent] msg:', e.data.action);
switch (e.data.action) {
case 'Istart':
if (pendingToggle) {
pendingToggle = false;
var iframe = document.getElementById('playerDF');
if (iframe && iframe.contentWindow)
iframe.contentWindow.postMessage({ action: 'togglePlay' }, '*');
}
break;
case 'nextEp':
pendingToggle = true;
try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(_) {}
nextEp();
break;
case 'prevEp':
pendingToggle = true;
try { sessionStorage.setItem('asp_pendingToggle', '1'); } catch(_) {}
prevEp();
break;
}
}
//**** IFRAME (<video> natif): raccourcis clavier ****
function iframeKeyHandler(e) {
if (/input|textarea|select/i.test(e.target.tagName)) return;
var v = document.querySelector('video');
// Touches navigation : toujours actives
switch (e.key) {
case 'n': case 'N':
e.preventDefault();
window.parent.postMessage({ action: 'nextEp' }, '*');
return;
case 'p': case 'P':
e.preventDefault();
window.parent.postMessage({ action: 'prevEp' }, '*');
return;
}
// Touches de contrôle : soumises à CONTROL_DOMAINS
if (!isControlHost || !v) return;
switch (e.key) {
case ' ':
e.preventDefault();
break;
case 'ArrowRight':
e.preventDefault();
v.currentTime = Math.min(v.duration, v.currentTime + 10);
break;
case 'ArrowLeft':
e.preventDefault();
v.currentTime = Math.max(0, v.currentTime - 10);
break;
case 'ArrowUp':
e.preventDefault();
v.volume = Math.min(1, v.volume + 0.1);
break;
case 'ArrowDown':
e.preventDefault();
v.volume = Math.max(0, v.volume - 0.1);
break;
case 'f': case 'F':
e.preventDefault();
if (document.fullscreenElement) document.exitFullscreen();
else if (v.requestFullscreen) v.requestFullscreen();
break;
}
}
//**** IFRAME: toggle play/pause ****
function togglePlayPauseAfterDelay() {
setTimeout(function() {
var v = document.querySelector('video');
if (!v) return;
if (v.paused) v.play().catch(function(){});
else v.pause();
}, 300);
}
//**** IFRAME (<video> natif): détection fin ****
function addVideoEndDetectors() {
var sent = false;
function sendNext(src) {
if (sent) return;
sent = true;
_log('[iframe] → nextEp via', src);
window.parent.postMessage({ action: 'nextEp' }, '*');
}
function attachToVideo() {
var v = document.querySelector('video');
if (!v) return;
v.addEventListener('ended', function() { sendNext('ended'); });
var lastT = -1;
var stallTimer = setInterval(function() {
if (sent) { clearInterval(stallTimer); return; }
var d = v.duration;
if (!isFinite(d) || !d) return;
var now = v.currentTime;
if (now === lastT && (d - now) <= EPS && v.paused) {
sendNext('stall-end');
clearInterval(stallTimer);
}
lastT = now;
}, 1000);
}
if (document.querySelector('video')) attachToVideo();
else {
var obs = new MutationObserver(function() {
if (document.querySelector('video')) {
obs.disconnect();
attachToVideo();
}
});
obs.observe(document.body, { childList: true, subtree: true });
}
}
//**********************
//* IFRAME JW PLAYER (VidMoly, etc.)
//**********************
function detectJWPlayer() {
try {
var p = jwplayer();
return (p && typeof p.getState === 'function') ? p : null;
} catch (_) { return null; }
}
function waitForJWPlayer(timeout) {
timeout = timeout || 10000;
return new Promise(function(resolve, reject) {
var t0 = Date.now();
var check = setInterval(function() {
var p = detectJWPlayer();
if (p) { clearInterval(check); resolve(p); }
if (Date.now() - t0 > timeout) { clearInterval(check); reject(new Error('JW timeout')); }
}, 300);
});
}
function forcePlayJW(player) {
_log('[JW] forcePlay, state:', player.getState());
var overlays = [
'.jw-display-icon-container',
'.jw-icon-display',
'.jw-controls .jw-icon-playback',
'.vjs-big-play-button'
];
for (var i = 0; i < overlays.length; i++) {
var el = document.querySelector(overlays[i]);
if (el) {
_log('[JW] click overlay:', overlays[i]);
el.click();
break;
}
}
try { player.play(); } catch(e) { _err('[JW] play():', e); }
setTimeout(function() {
if (player.getState() !== 'playing') {
_log('[JW] retry play');
try { player.play(); } catch(_) {}
}
}, 500);
setTimeout(function() {
if (player.getState() !== 'playing') {
_log('[JW] retry2 play + fallback <video>');
try { player.play(); } catch(_) {}
var v = document.querySelector('video');
if (v && v.paused) v.play().catch(function(){});
}
}, 1500);
}
function jwKeyHandler(player, e) {
if (/input|textarea|select/i.test(e.target.tagName)) return;
// Touches navigation : toujours actives
switch (e.key) {
case 'n': case 'N':
e.preventDefault();
window.parent.postMessage({ action: 'nextEp' }, '*');
return;
case 'p': case 'P':
e.preventDefault();
window.parent.postMessage({ action: 'prevEp' }, '*');
return;
}
// Touches de contrôle : soumises à CONTROL_DOMAINS
if (!isControlHost) return;
switch (e.key) {
case ' ':
e.preventDefault();
player.getState() === 'playing' ? player.pause() : player.play();
break;
case 'ArrowRight':
e.preventDefault();
player.seek(player.getPosition() + 10);
break;
case 'ArrowLeft':
e.preventDefault();
player.seek(Math.max(0, player.getPosition() - 10));
break;
case 'ArrowUp':
e.preventDefault();
player.setVolume(Math.min(100, player.getVolume() + 10));
break;
case 'ArrowDown':
e.preventDefault();
player.setVolume(Math.max(0, player.getVolume() - 10));
break;
case 'f': case 'F':
e.preventDefault();
player.setFullscreen(!player.getFullscreen());
break;
}
}
function attachJWEndDetectors(player) {
var sent = false;
function sendNext(src) {
if (sent) return;
sent = true;
_log('[JW] → nextEp via', src);
window.parent.postMessage({ action: 'nextEp' }, '*');
}
// 1) JW complete event
player.on('complete', function() { sendNext('jw-complete'); });
// 2) Fallback <video> ended
var v = document.querySelector('video');
if (v) {
v.addEventListener('ended', function() { sendNext('video-ended'); });
}
// 3) Détection fin : remaining <= 0.5 pendant 2 checks consécutifs
var endCount = 0;
var stallInterval = setInterval(function() {
if (sent) { clearInterval(stallInterval); return; }
try {
var dur = player.getDuration();
var pos = player.getPosition();
if (!isFinite(dur) || dur <= 0) return;
var remaining = dur - pos;
if (remaining <= 0.5) {
endCount++;
_log('[JW] fin proche, count:', endCount, 'remaining:', remaining);
if (endCount >= 2) {
sendNext('jw-end-detect');
clearInterval(stallInterval);
}
} else {
endCount = 0;
}
} catch (_) {}
}, 1000);
}
function attachJWIframeHandlers(player) {
_log('[JW] context=iframe, state:', player.getState());
document.addEventListener('keydown', function(e) { jwKeyHandler(player, e); }, true);
window.addEventListener('message', function(e) {
if (!e.data || !e.data.action) return;
_log('[JW] msg reçu:', e.data.action);
switch (e.data.action) {
case 'togglePlay':
try { window.focus(); } catch(_) {}
forcePlayJW(player);
break;
case 'seekForward':
player.seek(player.getPosition() + (e.data.value || 10));
break;
case 'seekBackward':
player.seek(Math.max(0, player.getPosition() - (e.data.value || 10)));
break;
case 'volumeUp':
player.setVolume(Math.min(100, player.getVolume() + (e.data.value || 10)));
break;
case 'volumeDown':
player.setVolume(Math.max(0, player.getVolume() - (e.data.value || 10)));
break;
case 'toggleFullscreen':
player.setFullscreen(!player.getFullscreen());
break;
}
});
attachJWEndDetectors(player);
_log('[JW] → Istart');
window.parent.postMessage({ action: 'Istart' }, '*');
}
//**********************
//* ROUTAGE PARENT / IFRAME
//**********************
function attachParentHandlers() {
_log('context=parent');
document.addEventListener('keydown', parentKeyHandler, true);
window.addEventListener('message', messageHandler);
}
function attachIframeHandlers() {
_log('context=iframe');
// Détection synchrone JW
var jwp = detectJWPlayer();
if (jwp) {
attachJWIframeHandlers(jwp);
return;
}
// Envoyer Istart + attacher video natif TOUT DE SUITE
document.addEventListener('keydown', iframeKeyHandler, true);
window.addEventListener('message', function(e) {
if (!e.data || !e.data.action) return;
_log('[iframe] msg reçu:', e.data.action);
var v;
switch (e.data.action) {
case 'togglePlay':
try { window.focus(); } catch(_) {}
v = document.querySelector('video');
if (v) v.focus();
togglePlayPauseAfterDelay();
break;
case 'seekForward':
v = document.querySelector('video');
if (v) v.currentTime = Math.min(v.duration, v.currentTime + (e.data.value || 10));
break;
case 'seekBackward':
v = document.querySelector('video');
if (v) v.currentTime = Math.max(0, v.currentTime - (e.data.value || 10));
break;
case 'volumeUp':
v = document.querySelector('video');
if (v) v.volume = Math.min(1, v.volume + (e.data.value || 10) / 100);
break;
case 'volumeDown':
v = document.querySelector('video');
if (v) v.volume = Math.max(0, v.volume - (e.data.value || 10) / 100);
break;
case 'toggleFullscreen':
v = document.querySelector('video');
if (v) {
if (document.fullscreenElement) document.exitFullscreen();
else if (v.requestFullscreen) v.requestFullscreen();
}
break;
}
});
addVideoEndDetectors();
// Envoyer Istart dès que possible
function sendIstart() {
_log('[iframe] → Istart');
window.parent.postMessage({ action: 'Istart' }, '*');
}
if (document.readyState === 'complete') setTimeout(sendIstart, 100);
else window.addEventListener('load', function() { setTimeout(sendIstart, 100); });
// Tenter JW en arrière-plan (upgrade si trouvé)
waitForJWPlayer(3000).then(function(player) {
_log('[iframe] JW détecté tardivement, upgrade');
document.removeEventListener('keydown', iframeKeyHandler, true);
attachJWIframeHandlers(player);
}).catch(function() {
_log('[iframe] Confirmé: pas de JW, video natif actif');
});
}
if (fromAnimeParent) attachParentHandlers();
else if (fromAnimeIframe) attachIframeHandlers();
else {
var hasPlayerIframeId = !!document.getElementById('playerDF');
var hasVideo = !!document.querySelector('video');
_warn('context unknown -> fallback', { hasPlayerIframeId:hasPlayerIframeId, hasVideo:hasVideo });
if (isTop && hasPlayerIframeId) attachParentHandlers();
else if (!isTop && hasVideo) attachIframeHandlers();
else if (!isTop) attachIframeHandlers();
else _warn('fallback -> nothing to attach');
}
})();
`;
const script = document.createElement('script');
script.defer = true;
script.textContent = injectedCode;
document.documentElement.appendChild(script);
script.remove();
})();