stream-fusion-better/stream_fusion/static/app.js
2026-04-01 23:24:44 -04:00

1172 lines
No EOL
48 KiB
JavaScript

// JAVASCRIPT POUR PAGE PRINCIPALE
// app.js
// ═══ DROPDOWN POUR PRIORITÉ INDEXEURS ═══
var _prioMeta = {
'priority_private': { label: 'Prioritaire', desc: 'toujours interrogé', rank: 'P1', css: 'prio-priority' },
'intermediary_private': { label: 'Intermédiaire', desc: 'si résultats insuffisants', rank: 'P2', css: 'prio-intermediary' },
'fallback_private': { label: 'Fallback', desc: 'dernier recours', rank: 'P3', css: 'prio-fallback' }
};
function updatePriorityLevel(selectEl) {
var wrap = selectEl.closest('.priority-select-wrap');
if (!wrap) return;
var val = selectEl.value;
if (val.indexOf('priority') === 0) wrap.setAttribute('data-level', 'priority');
else if (val.indexOf('intermediary') === 0) wrap.setAttribute('data-level', 'intermediary');
else wrap.setAttribute('data-level', 'fallback');
}
function initPriorityDropdowns() {
document.querySelectorAll('.priority-select-wrap select').forEach(function(sel) {
var wrap = sel.closest('.priority-select-wrap');
if (wrap.querySelector('.prio-trigger')) return; // already initialized
var trigger = document.createElement('div');
trigger.className = 'prio-trigger';
trigger.setAttribute('tabindex', '0');
var dropdown = document.createElement('div');
dropdown.className = 'prio-dropdown';
Array.from(sel.options).forEach(function(opt) {
var meta = _prioMeta[opt.value];
if (!meta) return;
var optEl = document.createElement('div');
optEl.className = 'prio-option ' + meta.css;
optEl.setAttribute('data-value', opt.value);
optEl.innerHTML = '<span class="prio-dot"></span>'
+ '<span><span class="prio-label">' + meta.label + '</span> <span class="prio-desc">— ' + meta.desc + '</span></span>'
+ '<span class="prio-rank">' + meta.rank + '</span>';
if (opt.selected) optEl.classList.add('selected');
optEl.addEventListener('click', function(e) {
e.stopPropagation();
sel.value = opt.value;
sel.dispatchEvent(new Event('change', { bubbles: true }));
updateTrigger(wrap, opt.value);
dropdown.querySelectorAll('.prio-option').forEach(function(o) { o.classList.remove('selected'); });
optEl.classList.add('selected');
closeDropdown(wrap);
});
dropdown.appendChild(optEl);
});
wrap.appendChild(trigger);
wrap.appendChild(dropdown);
var activeValue = sel.value;
if (!activeValue || !_prioMeta[activeValue]) {
var defaultOpt = sel.querySelector('option[selected]') || sel.options[0];
if (defaultOpt) {
sel.value = defaultOpt.value;
activeValue = defaultOpt.value;
}
}
dropdown.querySelectorAll('.prio-option').forEach(function(o) {
o.classList.toggle('selected', o.getAttribute('data-value') === activeValue);
});
updateTrigger(wrap, activeValue);
updatePriorityLevel(sel);
trigger.addEventListener('click', function(e) {
e.stopPropagation();
var isOpen = trigger.classList.contains('open');
closeAllDropdowns();
if (!isOpen) {
trigger.classList.add('open');
dropdown.classList.add('open');
}
});
});
}
function updateTrigger(wrap, value) {
var trigger = wrap.querySelector('.prio-trigger');
var meta = _prioMeta[value];
if (!trigger || !meta) return;
trigger.innerHTML = '<span class="prio-dot"></span>'
+ '<span class="prio-label">' + meta.label + ' <span style="color:var(--text-muted);font-weight:400;">— ' + meta.desc + '</span></span>'
+ '<span class="prio-rank">' + meta.rank + '</span>'
+ '<svg class="prio-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
// couleur
trigger.className = 'prio-trigger ' + meta.css;
if (wrap.querySelector('.prio-dropdown.open')) trigger.classList.add('open');
}
function closeDropdown(wrap) {
var t = wrap.querySelector('.prio-trigger');
var d = wrap.querySelector('.prio-dropdown');
if (t) t.classList.remove('open');
if (d) d.classList.remove('open');
}
function closeAllDropdowns() {
document.querySelectorAll('.priority-select-wrap').forEach(function(w) { closeDropdown(w); });
}
document.addEventListener('click', function() { closeAllDropdowns(); });
// ═══ YGG PRIORITY FLAIR ═══
function updateYggPriorityFlair() {
var cb = document.getElementById('yggflixPriority');
var flair = document.getElementById('yggPriorityFlair');
if (!cb || !flair) return;
var label = flair.querySelector('.flair-label');
var bubble = flair.querySelector('.info-tip-bubble');
if (!label || !bubble) return;
if (cb.checked) {
flair.className = 'status-flair active info-tip-wrap';
label.textContent = 'Actif';
bubble.innerHTML = '<strong>Recherche prioritaire activée</strong><br>YGG Relay est interrogé en <strong>Phase 1</strong>, en même temps que les indexeurs prioritaires.';
} else {
flair.className = 'status-flair inactive info-tip-wrap';
label.textContent = 'Inactif';
bubble.innerHTML = '<strong>Recherche prioritaire désactivée</strong><br>YGG Relay sera interrogé <strong>en dernier</strong>, après tous les indexeurs privés.';
}
}
// ═══ CARTES ═══
function toggleCard(headerEl) {
var card = headerEl.closest('.card');
var children = card.querySelectorAll(':scope > :not(.card-header)');
var isCollapsing = !card.classList.contains('collapsed');
if (isCollapsing) {
// Set explicit height from current size so transition has a real start value
children.forEach(function(el) {
el.style.maxHeight = el.scrollHeight + 'px';
el.offsetHeight; // force reflow
});
card.classList.add('collapsed');
children.forEach(function(el) {
el.style.maxHeight = '0';
});
} else {
card.classList.remove('collapsed');
children.forEach(function(el) {
el.style.maxHeight = el.scrollHeight + 'px';
});
// After transition ends, remove inline max-height so content can reflow naturally
var onDone = function() {
children.forEach(function(el) {
el.style.maxHeight = '';
});
children[0] && children[0].removeEventListener('transitionend', onDone);
};
if (children[0]) children[0].addEventListener('transitionend', onDone, { once: true });
}
}
// ═══ VALIDATION CLÉ API EN LIVE ═══
function validateKeyLive(val) {
var status = document.getElementById('apiKeyStatus');
var msg = document.getElementById('apiKeyMsg');
var msgText = document.getElementById('apiKeyMsgText');
var errDiv = document.getElementById('apiKeyError');
if (!val) {
status.className = 'api-key-status';
msg.className = 'api-key-msg';
return;
}
if (val !== val.trim() || val.indexOf(' ') !== -1) {
status.className = 'api-key-status invalid';
msg.className = 'api-key-msg show invalid';
msgText.textContent = 'Format invalide — une clé UUID v4 est requise';
return;
}
var isValid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(val);
if (isValid) {
status.className = 'api-key-status';
msg.className = 'api-key-msg';
if (errDiv) errDiv.classList.add('hidden');
} else {
status.className = 'api-key-status invalid';
msg.className = 'api-key-msg show invalid';
msgText.textContent = 'Format invalide — une clé UUID v4 est requise';
}
}
function togglePw(btn) {
var input = btn.parentElement.querySelector('input');
if (input.type === 'password') {
input.type = 'text';
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" y1="2" x2="22" y2="22"/></svg>';
} else {
input.type = 'password';
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>';
}
}
// ═══ PLUS DE TOASTS ═══
var _iconSuccess = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>';
var _iconError = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
var _iconWarning = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
function showToast(msg, type, duration) {
var t = document.getElementById('toast');
var m = document.getElementById('toastMsg');
var icon = document.getElementById('toastIcon');
m.textContent = msg || 'Succès !';
t.className = 'toast';
if (type === 'error') {
t.classList.add('error');
icon.innerHTML = _iconError;
} else if (type === 'warning') {
t.classList.add('warning');
icon.innerHTML = _iconWarning;
} else {
icon.innerHTML = _iconSuccess;
}
t.classList.add('show');
clearTimeout(t._tid);
t._tid = setTimeout(function() { t.classList.remove('show'); }, duration || 3500);
}
// ═══ GENERATEUR MANIFEST ═══
var _manifestUrl = '';
async function generateManifest() {
if (typeof window.clearValidationErrors === 'function') window.clearValidationErrors();
document.getElementById('manifestResult').classList.remove('show');
_manifestUrl = '';
// getLink est async — on l'appelle avec 'none' pour obtenir l'URL sans side effect
var manifestUrl = await getLink('none');
if (!manifestUrl) return;
_manifestUrl = manifestUrl;
// TEST MANIFEST - VÉRIFICATION CLÉ API
var btn = document.getElementById('generateBtn');
var btnOrigHTML = btn.innerHTML;
btn.classList.add('loading');
btn.innerHTML = '<div class="btn-spinner"></div> Vérification…';
fetch(_manifestUrl, { method: 'GET' })
.then(function(resp) {
btn.classList.remove('loading');
btn.innerHTML = btnOrigHTML;
if (resp.status === 403) {
// Pas bon
showToast('Clé API rejetée par le serveur', 'error', 4500);
if (typeof window.clearValidationErrors === 'function') {
window.clearValidationErrors();
window._errorSteps.add(0);
var dot = window._wizDots[0];
if (dot) { dot.classList.add('error'); dot.innerHTML = window._dotIcons[0]; }
window.showFieldError('ApiKey', 'Clé API invalide — le serveur a retourné une erreur 403', 0);
window.goToStep(0);
}
return;
}
if (!resp.ok && resp.status !== 200) {
showToast('Manifest généré (le serveur n\'a pas pu être vérifié)', 'warning', 4000);
}
// SUCCÈS
document.getElementById('manifestUrl').value = _manifestUrl;
document.getElementById('manifestResult').classList.add('show');
setTimeout(function() {
document.getElementById('manifestResult').scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
})
.catch(function(err) {
// ERREUR RÉSEAU
btn.classList.remove('loading');
btn.innerHTML = btnOrigHTML;
showToast('Impossible de vérifier la clé — manifest généré quand même', 'warning', 4000);
document.getElementById('manifestUrl').value = _manifestUrl;
document.getElementById('manifestResult').classList.add('show');
setTimeout(function() {
document.getElementById('manifestResult').scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
});
}
function copyManifestUrl() {
var url = document.getElementById('manifestUrl').value;
if (!url) return;
navigator.clipboard.writeText(url).then(function() {
showToast('Manifest copié dans le presse-papier !', 'success', 3000);
var btn = document.querySelector('.manifest-url-copy');
btn.style.background = 'rgba(34,197,94,0.2)';
setTimeout(function() { btn.style.background = ''; }, 600);
}, function() {
showToast('Erreur lors de la copie', 'error', 3000);
});
}
function installToStremio() {
var url = document.getElementById('manifestUrl').value;
if (!url) return;
var stremioUrl = url.replace(/^https?:\/\//, 'stremio://');
window.open(stremioUrl, '_blank');
}
// Fuck les alertes moi j'veux des toasts
(function() {
var _origAlert = window.alert;
window.alert = function(msg) {
if (!msg) return;
var s = String(msg).toLowerCase();
if (s.indexOf('copied') !== -1 || s.indexOf('copié') !== -1 || s.indexOf('clipboard') !== -1) {
showToast('Manifest copié dans le presse-papier !', 'success', 3000);
}
else if (s.indexOf('erreur') !== -1 || s.indexOf('error') !== -1 || s.indexOf('invalide') !== -1 || s.indexOf('invalid') !== -1) {
showToast(msg, 'error', 4500);
if (typeof window.highlightValidationErrors === 'function') {
window.highlightValidationErrors(msg);
}
}
else if (s.indexOf('please fill') !== -1 || s.indexOf('required') !== -1 || s.indexOf('veuillez') !== -1 || s.indexOf('manquant') !== -1) {
showToast(msg, 'warning', 5000);
if (typeof window.highlightValidationErrors === 'function') {
window.highlightValidationErrors(msg);
}
}
else if (s.indexOf('expiré') !== -1 || s.indexOf('expired') !== -1 || s.indexOf('réessayer') !== -1) {
showToast(msg, 'warning', 5000);
}
else if (s.indexOf('passkey') !== -1 || s.indexOf('caractère') !== -1) {
showToast(msg, 'error', 4500);
if (typeof window.highlightValidationErrors === 'function') {
window.highlightValidationErrors(msg);
}
}
else {
showToast(msg, 'warning', 4000);
}
};
})();
function syncCred(prefix) {
var email = document.getElementById(prefix + '_email');
var pass = document.getElementById(prefix + '_pass');
var hidden = document.getElementById(prefix + '_credentials');
if (email && pass && hidden) {
var e = email.value.trim();
var p = pass.value;
hidden.value = (e || p) ? e + ':' + p : '';
}
}
function loadCred(prefix) {
var hidden = document.getElementById(prefix + '_credentials');
var email = document.getElementById(prefix + '_email');
var pass = document.getElementById(prefix + '_pass');
if (hidden && email && pass && hidden.value && hidden.value.indexOf(':') !== -1) {
var parts = hidden.value.split(':');
email.value = parts[0] || '';
pass.value = parts.slice(1).join(':') || '';
}
}
document.addEventListener('DOMContentLoaded', function() {
var ygg = document.getElementById('yggflix');
if (ygg) {
Object.defineProperty(ygg, 'checked', {
get: function() { return true; },
set: function() {},
configurable: true
});
Object.defineProperty(ygg, 'disabled', {
get: function() { return false; },
set: function() {},
configurable: true
});
}
setTimeout(function() {
loadCred('offcloud');
loadCred('pikpak');
var apiKey = document.getElementById('ApiKey');
if (apiKey) {
if (apiKey.value) validateKeyLive(apiKey.value);
// Reinforce live validation via addEventListener (in case inline oninput is stale)
apiKey.addEventListener('input', function() {
validateKeyLive(this.value);
});
}
}, 150);
// NAV
var _wizSteps = document.querySelectorAll('.wizard-step');
var _wizDots = document.querySelectorAll('.wiz-dot');
var _wizLines = document.querySelectorAll('.wiz-line');
var _wizTitle = document.getElementById('wizTitle');
var _wizSub = document.getElementById('wizSubtitle');
var _wizProgress = document.getElementById('wizProgress');
var _wizArrowPrev = document.getElementById('wizArrowPrev');
var _wizArrowNext = document.getElementById('wizArrowNext');
var _wizPrev = document.getElementById('wizPrev');
var _wizNext = document.getElementById('wizNext');
var _wizCounter = document.getElementById('wizCounter');
var _currentStep = 0;
var _prevStep = 0;
var _totalSteps = _wizSteps.length;
var _dotIcons = [];
_wizDots.forEach(function(d) { _dotIcons.push(d.innerHTML); });
var _checkSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>';
var _errorSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
var _stepMeta = [
{ title: "Authentification", sub: "Entrez votre clé d'accès pour commencer" },
{ title: "Services de Débridage", sub: "Choisissez et configurez vos débrideurs" },
{ title: "Sources de Torrents", sub: "Configurez vos fournisseurs de recherche" },
{ title: "Services & Contenu", sub: "Personnalisez les services, catalogues et métadonnées" },
{ title: "Tri & Exclusions", sub: "Filtrez les résultats selon vos préférences" },
{ title: "Langues & Limites", sub: "Définissez vos langues et limites de résultats" },
{ title: "Installation", sub: "Générez et installez votre addon" },
];
var _fieldStepMap = {
'API Key': { step: 0, field: 'ApiKey', label: 'Clé API Stream-Fusion manquante ou invalide' },
'Real-Debrid Account Connection': { step: 1, field: 'rd_token_info_div', label: 'Connexion Real-Debrid requise' },
'AllDebrid Account Connection': { step: 1, field: 'ad_token_info_div', label: 'Connexion AllDebrid requise' },
'TorBox Account Connection': { step: 1, field: 'tb_token_info_div', label: 'Connexion TorBox requise' },
'Premiumize Account Connection': { step: 1, field: 'pm_token_info_div', label: 'Connexion Premiumize requise' },
'Debrid-Link API Key': { step: 1, field: 'debridlink_api_key', label: 'Clé API Debrid-Link manquante' },
'EasyDebrid API Key': { step: 1, field: 'easydebrid_api_key', label: 'Clé API EasyDebrid manquante' },
'Offcloud Credentials': { step: 1, field: 'offcloud_credentials_div', label: 'Identifiants Offcloud manquants' },
'PikPak Credentials': { step: 1, field: 'pikpak_credentials_div', label: 'Identifiants PikPak manquants' },
'Cache URL': { step: 3, field: 'cacheUrl', label: 'URL de cache manquante' },
'C411 API Key': { step: 2, field: 'c411ApiKey', label: 'Clé API C411 manquante' },
'Torr9 API Key': { step: 2, field: 'torr9ApiKey', label: 'Clé API Torr9 manquante' },
'LaCale API Key': { step: 2, field: 'lacaleApiKey', label: 'Clé API La-Cale manquante' },
'Generation Free API Key': { step: 2, field: 'generationfreeApiKey', label: 'Clé API Generation Free manquante' },
'ABN API Key': { step: 2, field: 'abnApiKey', label: 'Clé API ABN manquante' },
'G3MINI API Key': { step: 2, field: 'g3miniApiKey', label: 'Clé API G3MINI manquante' },
'TheOldSchool API Key': { step: 2, field: 'theoldschoolApiKey', label: 'Clé API TheOldSchool manquante' },
'Languages': { step: 5, field: 'languageCheckBoxes', label: 'Sélectionnez au moins une langue' },
'StremThru URL': { step: 3, field: 'stremthru_url', label: 'URL StremThru manquante' },
};
var _errorSteps = new Set();
function clearValidationErrors() {
_errorSteps.clear();
_wizDots.forEach(function(d) { d.classList.remove('error'); });
document.querySelectorAll('.field-error-hint').forEach(function(el) { el.remove(); });
}
function showFieldError(fieldId, message, stepIdx) {
var el = document.getElementById(fieldId);
if (!el) return;
var target = el.closest('.fi-group') || el.closest('.tog-row') || el.closest('.cred-pair') || el.parentElement;
if (!target) return;
if (target.querySelector('.field-error-hint')) return;
var hint = document.createElement('div');
hint.className = 'field-error-hint';
hint.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>' + message;
target.appendChild(hint);
function removeHint() {
if (hint.parentElement) hint.remove();
el.removeEventListener('input', removeHint);
el.removeEventListener('change', removeHint);
if (typeof stepIdx === 'number') {
var stepEl = document.querySelector('[data-wizard-step="' + stepIdx + '"]');
if (stepEl && !stepEl.querySelector('.field-error-hint')) {
_errorSteps.delete(stepIdx);
updateWizard(true);
}
}
}
el.addEventListener('input', removeHint);
el.addEventListener('change', removeHint);
var siblings = target.querySelectorAll('input');
siblings.forEach(function(sib) {
if (sib !== el) {
sib.addEventListener('input', removeHint, { once: true });
sib.addEventListener('change', removeHint, { once: true });
}
});
}
function highlightValidationErrors(alertMsg) {
clearValidationErrors();
var stepsWithErrors = new Set();
// Sort keys longest-first so "C411 API Key" is consumed before "API Key"
var keys = Object.keys(_fieldStepMap).sort(function(a, b) { return b.length - a.length; });
var remaining = alertMsg;
keys.forEach(function(key) {
if (remaining.indexOf(key) !== -1) {
var info = _fieldStepMap[key];
stepsWithErrors.add(info.step);
_errorSteps.add(info.step);
showFieldError(info.field, info.label, info.step);
// Strip matched key so shorter substrings don't false-positive
while (remaining.indexOf(key) !== -1) {
remaining = remaining.replace(key, '');
}
}
});
stepsWithErrors.forEach(function(stepIdx) {
var dot = _wizDots[stepIdx];
if (dot) {
dot.classList.remove('active', 'done');
dot.classList.add('error');
dot.innerHTML = _dotIcons[stepIdx];
}
});
if (stepsWithErrors.size > 0) {
var first = Math.min.apply(null, Array.from(stepsWithErrors));
goToStep(first);
}
}
function updateWizard(skipAnim) {
var direction = _currentStep > _prevStep ? 'slide-left' : 'slide-right';
_wizSteps.forEach(function(s, i) {
s.classList.remove('active', 'slide-left', 'slide-right');
if (i === _currentStep) {
s.classList.add('active', direction);
if (!skipAnim) {
s.style.animation = 'none';
s.offsetHeight;
s.style.animation = '';
}
}
});
_wizDots.forEach(function(d, i) {
d.classList.remove('active', 'done', 'error');
if (i === _currentStep) {
d.classList.add('active');
d.innerHTML = _dotIcons[i];
} else if (i < _currentStep) {
d.classList.add('done');
d.innerHTML = _dotIcons[i];
} else {
d.innerHTML = _dotIcons[i];
}
});
_errorSteps.forEach(function(stepIdx) {
if (stepIdx === _currentStep) return;
var d = _wizDots[stepIdx];
if (d) {
d.classList.remove('active', 'done');
d.classList.add('error');
d.innerHTML = _dotIcons[stepIdx];
}
});
_wizLines.forEach(function(l, i) {
l.classList.toggle('done', i < _currentStep);
});
if (_stepMeta[_currentStep]) {
_wizTitle.textContent = _stepMeta[_currentStep].title;
_wizSub.textContent = _stepMeta[_currentStep].sub;
}
var pct = ((_currentStep + 1) / _totalSteps) * 100;
_wizProgress.style.width = pct + '%';
// FLÈCHES
_wizArrowPrev.disabled = _currentStep === 0;
_wizArrowNext.disabled = _currentStep === _totalSteps - 1;
// BOUTONS
_wizPrev.disabled = _currentStep === 0;
_wizPrev.style.visibility = _currentStep === 0 ? 'hidden' : 'visible';
_wizNext.style.display = _currentStep === _totalSteps - 1 ? 'none' : '';
// COMPTEUR
_wizCounter.textContent = (_currentStep + 1) + ' / ' + _totalSteps;
if (!skipAnim) window.scrollTo({ top: 0, behavior: 'smooth' });
}
window.goToStep = function(n) {
if (n < 0 || n >= _totalSteps) return;
_prevStep = _currentStep;
_currentStep = n;
updateWizard();
};
window.nextStep = function() { goToStep(_currentStep + 1); };
window.prevStep = function() { goToStep(_currentStep - 1); };
window.clearValidationErrors = clearValidationErrors;
window.highlightValidationErrors = highlightValidationErrors;
window.showFieldError = showFieldError;
window._wizDots = _wizDots;
window._errorSteps = _errorSteps;
window._errorSvg = _errorSvg;
window._dotIcons = _dotIcons;
// Clear error hints when a provider toggle is unchecked
var _toggleFieldMap = {
'c411': 'c411ApiKey',
'torr9': 'torr9ApiKey',
'lacale': 'lacaleApiKey',
'generationfree': 'generationfreeApiKey',
'abn': 'abnApiKey',
'g3mini': 'g3miniApiKey',
'theoldschool': 'theoldschoolApiKey',
'debrid_rd': 'rd_token_info_div',
'debrid_ad': 'ad_token_info_div',
'debrid_tb': 'tb_token_info_div',
'debrid_pm': 'pm_token_info_div',
'debrid_dl': 'debridlink_api_key',
'debrid_ed': 'easydebrid_api_key',
'debrid_oc': 'offcloud_credentials_div',
'debrid_pk': 'pikpak_credentials_div',
'stremthru_enabled': 'stremthru_url'
};
Object.keys(_toggleFieldMap).forEach(function(togId) {
var tog = document.getElementById(togId);
if (!tog) return;
tog.addEventListener('change', function() {
if (tog.checked) return;
var fieldId = _toggleFieldMap[togId];
var el = document.getElementById(fieldId);
if (!el) return;
var target = el.closest('.fi-group') || el.closest('.tog-row') || el.closest('.cred-pair') || el.parentElement;
if (!target) return;
var hints = target.querySelectorAll('.field-error-hint');
hints.forEach(function(h) { h.remove(); });
// Find which step this field belongs to and clear error if no more hints in that step
var info = _fieldStepMap[Object.keys(_fieldStepMap).filter(function(k) { return _fieldStepMap[k].field === fieldId; })[0]];
if (info) {
var stepEl = document.querySelector('[data-wizard-step="' + info.step + '"]');
if (stepEl && !stepEl.querySelector('.field-error-hint')) {
_errorSteps.delete(info.step);
updateWizard(true);
}
}
});
});
// Show deprecation modal when Real-Debrid is toggled on
var _rdCheckbox = document.getElementById('debrid_rd');
var _rdModal = document.getElementById('rd-modal');
var _rdModalClose = document.getElementById('rd-modal-close');
if (_rdCheckbox && _rdModal) {
_rdCheckbox.addEventListener('change', function() {
if (_rdCheckbox.checked) {
_rdModal.classList.add('show');
}
});
if (_rdModalClose) {
_rdModalClose.addEventListener('click', function() {
_rdModal.classList.remove('show');
});
}
_rdModal.addEventListener('click', function(e) {
if (e.target === _rdModal) _rdModal.classList.remove('show');
});
}
// Auto-uncheck "Personnaliser l'ordre" when no debrid services remain
var _debridOpts = document.getElementById('debridDownloaderOptions');
// Click anywhere on a radio row to select it
if (_debridOpts) {
_debridOpts.addEventListener('click', function(e) {
var row = e.target.closest('#debridDownloaderOptions > div');
if (!row) return;
var radio = row.querySelector('input[type="radio"]');
if (radio && !radio.checked) {
radio.checked = true;
radio.dispatchEvent(new Event('change', { bubbles: true }));
}
});
}
var _debridOrderCb = document.getElementById('debrid_order');
var _debridOrderList = document.getElementById('debridOrderList');
if (_debridOpts && _debridOrderCb) {
function _enforceOrderState() {
var hasRealOptions = _debridOpts.children.length > 0;
if (!hasRealOptions) {
_debridOrderCb.disabled = true;
_debridOrderCb.checked = false;
if (_debridOrderList) _debridOrderList.classList.add('hidden');
}
}
// After ANY input change in the form, wait for config.js to finish then enforce
var _form = _debridOrderCb.closest('form') || document;
_form.addEventListener('change', function() {
setTimeout(_enforceOrderState, 0);
}, true);
// Also block direct clicks when no options exist
_debridOrderCb.addEventListener('click', function(e) {
if (_debridOpts.children.length === 0) {
e.preventDefault();
_debridOrderCb.checked = false;
}
});
// Initial enforcement
setTimeout(_enforceOrderState, 200);
}
// ═══ LISTE ORDRE DEBRID ═══
var _dolAnimating = false;
var _dolDrag = null;
window._updateDolPositions = function() {
if (!_debridOrderList) return;
Array.from(_debridOrderList.querySelectorAll(':scope > li')).forEach(function(li, i) {
var pos = li.querySelector('.dol-pos');
if (pos) pos.textContent = i + 1;
});
};
window._updateDolArrows = function() {
if (!_debridOrderList) return;
var items = Array.from(_debridOrderList.querySelectorAll(':scope > li'));
items.forEach(function(li, i) {
var up = li.querySelector('.dol-up');
var down = li.querySelector('.dol-down');
if (up) up.classList.toggle('disabled', i === 0);
if (down) down.classList.toggle('disabled', i === items.length - 1);
});
};
function _dolGetY(e) {
return e.touches ? e.touches[0].clientY : e.clientY;
}
function _dolGetItems() {
return Array.from(_debridOrderList.querySelectorAll(':scope > li:not(.dol-lifted)'));
}
function _dolRecordRects(items) {
var rects = new Map();
items.forEach(function(el) { rects.set(el, el.getBoundingClientRect()); });
return rects;
}
function _dolFlipAnimate(items, oldRects) {
items.forEach(function(el) {
var oldR = oldRects.get(el);
if (!oldR) return;
var newR = el.getBoundingClientRect();
var dy = oldR.top - newR.top;
if (Math.abs(dy) < 1) return;
el.classList.remove('dol-shifting');
el.style.transform = 'translateY(' + dy + 'px)';
el.offsetHeight;
el.classList.add('dol-shifting');
el.style.transform = '';
});
}
function _dolStartDrag(e) {
if (_dolAnimating || _dolDrag) return;
var li = e.target.closest('#debridOrderList > li');
if (!li || e.target.closest('.dol-arrow')) return;
e.preventDefault();
var rect = li.getBoundingClientRect();
var clientY = _dolGetY(e);
// Create placeholder
var ph = document.createElement('div');
ph.className = 'dol-placeholder';
ph.style.height = rect.height + 'px';
_debridOrderList.insertBefore(ph, li);
li.classList.add('dol-lifted');
li.style.width = rect.width + 'px';
li.style.top = '0px';
li.style.left = '0px';
var fixedRect = li.getBoundingClientRect();
var compX = fixedRect.left;
var compY = fixedRect.top;
li.style.left = (rect.left - compX) + 'px';
li.style.top = (rect.top - compY) + 'px';
_dolDrag = {
li: li,
ph: ph,
offsetY: clientY - rect.top,
compX: compX,
compY: compY,
itemH: rect.height + 6,
lastIdx: Array.from(_debridOrderList.children).indexOf(ph)
};
}
function _dolMoveDrag(e) {
if (!_dolDrag) return;
e.preventDefault();
var clientY = _dolGetY(e);
var d = _dolDrag;
d.li.style.top = (clientY - d.offsetY - d.compY) + 'px';
// Determine target index from cursor position
var siblings = _dolGetItems();
var targetIdx = siblings.length;
for (var i = 0; i < siblings.length; i++) {
var r = siblings[i].getBoundingClientRect();
if (clientY < r.top + r.height / 2) {
targetIdx = i;
break;
}
}
var children = Array.from(_debridOrderList.children).filter(function(c) { return !c.classList.contains('dol-lifted'); });
var curIdx = children.indexOf(d.ph);
if (targetIdx !== curIdx) {
var allMovable = _dolGetItems();
var oldRects = _dolRecordRects(allMovable);
if (targetIdx >= siblings.length) {
_debridOrderList.appendChild(d.ph);
} else {
_debridOrderList.insertBefore(d.ph, siblings[targetIdx]);
}
_dolFlipAnimate(allMovable, oldRects);
d.lastIdx = targetIdx;
window._updateDolPositions();
}
}
function _dolEndDrag() {
if (!_dolDrag) return;
var d = _dolDrag;
_dolDrag = null;
var phRect = d.ph.getBoundingClientRect();
// Animate the lifted item to the placeholder position
d.li.classList.add('dol-snap');
d.li.style.top = (phRect.top - d.compY) + 'px';
d.li.style.left = (phRect.left - d.compX) + 'px';
d.li.style.transform = 'scale(1)';
setTimeout(function() {
// Remove lifting state
d.li.classList.remove('dol-lifted', 'dol-snap');
d.li.style.width = '';
d.li.style.left = '';
d.li.style.top = '';
d.li.style.transform = '';
// Put item where placeholder is
_debridOrderList.insertBefore(d.li, d.ph);
d.ph.remove();
// Clean shifting classes
_dolGetItems().forEach(function(el) {
el.classList.remove('dol-shifting');
el.style.transform = '';
});
// Landing glow
d.li.classList.add('dol-landing');
var posBadge = d.li.querySelector('.dol-pos');
if (posBadge) posBadge.classList.add('dol-pos-pop');
setTimeout(function() {
d.li.classList.remove('dol-landing');
if (posBadge) posBadge.classList.remove('dol-pos-pop');
}, 350);
window._updateDolPositions();
window._updateDolArrows();
}, 220);
}
if (_debridOrderList) {
_debridOrderList.addEventListener('mousedown', _dolStartDrag);
document.addEventListener('mousemove', _dolMoveDrag);
document.addEventListener('mouseup', _dolEndDrag);
_debridOrderList.addEventListener('touchstart', _dolStartDrag, { passive: false });
document.addEventListener('touchmove', _dolMoveDrag, { passive: false });
document.addEventListener('touchend', _dolEndDrag);
// Arrow buttons (FLIP animation)
_debridOrderList.addEventListener('click', function(e) {
var btn = e.target.closest('.dol-arrow');
if (!btn || _dolAnimating) return;
var li = btn.closest('li');
if (!li) return;
var isUp = btn.classList.contains('dol-up');
var sibling = isUp ? li.previousElementSibling : li.nextElementSibling;
if (!sibling || sibling.tagName !== 'LI') return;
_dolAnimating = true;
var liFirst = li.getBoundingClientRect();
var sibFirst = sibling.getBoundingClientRect();
if (isUp) {
_debridOrderList.insertBefore(li, sibling);
} else {
_debridOrderList.insertBefore(sibling, li);
}
window._updateDolPositions();
window._updateDolArrows();
var liLast = li.getBoundingClientRect();
var sibLast = sibling.getBoundingClientRect();
li.style.transition = 'none';
sibling.style.transition = 'none';
li.style.transform = 'translateY(' + (liFirst.top - liLast.top) + 'px)';
sibling.style.transform = 'translateY(' + (sibFirst.top - sibLast.top) + 'px)';
li.style.zIndex = '5';
li.offsetHeight;
li.style.transition = 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1)';
sibling.style.transition = 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1)';
li.style.transform = '';
sibling.style.transform = '';
li.classList.add('dol-landing');
var posBadge = li.querySelector('.dol-pos');
if (posBadge) posBadge.classList.add('dol-pos-pop');
setTimeout(function() {
li.style.transition = ''; li.style.zIndex = '';
sibling.style.transition = '';
li.classList.remove('dol-landing');
if (posBadge) posBadge.classList.remove('dol-pos-pop');
_dolAnimating = false;
}, 320);
});
var _dolObserver = new MutationObserver(function() {
window._updateDolPositions();
window._updateDolArrows();
});
_dolObserver.observe(_debridOrderList, { childList: true });
}
// ═══ HINT INFO MODAL ═══
var _hintModal = document.getElementById('hint-modal');
var _hintTitle = document.getElementById('hint-modal-title');
var _hintText = document.getElementById('hint-modal-text');
var _hintClose = document.getElementById('hint-modal-close');
if (_hintModal) {
document.addEventListener('click', function(e) {
var flair = e.target.closest('.hint-flair');
if (!flair) return;
e.preventDefault();
e.stopPropagation();
_hintTitle.textContent = flair.getAttribute('data-hint-title') || '';
_hintText.textContent = flair.getAttribute('data-hint-text') || '';
_hintModal.classList.add('show');
});
if (_hintClose) {
_hintClose.addEventListener('click', function() {
_hintModal.classList.remove('show');
});
}
_hintModal.addEventListener('click', function(e) {
if (e.target === _hintModal) _hintModal.classList.remove('show');
});
}
document.addEventListener('keydown', function(e) {
var tag = document.activeElement.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault(); nextStep();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault(); prevStep();
}
});
updateWizard(true);
// YGG PRIORITY FLAIR
var _yggPriCb = document.getElementById('yggflixPriority');
if (_yggPriCb) {
_yggPriCb.addEventListener('change', updateYggPriorityFlair);
updateYggPriorityFlair();
}
// INIT PRIORITY DROPDOWNS
initPriorityDropdowns();
// Re-init when provider fields become visible (toggled on)
document.querySelectorAll('.tog-row input[type="checkbox"]').forEach(function(cb) {
cb.addEventListener('change', function() {
setTimeout(initPriorityDropdowns, 50);
});
});
// EXCLUSIONS MOTS CLÉS
var hiddenInput = document.getElementById('exclusion-keywords');
var wrap = document.getElementById('tagWrap');
var ghost = document.getElementById('tagInput');
var tags = [];
function syncHidden() {
hiddenInput.value = tags.join(', ');
}
function createChip(text) {
var chip = document.createElement('span');
chip.className = 'tag-chip';
chip.setAttribute('data-tag', text);
chip.innerHTML = '<span>' + text + '</span>'
+ '<span class="tag-chip-x" title="Supprimer">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
+ '</span>';
chip.querySelector('.tag-chip-x').addEventListener('click', function(e) {
e.stopPropagation();
removeTag(text);
});
return chip;
}
function addTag(raw) {
var text = raw.trim();
if (!text || tags.indexOf(text) !== -1) return;
tags.push(text);
wrap.insertBefore(createChip(text), ghost);
syncHidden();
}
function removeTag(text) {
var idx = tags.indexOf(text);
if (idx === -1) return;
tags.splice(idx, 1);
var chip = wrap.querySelector('.tag-chip[data-tag="' + CSS.escape(text) + '"]');
if (chip) {
chip.style.animation = 'none';
chip.style.transition = 'opacity 0.2s, transform 0.2s';
chip.style.opacity = '0';
chip.style.transform = 'scale(0.7)';
setTimeout(function() { chip.remove(); }, 200);
}
syncHidden();
}
function removeLast() {
if (tags.length === 0) return;
removeTag(tags[tags.length - 1]);
}
ghost.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
var val = ghost.value.replace(/,/g, '');
addTag(val);
ghost.value = '';
}
if (e.key === 'Backspace' && ghost.value === '') {
removeLast();
}
});
ghost.addEventListener('paste', function(e) {
e.preventDefault();
var paste = (e.clipboardData || window.clipboardData).getData('text');
var parts = paste.split(',');
parts.forEach(function(p) { addTag(p); });
ghost.value = '';
});
var _origValDesc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
var observer = new MutationObserver(function() { loadFromHidden(); });
observer.observe(hiddenInput, { attributes: true, attributeFilter: ['value'] });
function loadFromHidden() {
var val = _origValDesc.get.call(hiddenInput);
if (!val) return;
var existing = val.split(',');
existing.forEach(function(p) {
var t = p.trim();
if (t && tags.indexOf(t) === -1) addTag(t);
});
}
setTimeout(loadFromHidden, 100);
var sliders = [
{ range: 'range_maxSize', input: 'maxSize', fill: 'fill_maxSize', max: 500 },
{ range: 'range_resultsPerQuality', input: 'resultsPerQuality', fill: 'fill_resultsPerQuality', max: 50 },
{ range: 'range_maxResults', input: 'maxResults', fill: 'fill_maxResults', max: 100 },
{ range: 'range_minCachedResults', input: 'minCachedResults', fill: 'fill_minCachedResults', max: 50 },
{ range: 'range_minPostgresResults', input: 'minPostgresResults', fill: 'fill_minPostgresResults', max: 20 },
{ range: 'range_postgresMaxAgeDays', input: 'postgresMaxAgeDays', fill: 'fill_postgresMaxAgeDays', max: 30 },
{ range: 'range_rdMinCachedBeforeCheck', input: 'rdMinCachedBeforeCheck', fill: 'fill_rdMinCachedBeforeCheck', max: 10 }
];
sliders.forEach(function(s) {
var range = document.getElementById(s.range);
var input = document.getElementById(s.input);
var fill = document.getElementById(s.fill);
if (!range || !input || !fill) return;
function updateFill() {
var min = parseFloat(range.min) || 0;
var max = parseFloat(range.max) || s.max;
var val = parseFloat(range.value) || 0;
var pct = ((val - min) / (max - min)) * 100;
fill.style.width = pct + '%';
}
range.addEventListener('input', function() {
input.value = range.value;
updateFill();
});
input.addEventListener('input', function() {
var v = parseFloat(input.value);
if (!isNaN(v)) {
range.value = Math.min(Math.max(v, parseFloat(range.min) || 0), parseFloat(range.max) || s.max);
updateFill();
}
});
input.addEventListener('change', function() {
var v = parseFloat(input.value);
if (!isNaN(v)) {
range.value = Math.min(Math.max(v, parseFloat(range.min) || 0), parseFloat(range.max) || s.max);
updateFill();
}
});
// Initial sync: config.js may have already set the number input before app.js loaded
var initVal = parseFloat(input.value);
if (!isNaN(initVal)) {
range.value = Math.min(Math.max(initVal, parseFloat(range.min) || 0), parseFloat(range.max) || s.max);
}
updateFill();
// Override value setter so future programmatic changes auto-sync
var _desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
Object.defineProperty(input, 'value', {
get: function() { return _desc.get.call(this); },
set: function(val) {
_desc.set.call(this, val);
var v = parseFloat(val);
if (!isNaN(v)) {
range.value = Math.min(Math.max(v, parseFloat(range.min) || 0), parseFloat(range.max) || s.max);
}
updateFill();
},
configurable: true
});
});
var _rdToggle = document.getElementById('debrid_rd');
if (_rdToggle) {
_rdToggle.addEventListener('change', function() {
setTimeout(function() {
var rdRange = document.getElementById('range_rdMinCachedBeforeCheck');
var rdInput = document.getElementById('rdMinCachedBeforeCheck');
var rdFill = document.getElementById('fill_rdMinCachedBeforeCheck');
if (rdRange && rdInput && rdFill) {
var pct = ((parseFloat(rdRange.value) - 0) / (10 - 0)) * 100;
rdFill.style.width = pct + '%';
}
}, 100);
});
}
});