/** * 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 = ' 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(); } })();