mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-04-20 15:36:52 -04:00
398 lines
17 KiB
JavaScript
398 lines
17 KiB
JavaScript
/**
|
|
* Media Hunt Setup Wizard — guided configuration flow inside Media Hunt.
|
|
*
|
|
* Shows a full-takeover wizard when the user first navigates to any Media
|
|
* Hunt section and essential configuration is missing. Steps:
|
|
* 1. Instance 2. Indexers 3. Root Folders 4. Download Client
|
|
* 5. (conditional) Usenet Servers — shown only when NZB Hunt is configured
|
|
* as the download client.
|
|
*
|
|
* Once all steps are complete **or** the user clicks "Skip", the wizard
|
|
* never appears again (flag stored in localStorage).
|
|
*
|
|
* Attaches to window.SetupWizard.
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
var PREF_KEY = 'media-hunt-wizard-completed';
|
|
var TOTAL_BASE_STEPS = 4; // steps 1-4 (always present)
|
|
var stepStatus = { 1: false, 2: false, 3: false, 4: false, 5: false };
|
|
var nzbHuntIsClient = false; // whether step 5 is relevant
|
|
var _refreshing = false;
|
|
|
|
// ── Public API ───────────────────────────────────────────────────
|
|
window.SetupWizard = {
|
|
/**
|
|
* Should the wizard be shown? Checks localStorage first (fast path)
|
|
* then verifies against APIs. Calls `cb(needsWizard)`.
|
|
*/
|
|
check: function(cb) {
|
|
if (_isDismissed()) { cb(false); return; }
|
|
|
|
// If a force-reset was requested, always show the wizard once
|
|
var forceShow = false;
|
|
try { forceShow = sessionStorage.getItem('setup-wizard-force-show') === '1'; } catch (e) {}
|
|
|
|
_checkAllSteps(function() {
|
|
var allDone = _allStepsComplete();
|
|
if (allDone && !forceShow) {
|
|
// All steps done — mark complete so wizard and banners never show again
|
|
_markComplete();
|
|
cb(false);
|
|
} else {
|
|
// Clear the force flag so it only applies once
|
|
try { sessionStorage.removeItem('setup-wizard-force-show'); } catch (e) {}
|
|
cb(true);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Show the wizard view and update its step indicators.
|
|
* Called by app.js after `check()` returns true.
|
|
*/
|
|
show: function() {
|
|
var view = document.getElementById('media-hunt-setup-wizard-view');
|
|
if (view) view.style.display = '';
|
|
_setSidebarVisible(false);
|
|
_setSponsorsVisible(false);
|
|
// Restore any page-header-bars that were hidden during wizard navigation
|
|
var hiddenHeaders = document.querySelectorAll('.page-header-bar');
|
|
for (var i = 0; i < hiddenHeaders.length; i++) {
|
|
hiddenHeaders[i].style.display = '';
|
|
}
|
|
_updateStepUI();
|
|
_expandFirstIncomplete();
|
|
_maybeShowReturnBanner();
|
|
},
|
|
|
|
/**
|
|
* Hide the wizard view and restore sidebar.
|
|
*/
|
|
hide: function() {
|
|
var view = document.getElementById('media-hunt-setup-wizard-view');
|
|
if (view) view.style.display = 'none';
|
|
_setSidebarVisible(true);
|
|
_setSponsorsVisible(true);
|
|
},
|
|
|
|
/**
|
|
* Re-check all steps and update UI. Used when user returns from
|
|
* a configuration page back to any Media Hunt section.
|
|
*/
|
|
refresh: function(cb) {
|
|
if (_refreshing) { if (cb) cb(); return; }
|
|
_refreshing = true;
|
|
_checkAllSteps(function() {
|
|
_refreshing = false;
|
|
var allDone = _allStepsComplete();
|
|
if (allDone) {
|
|
// All steps done — mark complete so wizard and banners never show again
|
|
_markComplete();
|
|
if (cb) cb();
|
|
return;
|
|
}
|
|
_updateStepUI();
|
|
_expandFirstIncomplete();
|
|
if (cb) cb();
|
|
});
|
|
},
|
|
|
|
/** Cached status from last check. */
|
|
isComplete: function() {
|
|
return _isDismissed() || _allStepsComplete();
|
|
},
|
|
|
|
/**
|
|
* Call after successful save on a wizard-related config page (instances,
|
|
* indexers, root folders, clients). If the wizard is still incomplete,
|
|
* redirects to Collections so the wizard refreshes and shows the next step.
|
|
*/
|
|
maybeReturnToCollection: function() {
|
|
if (this.isComplete()) return;
|
|
try { sessionStorage.setItem('setup-wizard-return-from-config', '1'); } catch (e) {}
|
|
if (window.huntarrUI && typeof window.huntarrUI.switchSection === 'function') {
|
|
window.huntarrUI.switchSection('media-hunt-collection');
|
|
} else {
|
|
window.location.hash = '#media-hunt-collection';
|
|
}
|
|
}
|
|
};
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────
|
|
function _isDismissed() {
|
|
return HuntarrUtils.getUIPreference(PREF_KEY, false) === true;
|
|
}
|
|
|
|
function _markComplete() {
|
|
HuntarrUtils.setUIPreference(PREF_KEY, true);
|
|
// Clear the wizard navigation flag so banners stop showing
|
|
try { sessionStorage.removeItem('setup-wizard-active-nav'); } catch (e) {}
|
|
}
|
|
|
|
function _totalSteps() {
|
|
return nzbHuntIsClient ? TOTAL_BASE_STEPS + 1 : TOTAL_BASE_STEPS;
|
|
}
|
|
|
|
function _allStepsComplete() {
|
|
for (var s = 1; s <= _totalSteps(); s++) {
|
|
if (!stepStatus[s]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function _setSidebarVisible(visible) {
|
|
var wrapper = document.getElementById('sidebar-wrapper');
|
|
if (wrapper) wrapper.style.display = visible ? '' : 'none';
|
|
}
|
|
|
|
function _setSponsorsVisible(visible) {
|
|
if (visible) {
|
|
document.body.classList.remove('setup-wizard-active');
|
|
} else {
|
|
document.body.classList.add('setup-wizard-active');
|
|
}
|
|
}
|
|
|
|
function _maybeShowReturnBanner() {
|
|
try {
|
|
if (sessionStorage.getItem('setup-wizard-return-from-config') !== '1') return;
|
|
sessionStorage.removeItem('setup-wizard-return-from-config');
|
|
} catch (e) { return; }
|
|
var wizard = document.getElementById('media-hunt-setup-wizard');
|
|
if (!wizard) return;
|
|
var banner = document.createElement('div');
|
|
banner.className = 'setup-wizard-return-banner';
|
|
banner.setAttribute('role', 'status');
|
|
banner.innerHTML = '<i class="fas fa-check-circle"></i> Configuration saved! Continue with the next step below.';
|
|
wizard.insertBefore(banner, wizard.firstChild);
|
|
setTimeout(function() {
|
|
banner.style.opacity = '0';
|
|
banner.style.transition = 'opacity 0.3s ease';
|
|
setTimeout(function() { if (banner.parentNode) banner.parentNode.removeChild(banner); }, 300);
|
|
}, 4500);
|
|
}
|
|
|
|
// ── Step Checks ─────────────────────────────────────────────────
|
|
function _checkAllSteps(cb) {
|
|
var ts = '?_=' + Date.now();
|
|
Promise.all([
|
|
fetch('./api/movie-hunt/instances' + ts, { cache: 'no-store' }).then(function(r) { return r.json(); }),
|
|
fetch('./api/tv-hunt/instances' + ts, { cache: 'no-store' }).then(function(r) { return r.json(); }),
|
|
fetch('./api/indexer-hunt/indexers' + ts, { cache: 'no-store' }).then(function(r) { return r.json(); }),
|
|
fetch('./api/movie-hunt/has-clients' + ts, { cache: 'no-store' }).then(function(r) { return r.json(); }),
|
|
fetch('./api/nzb-hunt/is-client-configured' + ts, { cache: 'no-store' }).then(function(r) { return r.json(); }).catch(function() { return { configured: false }; })
|
|
]).then(function(results) {
|
|
var movieInstances = results[0].instances || [];
|
|
var tvInstances = results[1].instances || [];
|
|
var indexers = results[2].indexers || [];
|
|
var hasClients = results[3].has_clients === true;
|
|
nzbHuntIsClient = results[4].configured === true;
|
|
|
|
stepStatus[1] = movieInstances.length > 0 || tvInstances.length > 0;
|
|
stepStatus[2] = indexers.length > 0;
|
|
stepStatus[4] = hasClients;
|
|
|
|
// Toggle step 5 visibility
|
|
var step5el = document.getElementById('setup-step-5');
|
|
if (step5el) step5el.style.display = nzbHuntIsClient ? '' : 'none';
|
|
|
|
// Root folders — need at least one instance first
|
|
if (stepStatus[1]) {
|
|
_checkRootFolders(movieInstances, tvInstances, function(hasRoots) {
|
|
stepStatus[3] = hasRoots;
|
|
// NZB servers
|
|
if (nzbHuntIsClient) {
|
|
_checkNzbServers(function(hasServers) {
|
|
stepStatus[5] = hasServers;
|
|
if (cb) cb();
|
|
});
|
|
} else {
|
|
stepStatus[5] = true; // not applicable — treat as done
|
|
if (cb) cb();
|
|
}
|
|
});
|
|
} else {
|
|
stepStatus[3] = false;
|
|
if (nzbHuntIsClient) {
|
|
_checkNzbServers(function(hasServers) {
|
|
stepStatus[5] = hasServers;
|
|
if (cb) cb();
|
|
});
|
|
} else {
|
|
stepStatus[5] = true;
|
|
if (cb) cb();
|
|
}
|
|
}
|
|
}).catch(function() {
|
|
stepStatus[1] = stepStatus[2] = stepStatus[3] = stepStatus[4] = stepStatus[5] = false;
|
|
nzbHuntIsClient = false;
|
|
if (cb) cb();
|
|
});
|
|
}
|
|
|
|
function _checkRootFolders(movieInstances, tvInstances, cb) {
|
|
var fetches = [];
|
|
if (movieInstances.length > 0) {
|
|
fetches.push(
|
|
fetch('./api/movie-hunt/root-folders', { cache: 'no-store' })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) { return (d.root_folders || d.rootFolders || []).length > 0; })
|
|
.catch(function() { return false; })
|
|
);
|
|
}
|
|
if (tvInstances.length > 0) {
|
|
fetches.push(
|
|
fetch('./api/tv-hunt/root-folders', { cache: 'no-store' })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) { return (d.root_folders || d.rootFolders || []).length > 0; })
|
|
.catch(function() { return false; })
|
|
);
|
|
}
|
|
if (fetches.length === 0) { cb(false); return; }
|
|
Promise.all(fetches).then(function(results) {
|
|
cb(results.some(function(v) { return v; }));
|
|
}).catch(function() { cb(false); });
|
|
}
|
|
|
|
function _checkNzbServers(cb) {
|
|
fetch('./api/nzb-hunt/home-stats', { cache: 'no-store' })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
// API returns has_servers (boolean) or servers (array)
|
|
var hasServers = d.has_servers === true || (d.servers && d.servers.length > 0);
|
|
cb(hasServers);
|
|
})
|
|
.catch(function() { cb(false); });
|
|
}
|
|
|
|
// ── UI Updates ──────────────────────────────────────────────────
|
|
function _updateStepUI() {
|
|
var total = _totalSteps();
|
|
var completedCount = 0;
|
|
|
|
for (var s = 1; s <= 5; s++) {
|
|
var stepEl = document.getElementById('setup-step-' + s);
|
|
var indicator = document.getElementById('setup-step-indicator-' + s);
|
|
if (!stepEl || !indicator) continue;
|
|
if (s > total) { stepEl.style.display = 'none'; continue; }
|
|
|
|
stepEl.classList.remove('completed', 'current');
|
|
|
|
if (stepStatus[s]) {
|
|
stepEl.classList.add('completed');
|
|
completedCount++;
|
|
if (!indicator.querySelector('.step-check')) {
|
|
var check = document.createElement('i');
|
|
check.className = 'fas fa-check step-check';
|
|
indicator.appendChild(check);
|
|
}
|
|
} else {
|
|
// Remove leftover check icon if step became incomplete
|
|
var existing = indicator.querySelector('.step-check');
|
|
if (existing) existing.remove();
|
|
}
|
|
}
|
|
|
|
// Mark first incomplete step as "current"
|
|
for (var s = 1; s <= total; s++) {
|
|
if (!stepStatus[s]) {
|
|
var el = document.getElementById('setup-step-' + s);
|
|
if (el) el.classList.add('current');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Progress bar
|
|
var fill = document.getElementById('setup-wizard-progress-fill');
|
|
if (fill) {
|
|
fill.style.width = (completedCount / total * 100) + '%';
|
|
}
|
|
}
|
|
|
|
function _expandFirstIncomplete() {
|
|
var total = _totalSteps();
|
|
for (var s = 1; s <= 5; s++) {
|
|
var el = document.getElementById('setup-step-' + s);
|
|
if (el) el.classList.remove('expanded');
|
|
}
|
|
for (var s = 1; s <= total; s++) {
|
|
if (!stepStatus[s]) {
|
|
_expandStep(s);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function _expandStep(num) {
|
|
var stepEl = document.getElementById('setup-step-' + num);
|
|
if (!stepEl) return;
|
|
for (var s = 1; s <= 5; s++) {
|
|
var el = document.getElementById('setup-step-' + s);
|
|
if (el && s !== num) el.classList.remove('expanded');
|
|
}
|
|
stepEl.classList.toggle('expanded');
|
|
}
|
|
|
|
// ── Event Bindings ──────────────────────────────────────────────
|
|
function _bindEvents() {
|
|
document.addEventListener('click', function(e) {
|
|
// Wizard nav buttons (use switchSection for reliable navigation)
|
|
var navBtn = e.target.closest('[data-wizard-nav]');
|
|
if (navBtn) {
|
|
var section = navBtn.getAttribute('data-wizard-nav');
|
|
// Mark that user is navigating from the setup wizard
|
|
try {
|
|
sessionStorage.setItem('setup-wizard-active-nav', '1');
|
|
} catch (e2) {}
|
|
if (section && window.huntarrUI && typeof window.huntarrUI.switchSection === 'function') {
|
|
window.huntarrUI.switchSection(section);
|
|
} else if (section) {
|
|
window.location.hash = '#' + section;
|
|
}
|
|
// Hide the back/breadcrumb in the target section's header bar
|
|
// (redundant during setup — the "Continue to Setup Guide" banner
|
|
// provides all the navigation the user needs)
|
|
// NOTE: Only hide .reqset-toolbar-left, NOT the entire .page-header-bar,
|
|
// because the save button lives in .reqset-toolbar-right and must stay visible.
|
|
setTimeout(function() {
|
|
var allSections = document.querySelectorAll('.content-section');
|
|
for (var i = 0; i < allSections.length; i++) {
|
|
if (allSections[i].style.display !== 'none' && allSections[i].offsetParent !== null) {
|
|
var toolbarLeft = allSections[i].querySelector('.page-header-bar .reqset-toolbar-left');
|
|
if (toolbarLeft) toolbarLeft.style.display = 'none';
|
|
}
|
|
}
|
|
}, 150);
|
|
return;
|
|
}
|
|
|
|
// Step header toggle
|
|
var header = e.target.closest('[data-step-toggle]');
|
|
if (header) {
|
|
var step = parseInt(header.getAttribute('data-step-toggle'), 10);
|
|
if (!isNaN(step)) _expandStep(step);
|
|
}
|
|
|
|
// Skip button — permanently dismiss
|
|
if (e.target.closest('#setup-wizard-skip')) {
|
|
_markComplete();
|
|
window.SetupWizard.hide();
|
|
var collView = document.getElementById('media-hunt-collection-view');
|
|
if (collView) collView.style.display = 'block';
|
|
if (window.MediaHuntCollection && typeof window.MediaHuntCollection.init === 'function') {
|
|
window.MediaHuntCollection.init();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Init ────────────────────────────────────────────────────────
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', _bindEvents);
|
|
} else {
|
|
_bindEvents();
|
|
}
|
|
})();
|