Files
Admin9705 8900c1a4aa Update
2026-02-20 13:45:10 -05:00

1750 lines
73 KiB
JavaScript

/**
* Huntarr - Core Application Orchestrator
* Main entry point for the Huntarr UI.
* Coordinates between modular components and handles global application state.
*/
function _checkLogsMediaHuntInstances(cb) {
Promise.all([
fetch('./api/movie-hunt/instances', { cache: 'no-store' }).then(function(r) { return r.json(); }),
fetch('./api/tv-hunt/instances', { cache: 'no-store' }).then(function(r) { return r.json(); }),
fetch('./api/indexer-hunt/indexers', { cache: 'no-store' }).then(function(r) { return r.json(); }),
fetch('./api/movie-hunt/has-clients', { cache: 'no-store' }).then(function(r) { return r.json(); })
]).then(function(results) {
var movieCount = (results[0].instances || []).length;
var tvCount = (results[1].instances || []).length;
var indexerCount = (results[2].indexers || []).length;
var hasClients = results[3].has_clients === true;
var hasInstances = movieCount > 0 || tvCount > 0;
if (!hasInstances) {
cb('no-instances');
} else if (indexerCount === 0) {
cb('no-indexers');
} else if (!hasClients) {
cb('no-clients');
} else {
cb('ok');
}
}).catch(function() { cb('no-instances'); });
}
let huntarrUI = {
// Current state
currentSection: 'home', // Default section
currentHistoryApp: 'all', // Default history app
currentLogApp: 'all', // Default log app for compatibility
autoScroll: true,
eventSources: {}, // Event sources for compatibility
isLoadingStats: false, // Flag to prevent multiple simultaneous stats requests
configuredApps: {
sonarr: false,
radarr: false,
lidarr: false,
readarr: false, // Added readarr
whisparr: false, // Added whisparr
eros: false, // Added eros
swaparr: false // Added swaparr
},
configuredAppsInitialized: false, // Track if we've loaded app states at least once
originalSettings: {}, // Store the full original settings object
settingsChanged: false, // Legacy flag (auto-save enabled)
// Logo URL
logoUrl: './static/logo/256.png',
// Element references
elements: {},
// Initialize the application
init: function() {
console.log('[huntarrUI] Initializing UI...');
// EXPOSE huntarrUI to global scope early for modules that need it during loading
window.huntarrUI = this;
// Skip initialization on login page
const isLoginPage = document.querySelector('.login-container, #loginForm, .login-form');
if (isLoginPage) {
console.log('[huntarrUI] Login page detected, skipping full initialization');
return;
}
// Cache frequently used DOM elements
this.cacheElements();
this._enableRequestarr = true;
this._enableNzbHunt = true;
this._enableMediaHunt = true;
this._enableThirdPartyApps = true;
this._settingsLoaded = false;
fetch('./api/settings')
.then(r => r.json())
.then(all => {
var generalSettings = (all && all.general) || {};
this._enableRequestarr = generalSettings.enable_requestarr !== false;
this._enableNzbHunt = true;
this._enableMediaHunt = generalSettings.enable_media_hunt !== false;
this._enableThirdPartyApps = generalSettings.enable_third_party_apps !== false;
this._settingsLoaded = true;
// Update sidebar group visibility from database settings (nav-group-* IDs)
// IMPORTANT: Skip this for non-owner users — they are fully siloed
var isNonOwner = document.body.classList.contains('non-owner-mode');
if (!isNonOwner) {
var requestsGroup = document.getElementById('nav-group-requests');
var mediaHuntGroup = document.getElementById('nav-group-media-hunt');
var nzbHuntGroup = document.getElementById('nzb-hunt-sidebar-group');
var appsGroup = document.getElementById('nav-group-apps');
var appsLabel = document.getElementById('nav-group-apps-label');
if (requestsGroup) requestsGroup.style.display = (generalSettings.enable_requestarr === false) ? 'none' : '';
if (mediaHuntGroup) mediaHuntGroup.style.display = (generalSettings.enable_media_hunt === false) ? 'none' : '';
if (nzbHuntGroup) nzbHuntGroup.style.display = (generalSettings.enable_media_hunt === false) ? 'none' : '';
if (appsGroup) appsGroup.style.display = (generalSettings.enable_third_party_apps === false) ? 'none' : '';
if (appsLabel) appsLabel.style.display = (generalSettings.enable_media_hunt === false && generalSettings.enable_third_party_apps === false) ? 'none' : '';
}
if (typeof window.applyFeatureFlags === 'function') window.applyFeatureFlags();
// Initialize originalSettings early
this.originalSettings = all || {};
this.originalSettings.general = all.general || { ui_preferences: {} };
if (!this.originalSettings.general.ui_preferences) this.originalSettings.general.ui_preferences = {};
this.updateMovieHuntNavVisibility();
// NOW initialize UI preferences that depend on settings
if (window.HuntarrTheme) {
window.HuntarrTheme.initDarkMode();
}
this.logoUrl = HuntarrUtils.getUIPreference('logo-url', this.logoUrl);
if (typeof window.applyLogoToAllElements === 'function') {
window.applyLogoToAllElements();
}
// Settings are now loaded — re-initialize view toggle with correct preference
if (window.HuntarrStats && (this.currentSection === 'home' || !this.currentSection)) {
window.HuntarrStats.initViewToggle();
window.HuntarrStats.loadMediaStats(true);
}
if ((this.currentSection === 'home' || !this.currentSection) && window.HuntarrIndexerHuntHome && typeof window.HuntarrIndexerHuntHome.setup === 'function') {
window.HuntarrIndexerHuntHome.setup();
}
// Settings are loaded — now safe to check welcome preference
if (this.currentSection === 'home' || !this.currentSection) {
this._maybeShowWelcome();
}
})
.catch(() => {});
// Register event handlers
this.setupEventListeners();
this.setupLogoHandling();
// Auto-save enabled - no unsaved changes handler needed
// NOTE: loadMediaStats() + initViewToggle() + startPolling() are called
// by switchSection('home') via handleHashNavigation below — no need to duplicate here.
// Check if we need to navigate to a specific section after refresh
const targetSection = localStorage.getItem('huntarr-target-section');
if (targetSection) {
console.log(`[huntarrUI] Found target section after refresh: ${targetSection}`);
localStorage.removeItem('huntarr-target-section');
// Keep URL in sync so hash-based logic and back button work (replaceState avoids firing hashchange)
if (window.location.hash !== '#' + targetSection) {
window.history.replaceState(null, document.title, window.location.pathname + (window.location.search || '') + '#' + targetSection);
}
// Navigate to the target section
this.switchSection(targetSection);
} else {
// Initial navigation based on hash
this.handleHashNavigation(window.location.hash);
}
// Remove initial sidebar hiding style
const initialSidebarStyle = document.getElementById('initial-sidebar-state');
if (initialSidebarStyle) {
initialSidebarStyle.remove();
}
// Check which sidebar should be shown based on current section
console.log(`[huntarrUI] Initialization - current section: ${this.currentSection}`);
if (this.currentSection === 'settings' || this.currentSection === 'scheduling' || this.currentSection === 'notifications' || this.currentSection === 'backup-restore' || this.currentSection === 'user' || this.currentSection === 'settings-logs') {
console.log('[huntarrUI] Initialization - showing settings group');
this.showSettingsSidebar();
} else if (this.currentSection === 'system' || this.currentSection === 'hunt-manager' || this.currentSection === 'logs') {
console.log('[huntarrUI] Initialization - showing system group');
this.showMainSidebar();
} else if (this.currentSection === 'nzb-hunt-home' || this.currentSection === 'nzb-hunt-activity' || this.currentSection === 'nzb-hunt-server-editor' || this.currentSection === 'nzb-hunt-folders' || this.currentSection === 'nzb-hunt-servers' || this.currentSection === 'nzb-hunt-advanced' || (this.currentSection && this.currentSection.startsWith('nzb-hunt-settings'))) {
console.log('[huntarrUI] Initialization - showing NZB Hunt sidebar');
this.showNzbHuntSidebar();
} else if (this.currentSection === 'indexer-hunt' || this.currentSection === 'indexer-hunt-stats' || this.currentSection === 'indexer-hunt-history') {
console.log('[huntarrUI] Initialization - showing Media Config sidebar for Index Master');
this.showMovieHuntSidebar();
} else if ((this.currentSection && this.currentSection.startsWith('tv-hunt')) || this.currentSection === 'logs-tv-hunt') {
console.log('[huntarrUI] Initialization - showing media hunt sidebar (tv-hunt redirect)');
this.showMovieHuntSidebar();
} else if (this.currentSection === 'media-hunt-settings' || this.currentSection === 'media-hunt-instances' || this.currentSection === 'settings-instance-management' || this.currentSection === 'settings-media-management' || this.currentSection === 'settings-profiles' || this.currentSection === 'settings-sizes' || this.currentSection === 'profile-editor' || this.currentSection === 'settings-custom-formats' || this.currentSection === 'settings-indexers' || this.currentSection === 'settings-import-media' || this.currentSection === 'settings-import-lists' || this.currentSection === 'settings-root-folders') {
console.log('[huntarrUI] Initialization - showing movie hunt sidebar (config)');
this.showMovieHuntSidebar();
} else if (this.currentSection === 'movie-hunt-home' || this.currentSection === 'movie-hunt-collection' || this.currentSection === 'media-hunt-collection' || this.currentSection === 'activity-queue' || this.currentSection === 'activity-history' || this.currentSection === 'activity-blocklist' || this.currentSection === 'activity-logs' || this.currentSection === 'logs-media-hunt' || this.currentSection === 'settings-clients' || this.currentSection === 'movie-hunt-instance-editor') {
console.log('[huntarrUI] Initialization - showing movie hunt sidebar');
this.showMovieHuntSidebar();
} else if (this.currentSection === 'requestarr' || this.currentSection === 'requestarr-discover' || this.currentSection === 'requestarr-movies' || this.currentSection === 'requestarr-tv' || this.currentSection === 'requestarr-hidden' || this.currentSection === 'requestarr-personal-blacklist' || this.currentSection === 'requestarr-filters' || this.currentSection === 'requestarr-settings' || this.currentSection === 'requestarr-smarthunt-settings' || this.currentSection === 'requestarr-users' || this.currentSection === 'requestarr-bundles' || this.currentSection === 'requestarr-requests' || this.currentSection === 'requestarr-global-blacklist') {
if (this._enableRequestarr === false) {
console.log('[huntarrUI] Requestarr disabled - redirecting to home');
this.switchSection('home');
} else {
console.log('[huntarrUI] Initialization - showing requestarr sidebar');
this.showRequestarrSidebar();
}
} else if (this.currentSection === 'apps' || this.currentSection === 'sonarr' || this.currentSection === 'radarr' || this.currentSection === 'lidarr' || this.currentSection === 'readarr' || this.currentSection === 'whisparr' || this.currentSection === 'eros' || this.currentSection === 'prowlarr' || this.currentSection === 'swaparr') {
console.log('[huntarrUI] Initialization - showing apps sidebar');
this.showAppsSidebar();
} else {
// Default: show main sidebar (Home)
console.log('[huntarrUI] Initialization - showing main sidebar (default)');
this.showMainSidebar();
}
// Auto-save enabled - no unsaved changes handler needed
// Load username
this.loadUsername();
// Preload stateful management info so it's ready when needed
this.loadStatefulInfo();
// Load current version
this.loadCurrentVersion();
// Load latest version from GitHub
this.loadLatestVersion();
// Load latest beta version from GitHub
this.loadBetaVersion();
// Load GitHub star count
this.loadGitHubStarCount();
// Initialize instance event handlers
this.setupInstanceEventHandlers();
// Setup navigation for sidebars
this.setupRequestarrNavigation();
this.setupMovieHuntNavigation();
this.setupTVHuntNavigation();
this.setupNzbHuntNavigation();
this.setupAppsNavigation();
this.setupSettingsNavigation();
this.setupSystemNavigation();
// Auto-save enabled - no unsaved changes handler needed
// Setup Swaparr components
this.setupSwaparrResetCycle();
// Setup Swaparr status polling (refresh every 30 seconds)
this.setupSwaparrStatusPolling();
// Setup Prowlarr status polling (refresh every 30 seconds)
this.setupProwlarrStatusPolling();
// Setup Indexer Hunt home card — DEFERRED until feature flags are loaded
// (setupIndexerHuntHome is called inside the settings .then() callback to avoid
// a race where _enableMediaHunt is still true when the card renders)
// Fetch current user role and apply UI restrictions for non-admin users
this.applyRoleBasedUI();
// Make dashboard visible after initialization to prevent FOUC
setTimeout(() => {
this.showDashboard();
// Mark as initialized after everything is set up to enable refresh on section changes
this.isInitialized = true;
console.log('[huntarrUI] Initialization complete - refresh on section change enabled');
}, 50); // Reduced from implicit longer delay
},
// ── Role-based UI stripping ──────────────────────────────
_userRole: null,
_userPermissions: null,
applyRoleBasedUI: function() {
fetch('./api/requestarr/users/me', { cache: 'no-store' })
.then(r => r.ok ? r.json() : null)
.then(data => {
if (!data || !data.user) return;
this._userRole = data.user.role || 'owner';
this._userPermissions = data.user.permissions || {};
window._huntarrUserRole = this._userRole;
window._huntarrUserPermissions = this._userPermissions;
console.log('[huntarrUI] User role:', this._userRole);
if (this._userRole === 'owner') {
// Owner sees everything — just load badge
this._updatePendingRequestBadge();
if (!this._pendingBadgeInterval) {
this._pendingBadgeInterval = setInterval(() => this._updatePendingRequestBadge(), 60000);
}
} else {
// Non-owner users: siloed to Requests only
this._applyNonOwnerRestrictions();
}
})
.catch(e => {
console.debug('[huntarrUI] Could not fetch user role:', e);
});
},
_updatePendingRequestBadge: function() {
fetch('./api/requestarr/requests/pending-count', { cache: 'no-store' })
.then(r => r.ok ? r.json() : null)
.then(data => {
var badge = document.getElementById('requestarr-pending-badge');
var mirrors = document.querySelectorAll('.requestarr-pending-badge-mirror');
var count = (data && data.count) || 0;
var text = count > 99 ? '99+' : String(count);
var show = count > 0;
if (badge) {
badge.textContent = text;
badge.style.display = show ? '' : 'none';
}
mirrors.forEach(function(m) {
m.textContent = text;
m.style.display = show ? '' : 'none';
});
})
.catch(function() {});
},
/**
* Non-owner users get a completely separate sidebar.
* We nuke the owner nav-menu and build a clean standalone one.
*/
_applyNonOwnerRestrictions: function() {
// 1. Mark body so CSS rules apply
document.body.classList.add('non-owner-mode');
// 2. Replace the entire nav-menu with a standalone non-owner sidebar
var navMenu = document.querySelector('#sidebar .nav-menu');
if (navMenu && !document.getElementById('non-owner-nav')) {
// Grab the Daughter's Sponsors data before we nuke the menu
var sponsorNav = document.getElementById('sidebar-partner-projects-nav');
var sponsorHref = sponsorNav ? sponsorNav.getAttribute('href') : '#';
var sponsorTarget = sponsorNav ? sponsorNav.getAttribute('target') : '_blank';
var sponsorNameEl = document.getElementById('sidebar-partner-projects-name');
var sponsorName = sponsorNameEl ? sponsorNameEl.textContent : 'Loading...';
// Build the non-owner nav
var nav = document.createElement('nav');
nav.className = 'nav-menu';
nav.id = 'non-owner-nav';
var items = [
{ id: 'requestarrDiscoverNav', hash: '#requestarr-discover', icon: 'fas fa-compass', label: 'Discover' },
{ id: 'requestarrTVNav', hash: '#requestarr-tv', icon: 'fas fa-tv', label: 'TV Shows' },
{ id: 'requestarrMoviesNav', hash: '#requestarr-movies', icon: 'fas fa-film', label: 'Movies' },
{ id: 'requestarrPersonalBlacklistNav', hash: '#requestarr-personal-blacklist', icon: 'fas fa-eye-slash', label: 'Personal Blacklist' },
{ id: 'requestarrRequestsNav', hash: '#requestarr-requests', icon: 'fas fa-inbox', label: 'Requests' }
];
// Section label
nav.innerHTML = '<div class="nav-group"><div class="nav-group-title">Request System</div></div>';
// Nav items — all same level, same style
items.forEach(function(item) {
var a = document.createElement('a');
a.href = './' + item.hash;
a.className = 'nav-item non-owner-nav-item';
a.id = item.id;
a.innerHTML = '<div class="nav-icon-wrapper"><i class="' + item.icon + '"></i></div><span>' + item.label + '</span>';
nav.appendChild(a);
});
// Daughter's Sponsors — always visible
var sponsorGroup = document.createElement('div');
sponsorGroup.className = 'nav-group';
sponsorGroup.id = 'main-sidebar-partner-projects-group';
sponsorGroup.innerHTML =
'<div class="nav-group-title">Daughter\'s Sponsors</div>' +
'<a href="' + sponsorHref + '" target="' + sponsorTarget + '" rel="noopener noreferrer" class="nav-item" id="sidebar-partner-projects-nav">' +
'<div class="nav-icon-wrapper"><i class="fas fa-heart" style="color: #ec4899;"></i></div>' +
'<span id="sidebar-partner-projects-name">' + sponsorName + '</span>' +
'</a>';
nav.appendChild(sponsorGroup);
navMenu.parentNode.replaceChild(nav, navMenu);
// Set up active highlighting for the non-owner nav
function setNonOwnerActive() {
var h = window.location.hash || '#requestarr-discover';
nav.querySelectorAll('.non-owner-nav-item').forEach(function(el) { el.classList.remove('active'); });
var map = {
'#requestarr-discover': 'requestarrDiscoverNav',
'#requestarr': 'requestarrDiscoverNav',
'#requestarr-tv': 'requestarrTVNav',
'#requestarr-movies': 'requestarrMoviesNav',
'#requestarr-hidden': 'requestarrPersonalBlacklistNav',
'#requestarr-personal-blacklist': 'requestarrPersonalBlacklistNav',
'#requestarr-requests': 'requestarrRequestsNav'
};
var targetId = map[h];
if (targetId) {
var el = document.getElementById(targetId);
if (el) el.classList.add('active');
}
}
window.addEventListener('hashchange', setNonOwnerActive);
setNonOwnerActive();
}
// 3. Redirect if current section is not allowed
var allowedSections = [
'requestarr', 'requestarr-discover', 'requestarr-movies',
'requestarr-tv', 'requestarr-hidden', 'requestarr-personal-blacklist',
'requestarr-requests',
];
if (allowedSections.indexOf(this.currentSection) === -1) {
window.location.hash = '#requestarr-discover';
}
// 4. Hide the Requests header bar (breadcrumb) — redundant for non-owner users
var headerBar = document.querySelector('.requestarr-header-bar');
if (headerBar) headerBar.style.display = 'none';
},
isAdminOnlySection: function(section) {
if (this._userRole === 'owner') return false;
if (!this._userRole) return false; // not loaded yet, don't block
// All non-owner users are siloed to these sections only
var allowed = [
'requestarr', 'requestarr-discover', 'requestarr-movies',
'requestarr-tv', 'requestarr-hidden', 'requestarr-personal-blacklist',
'requestarr-requests',
];
return allowed.indexOf(section) === -1;
},
runWhenRequestarrReady: function(actionName, callback) {
if (window.HuntarrRequestarr && typeof window.HuntarrRequestarr.runWhenRequestarrReady === 'function') {
window.HuntarrRequestarr.runWhenRequestarrReady(actionName, callback);
return;
}
// Requestarr bundle not loaded yet - wait for it before running callback
const startTime = Date.now();
const checkInterval = setInterval(() => {
if (window.HuntarrRequestarr && typeof window.HuntarrRequestarr.runWhenRequestarrReady === 'function') {
clearInterval(checkInterval);
window.HuntarrRequestarr.runWhenRequestarrReady(actionName, callback);
return;
}
if (Date.now() - startTime > 5000) {
clearInterval(checkInterval);
console.warn('[huntarrUI] HuntarrRequestarr not ready for ' + actionName + ' after 5s');
}
}, 50);
},
// Cache DOM elements for better performance
cacheElements: function() {
if (window.HuntarrDOM) {
window.HuntarrDOM.cacheElements(this);
}
},
// Set up event listeners
setupEventListeners: function() {
// Navigation
document.addEventListener('click', (e) => {
// Sidebar: hash links use client-side navigation
const sidebarNavItem = e.target.closest('#sidebar .nav-item');
if (sidebarNavItem) {
const link = sidebarNavItem.tagName === 'A' ? sidebarNavItem : sidebarNavItem.querySelector('a');
const href = link && link.getAttribute('href');
if (href && href.indexOf('#') >= 0) {
e.preventDefault();
const hash = href.substring(href.indexOf('#'));
const normalizedHash = (hash || '').replace(/^#+/, '');
if (window.location.hash !== hash) {
window.location.hash = hash;
} else if (normalizedHash === 'media-hunt-collection' && window.TVHuntCollection && typeof window.TVHuntCollection.showMainView === 'function') {
window.TVHuntCollection.showMainView();
}
if (typeof setActiveNavItem === 'function') setActiveNavItem();
return;
}
}
// Navigation link handling (other nav areas)
if (e.target.matches('.nav-link') || e.target.closest('.nav-link')) {
const link = e.target.matches('.nav-link') ? e.target : e.target.closest('.nav-link');
e.preventDefault();
this.handleNavigation(e);
}
// Handle cycle reset button clicks
if (e.target.matches('.cycle-reset-button') || e.target.closest('.cycle-reset-button')) {
const button = e.target.matches('.cycle-reset-button') ? e.target : e.target.closest('.cycle-reset-button');
const app = button.dataset.app;
if (app) {
this.resetAppCycle(app, button);
}
}
});
// History dropdown toggle
if (this.elements.historyDropdownBtn) {
this.elements.historyDropdownBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation(); // Prevent event bubbling
// Toggle this dropdown
this.elements.historyDropdownContent.classList.toggle('show');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.history-dropdown') && this.elements.historyDropdownContent.classList.contains('show')) {
this.elements.historyDropdownContent.classList.remove('show');
}
});
}
// History options
this.elements.historyOptions.forEach(option => {
option.addEventListener('click', (e) => this.handleHistoryOptionChange(e));
});
// Settings dropdown toggle
if (this.elements.settingsDropdownBtn) {
this.elements.settingsDropdownBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation(); // Prevent event bubbling
// Toggle this dropdown
this.elements.settingsDropdownContent.classList.toggle('show');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.settings-dropdown') && this.elements.settingsDropdownContent.classList.contains('show')) {
this.elements.settingsDropdownContent.classList.remove('show');
}
});
}
// Settings options
this.elements.settingsOptions.forEach(option => {
option.addEventListener('click', (e) => this.handleSettingsOptionChange(e));
});
// Save settings button
// Save button removed for auto-save
// Test notification button (delegated event listener for dynamic content)
document.addEventListener('click', (e) => {
if (e.target.id === 'testNotificationBtn' || e.target.closest('#testNotificationBtn')) {
this.testNotification();
}
});
// Start hunt button
if (this.elements.startHuntButton) {
this.elements.startHuntButton.addEventListener('click', () => this.startHunt());
}
// Stop hunt button
if (this.elements.stopHuntButton) {
this.elements.stopHuntButton.addEventListener('click', () => this.stopHunt());
}
// Logout button
if (this.elements.logoutLink) {
this.elements.logoutLink.addEventListener('click', (e) => this.logout(e));
}
// Requestarr, Movie Hunt, and TV Hunt navigation
this.setupRequestarrNavigation();
this.setupMovieHuntNavigation();
this.setupTVHuntNavigation();
// Dark mode toggle
const darkModeToggle = document.getElementById('darkModeToggle');
if (darkModeToggle) {
const prefersDarkMode = localStorage.getItem('huntarr-dark-mode') === 'true';
darkModeToggle.checked = prefersDarkMode;
darkModeToggle.addEventListener('change', function() {
const isDarkMode = this.checked;
document.body.classList.toggle('dark-theme', isDarkMode);
localStorage.setItem('huntarr-dark-mode', isDarkMode);
});
}
// Settings now use manual save - no auto-save setup
console.log('[huntarrUI] Settings using manual save - skipping auto-save setup');
// Auto-save enabled - no need to warn about unsaved changes
// Stateful management reset button
const resetStatefulBtn = document.getElementById('reset_stateful_btn');
if (resetStatefulBtn) {
resetStatefulBtn.addEventListener('click', () => this.resetStatefulManagement());
}
// Stateful management hours input
const statefulHoursInput = document.getElementById('stateful_management_hours');
if (statefulHoursInput) {
statefulHoursInput.addEventListener('change', () => {
this.updateStatefulExpirationOnUI();
});
}
// Handle window hash change
window.addEventListener('hashchange', (e) => {
// Check for unsaved changes before navigation
if (window._hasUnsavedChanges) {
var newHash = new URL(e.newURL).hash;
// Revert navigation immediately
history.pushState(null, null, e.oldURL);
if (window.HuntarrConfirm && window.HuntarrConfirm.show) {
window.HuntarrConfirm.show({
title: 'Unsaved Changes',
message: 'You have unsaved changes that will be lost if you leave.',
confirmLabel: 'Go Back',
cancelLabel: 'Leave',
onConfirm: () => {
// Stay — navigation already reverted above, modal closes
},
onCancel: () => {
window._hasUnsavedChanges = false;
window.location.hash = newHash;
}
});
} else {
if (!confirm('You have unsaved changes that will be lost. Leave anyway?')) {
return;
}
window._hasUnsavedChanges = false;
window.location.hash = newHash;
}
return;
}
this.handleHashNavigation(window.location.hash);
});
// Handle page unload/refresh
window.addEventListener('beforeunload', (e) => {
if (window._hasUnsavedChanges) {
e.preventDefault();
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
return e.returnValue;
}
});
// Settings form delegation - now triggers auto-save
const settingsFormContainer = document.querySelector('#settingsSection');
if (settingsFormContainer) {
// Settings now use manual save - remove auto-save event listeners
console.log('[huntarrUI] Settings section using manual save - no auto-save listeners');
}
// Auto-save enabled - no need for beforeunload warnings
// NOTE: Initial hash navigation is handled in init() after setupEventListeners() returns.
// Do NOT call handleHashNavigation here to avoid double-navigation on page load.
// HISTORY: Listen for change on #historyAppSelect
const historyAppSelect = document.getElementById('historyAppSelect');
if (historyAppSelect) {
historyAppSelect.addEventListener('change', (e) => {
const app = e.target.value;
this.handleHistoryOptionChange(app);
});
}
// Reset stats button
const resetButton = document.getElementById('reset-stats');
if (resetButton) {
resetButton.addEventListener('click', (e) => {
e.preventDefault();
this.resetMediaStats();
});
}
},
// Setup logo handling to prevent flashing during navigation
setupLogoHandling: function() {
if (window.HuntarrTheme) {
window.HuntarrTheme.setupLogoHandling();
}
},
// Navigation handling
handleNavigation: function(e) {
if (window.HuntarrNavigation) {
window.HuntarrNavigation.handleNavigation(e);
}
},
handleHashNavigation: function(hash) {
if (window.HuntarrNavigation) {
window.HuntarrNavigation.handleHashNavigation(hash);
}
},
// switchSection() extracted to app-sections.js (loaded after app.js in bundle-app.js)
// ─── Sidebar switching (unified sidebar — expand groups) ───
// With the unified sidebar, there's only one #sidebar element.
// These functions now delegate to the sidebar.html inline expandSidebarGroup().
_hideAllSidebars: function() {
// No-op: only one sidebar now, always visible
},
showMainSidebar: function() {
// Let setActiveNavItem handle group expansion based on hash
if (typeof setActiveNavItem === 'function') setActiveNavItem();
},
_maybeShowWelcome: function() {
// Settings must be loaded before we can check preferences
if (!window.huntarrUI || !window.huntarrUI.originalSettings || !window.huntarrUI.originalSettings.general) {
return; // Settings not loaded yet — will be retried after settings load
}
// Check if already dismissed
var dismissed = HuntarrUtils.getUIPreference('welcome-dismissed', false);
if (dismissed) return;
// Show the welcome modal
var modal = document.getElementById('huntarr-welcome-modal');
if (!modal) return;
modal.style.display = 'flex';
// Wire up dismiss handlers (only once)
if (!modal._welcomeWired) {
modal._welcomeWired = true;
var dismissBtn = document.getElementById('huntarr-welcome-dismiss');
var closeBtn = document.getElementById('huntarr-welcome-close');
var backdrop = document.getElementById('huntarr-welcome-backdrop');
var dismiss = function() {
modal.style.display = 'none';
HuntarrUtils.setUIPreference('welcome-dismissed', true);
};
if (dismissBtn) dismissBtn.addEventListener('click', dismiss);
if (closeBtn) closeBtn.addEventListener('click', dismiss);
if (backdrop) backdrop.addEventListener('click', dismiss);
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal.style.display === 'flex') dismiss();
});
}
},
_updateMainSidebarBetaVisibility: function() {
// Partner Projects always visible in unified sidebar
},
showAppsSidebar: function() {
if (typeof expandSidebarGroup === 'function') expandSidebarGroup('sidebar-group-apps');
if (typeof setActiveNavItem === 'function') setActiveNavItem();
},
showSettingsSidebar: function() {
// Settings now lives under System group
if (typeof expandSidebarGroup === 'function') expandSidebarGroup('sidebar-group-system');
if (typeof setActiveNavItem === 'function') setActiveNavItem();
},
showRequestarrSidebar: function() {
if (typeof expandSidebarGroup === 'function') expandSidebarGroup('sidebar-group-requests');
if (typeof setActiveNavItem === 'function') setActiveNavItem();
},
showTVHuntSidebar: function() {
this.showMovieHuntSidebar();
},
showMovieHuntSidebar: function() {
if (typeof expandSidebarGroup === 'function') expandSidebarGroup('sidebar-group-media-hunt');
if (window.HuntarrNavigation && typeof window.HuntarrNavigation.updateMovieHuntSidebarActive === 'function') {
window.HuntarrNavigation.updateMovieHuntSidebarActive();
}
},
showNzbHuntSidebar: function() {
if (typeof expandSidebarGroup === 'function') expandSidebarGroup('sidebar-group-nzb-hunt');
if (typeof setActiveNavItem === 'function') setActiveNavItem();
},
/** NZB Hunt sidebar is now always visible (controlled by applyFeatureFlags / enable_media_hunt).
* Kept as no-op for backward compatibility with callers. */
_refreshNzbHuntSidebarGroup: function() {
// No-op: sidebar visibility is handled by applyFeatureFlags()
},
/** Keep all Movie Hunt sidebar icons visible - no hiding when navigating between sections. */
_updateMovieHuntSidebarSettingsOnlyVisibility: function() {
// All navigation items remain visible for easier navigation
},
/** When in instance-editor for indexer/client, keep Index Master or Clients nav item highlighted. */
_highlightMovieHuntNavForEditor: function(appType) {
var subGroup = document.getElementById('index-master-sub');
if (subGroup) subGroup.classList.add('expanded');
// Query from unified sidebar
var items = document.querySelectorAll('#sidebar-group-media-hunt .nav-item');
for (var i = 0; i < items.length; i++) items[i].classList.remove('active');
var nav = appType === 'indexer' ? document.getElementById('movieHuntIndexMasterNav') : document.getElementById('movieHuntIndexMasterClientsNav');
if (nav) nav.classList.add('active');
},
/** Legacy: was used to show/hide Movie Hunt in Core by dev_mode. Movie Hunt is now in Beta and always visible. */
updateMovieHuntNavVisibility: function() {
// No-op in unified sidebar
},
// Simple event source disconnection for compatibility
disconnectAllEventSources: function() {
// Delegate to LogsModule if it exists
if (window.LogsModule && typeof window.LogsModule.disconnectAllEventSources === 'function') {
window.LogsModule.disconnectAllEventSources();
}
// Clear local references
this.eventSources = {};
},
// App tab switching
handleAppTabChange: function(e) {
const app = e.target.getAttribute('data-app');
if (!app) return;
// Update active tab
this.elements.appTabs.forEach(tab => {
tab.classList.remove('active');
});
e.target.classList.add('active');
// Let LogsModule handle app switching to preserve pagination
this.currentApp = app;
if (window.LogsModule && typeof window.LogsModule.handleAppChange === 'function') {
window.LogsModule.handleAppChange(app);
}
},
// Log option dropdown handling - Delegated to LogsModule
// (Removed to prevent conflicts with LogsModule.handleLogOptionChange)
// History option dropdown handling
handleHistoryOptionChange: function(app) {
if (window.HuntarrUIHandlers) {
window.HuntarrUIHandlers.handleHistoryOptionChange(app);
}
},
// Update the history placeholder text based on the selected app
updateHistoryPlaceholder: function(app) {
if (window.HuntarrUIHandlers) {
window.HuntarrUIHandlers.updateHistoryPlaceholder(app);
}
},
// Settings option handling
handleSettingsOptionChange: function(e) {
if (window.HuntarrUIHandlers) {
window.HuntarrUIHandlers.handleSettingsOptionChange(e);
}
},
// Compatibility methods that delegate to LogsModule
connectToLogs: function() {
if (window.HuntarrLogs) {
window.HuntarrLogs.connectToLogs();
}
},
clearLogs: function() {
if (window.LogsModule && typeof window.LogsModule.clearLogs === 'function') {
window.LogsModule.clearLogs(true); // true = from user action (e.g. button/menu)
}
},
insertLogInChronologicalOrder: function(newLogEntry) {
if (window.HuntarrLogs) {
window.HuntarrLogs.insertLogInChronologicalOrder(newLogEntry);
}
},
parseLogTimestamp: function(logEntry) {
return window.HuntarrLogs ? window.HuntarrLogs.parseLogTimestamp(logEntry) : null;
},
searchLogs: function() {
if (window.HuntarrLogs) {
window.HuntarrLogs.searchLogs();
}
},
simpleHighlightMatch: function(logEntry, searchText) {
if (window.HuntarrLogs) {
window.HuntarrLogs.simpleHighlightMatch(logEntry, searchText);
}
},
clearLogSearch: function() {
if (window.HuntarrLogs) {
window.HuntarrLogs.clearLogSearch();
}
},
// Settings handling
loadAllSettings: function() {
if (window.HuntarrSettings) {
window.HuntarrSettings.loadAllSettings();
}
},
populateSettingsForm: function(app, appSettings) {
if (window.HuntarrSettings) {
window.HuntarrSettings.populateSettingsForm(app, appSettings);
}
},
// Called when any setting input changes in the active tab
markSettingsAsChanged() {
console.log("[huntarrUI] markSettingsAsChanged called, current state:", this.settingsChanged);
if (!this.settingsChanged) {
console.log("[huntarrUI] Settings marked as changed. Enabling save button.");
this.settingsChanged = true;
this.updateSaveResetButtonState(true); // Enable buttons
} else {
console.log("[huntarrUI] Settings already marked as changed.");
}
},
saveSettings: function() {
if (window.HuntarrSettings) {
window.HuntarrSettings.saveSettings();
}
},
// Update save button state
updateSaveResetButtonState(enable) {
const saveBtn = document.getElementById('settings-save-button');
const notifSaveBtn = document.getElementById('notifications-save-button');
console.log('[huntarrUI] updateSaveResetButtonState called with enable:', enable);
console.log('[huntarrUI] Found buttons - settings:', !!saveBtn, 'notifications:', !!notifSaveBtn);
[saveBtn, notifSaveBtn].forEach(btn => {
if (!btn) return;
console.log('[huntarrUI] Updating button:', btn.id, 'enabled:', enable);
if (enable) {
btn.disabled = false;
btn.style.background = '#dc2626'; // Red color for enabled state
btn.style.color = '#ffffff';
btn.style.borderColor = '#b91c1c';
btn.style.cursor = 'pointer';
btn.style.boxShadow = '0 0 10px rgba(220, 38, 38, 0.3)';
} else {
btn.disabled = true;
btn.style.background = '#6b7280';
btn.style.color = '#9ca3af';
btn.style.borderColor = '#4b5563';
btn.style.cursor = 'not-allowed';
btn.style.boxShadow = 'none';
}
});
},
// Setup auto-save for settings
setupSettingsAutoSave: function() {
if (window.HuntarrSettings) {
window.HuntarrSettings.setupSettingsAutoSave();
}
},
// Trigger immediate auto-save
triggerSettingsAutoSave: function() {
if (window.HuntarrSettings) {
window.HuntarrSettings.triggerSettingsAutoSave();
}
},
// Auto-save settings function
autoSaveSettings: function(app) {
if (window.HuntarrSettings) {
window.HuntarrSettings.autoSaveSettings(app);
}
},
// Clean URL by removing special characters from the end
cleanUrlString: function(url) {
if (!url) return "";
// Trim whitespace first
let cleanUrl = url.trim();
// First remove any trailing slashes
cleanUrl = cleanUrl.replace(/[\/\\]+$/g, '');
// Then remove any other trailing special characters
// This regex will match any special character at the end that is not alphanumeric, hyphen, period, or underscore
return cleanUrl.replace(/[^a-zA-Z0-9\-\._]$/g, '');
},
// Get settings from the form, updated to handle instances consistently
getFormSettings: function(app) {
return window.HuntarrSettings ? window.HuntarrSettings.getFormSettings(app) : null;
},
// Test notification functionality
testNotification: function() {
if (window.HuntarrSettings) {
window.HuntarrSettings.testNotification();
}
},
autoSaveGeneralSettings: function(silent = false) {
return window.HuntarrSettings ? window.HuntarrSettings.autoSaveGeneralSettings(silent) : Promise.resolve();
},
autoSaveSwaparrSettings: function(silent = false) {
return window.HuntarrSettings ? window.HuntarrSettings.autoSaveSwaparrSettings(silent) : Promise.resolve();
},
// Handle instance management events
setupInstanceEventHandlers: function() {
if (window.HuntarrInstances) {
window.HuntarrInstances.setupInstanceEventHandlers();
}
},
// Add a new instance to the app
addAppInstance: function(appName) {
if (window.HuntarrInstances) {
window.HuntarrInstances.addAppInstance(appName);
}
},
// Remove an instance
removeAppInstance: function(appName, instanceId) {
if (window.HuntarrInstances) {
window.HuntarrInstances.removeAppInstance(appName, instanceId);
}
},
// Test connection for a specific instance
testInstanceConnection: function(appName, instanceId, url, apiKey) {
if (window.HuntarrInstances) {
window.HuntarrInstances.testInstanceConnection(appName, instanceId, url, apiKey);
}
},
// Helper function to translate HTTP error codes to user-friendly messages
getConnectionErrorMessage: function(status) {
return window.HuntarrInstances ? window.HuntarrInstances.getConnectionErrorMessage(status) : `Error ${status}`;
},
// App connections
checkAppConnections: function() {
if (window.HuntarrStats) {
window.HuntarrStats.checkAppConnections();
}
},
checkAppConnection: function(app) {
if (window.HuntarrStats) {
return window.HuntarrStats.checkAppConnection(app);
}
return Promise.resolve();
},
updateConnectionStatus: function(app, statusData) {
if (window.HuntarrStats) {
window.HuntarrStats.updateConnectionStatus(app, statusData);
}
},
// Centralized function to update empty state visibility based on all configured apps
updateEmptyStateVisibility: function() {
if (window.HuntarrStats) {
window.HuntarrStats.updateEmptyStateVisibility();
}
},
// Load and update Swaparr status card
loadSwaparrStatus: function() {
// Delegate to Swaparr module
if (window.HuntarrSwaparr) {
window.HuntarrSwaparr.loadSwaparrStatus();
}
},
// Setup Swaparr Reset buttons
setupSwaparrResetCycle: function() {
// Delegate to Swaparr module
if (window.HuntarrSwaparr) {
window.HuntarrSwaparr.setupSwaparrResetCycle();
}
},
// Reset Swaparr data function
resetSwaparrData: function() {
// Delegate to Swaparr module
if (window.HuntarrSwaparr) {
window.HuntarrSwaparr.resetSwaparrData();
}
},
// Update Swaparr stats display with animation
updateSwaparrStatsDisplay: function(stats) {
// Delegate to Swaparr module
if (window.HuntarrSwaparr) {
window.HuntarrSwaparr.updateSwaparrStatsDisplay(stats);
}
},
// Setup Swaparr status polling
setupSwaparrStatusPolling: function() {
// Delegate to Swaparr module
if (window.HuntarrSwaparr) {
window.HuntarrSwaparr.setupSwaparrStatusPolling();
}
},
// Prowlarr delegates — implementations in modules/features/prowlarr.js
loadProwlarrStatus: function() { if (window.HuntarrProwlarr) window.HuntarrProwlarr.loadProwlarrStatus(); },
loadProwlarrIndexers: function() { if (window.HuntarrProwlarr) window.HuntarrProwlarr.loadProwlarrIndexers(); },
loadProwlarrStats: function() { if (window.HuntarrProwlarr) window.HuntarrProwlarr.loadProwlarrStats(); },
updateIndexersList: function(d, e) { if (window.HuntarrProwlarr) window.HuntarrProwlarr.updateIndexersList(d, e); },
updateProwlarrStatistics: function(s, e) { if (window.HuntarrProwlarr) window.HuntarrProwlarr.updateProwlarrStatistics(s, e); },
showIndexerStats: function(n) { if (window.HuntarrProwlarr) window.HuntarrProwlarr.showIndexerStats(n); },
showOverallStats: function() { if (window.HuntarrProwlarr) window.HuntarrProwlarr.showOverallStats(); },
// User
loadUsername: function() {
if (window.HuntarrVersion) {
window.HuntarrVersion.loadUsername();
}
},
// Check if local access bypass is enabled and update UI accordingly
checkLocalAccessBypassStatus: function() {
if (window.HuntarrAuth) {
window.HuntarrAuth.checkLocalAccessBypassStatus();
}
},
updateUIForLocalAccessBypass: function(isEnabled) {
if (window.HuntarrAuth) {
window.HuntarrAuth.updateUIForLocalAccessBypass(isEnabled);
}
},
logout: function(e) {
if (window.HuntarrAuth) {
window.HuntarrAuth.logout(e);
}
},
// Media statistics handling
loadMediaStats: function() {
// Delegate to stats module
if (window.HuntarrStats) {
window.HuntarrStats.loadMediaStats();
}
},
updateStatsDisplay: function(stats, isFromCache = false) {
// Delegate to stats module
if (window.HuntarrStats) {
window.HuntarrStats.updateStatsDisplay(stats, isFromCache);
}
},
// Helper function to parse formatted numbers back to integers
parseFormattedNumber: function(formattedStr) {
// Delegate to stats module
return window.HuntarrStats ? window.HuntarrStats.parseFormattedNumber(formattedStr) : 0;
},
animateNumber: function(element, start, end) {
// Delegate to stats module
if (window.HuntarrStats) {
window.HuntarrStats.animateNumber(element, start, end);
}
},
// Format large numbers with appropriate suffixes (K, M, B, T)
formatLargeNumber: function(num) {
// Delegate to stats module
return window.HuntarrStats ? window.HuntarrStats.formatLargeNumber(num) : num.toString();
},
resetMediaStats: function(appType = null) {
// Delegate to stats module
if (window.HuntarrStats) {
window.HuntarrStats.resetMediaStats(appType);
}
},
// Utility functions
showNotification: function(message, type) {
// Delegate to notifications module
if (window.HuntarrNotifications) {
window.HuntarrNotifications.showNotification(message, type);
}
},
capitalizeFirst: function(string) {
// Delegate to helpers module
return window.HuntarrHelpers ? window.HuntarrHelpers.capitalizeFirst(string) : string.charAt(0).toUpperCase() + string.slice(1);
},
// Load current version from version.txt
// Load current version from version.txt
loadCurrentVersion: function() {
if (window.HuntarrVersion) {
window.HuntarrVersion.loadCurrentVersion();
}
},
// Load latest version from GitHub releases
loadLatestVersion: function() {
if (window.HuntarrVersion) {
window.HuntarrVersion.loadLatestVersion();
}
},
// Load latest beta version from GitHub tags
loadBetaVersion: function() {
if (window.HuntarrVersion) {
window.HuntarrVersion.loadBetaVersion();
}
},
// Load GitHub star count
loadGitHubStarCount: function() {
if (window.HuntarrVersion) {
window.HuntarrVersion.loadGitHubStarCount();
}
},
// Update home connection status
updateHomeConnectionStatus: function() {
console.log('[huntarrUI] Updating home connection statuses...');
// This function should ideally call checkAppConnection for all relevant apps
// or use the stored configuredApps status if checkAppConnection updates it.
this.checkAppConnections(); // Re-check all connections after a save might be simplest
},
// Load stateful management info
loadStatefulInfo: function(attempts = 0, skipCache = false) {
if (window.HuntarrStateful) {
window.HuntarrStateful.loadStatefulInfo(attempts, skipCache);
}
},
// Format date nicely with time, day, and relative time indication
formatDateNicely: function(date) {
return window.HuntarrStateful ? window.HuntarrStateful.formatDateNicely(date) : date.toLocaleString();
},
// Helper function to get the user's configured timezone from settings
getUserTimezone: function() {
return window.HuntarrHelpers ? window.HuntarrHelpers.getUserTimezone() : 'UTC';
},
// Reset stateful management - clear all processed IDs
resetStatefulManagement: function() {
if (window.HuntarrStateful) {
window.HuntarrStateful.resetStatefulManagement();
}
},
// Update stateful management expiration based on hours input
updateStatefulExpirationOnUI: function() {
const hoursInput = document.getElementById('stateful_management_hours');
if (!hoursInput) return;
const hours = parseInt(hoursInput.value) || 72;
// Show updating indicator
const expiresDateEl = document.getElementById('stateful_expires_date');
const initialStateEl = document.getElementById('stateful_initial_state');
if (expiresDateEl) {
expiresDateEl.textContent = 'Updating...';
}
const url = './api/stateful/update-expiration';
const cleanedUrl = this.cleanUrlString(url);
HuntarrUtils.fetchWithTimeout(cleanedUrl, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ hours: hours }),
cache: 'no-cache'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('[huntarrUI] Stateful expiration updated successfully:', data);
// Get updated info to show proper dates
this.loadStatefulInfo();
// Show a notification
this.showNotification(`Updated expiration to ${hours} hours (${(hours/24).toFixed(1)} days)`, 'success');
} else {
throw new Error(data.message || 'Unknown error updating expiration');
}
})
.catch(error => {
console.error('Error updating stateful expiration:', error);
this.showNotification(`Failed to update expiration: ${error.message}`, 'error');
// Reset the UI
if (expiresDateEl) {
expiresDateEl.textContent = 'Error updating';
}
// Try to reload original data
setTimeout(() => this.loadStatefulInfo(), 1000);
});
},
// Add the updateStatefulExpiration method
updateStatefulExpiration: function(hours) {
if (!hours || typeof hours !== 'number' || hours <= 0) {
console.error('[huntarrUI] Invalid hours value for updateStatefulExpiration:', hours);
return;
}
console.log(`[huntarrUI] Directly updating stateful expiration to ${hours} hours`);
// Make a direct API call to update the stateful expiration
HuntarrUtils.fetchWithTimeout('./api/stateful/update-expiration', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ hours: hours })
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('[huntarrUI] Stateful expiration updated successfully:', data);
// Update the expiration date display
const expiresDateEl = document.getElementById('stateful_expires_date');
if (expiresDateEl && data.expires_date) {
expiresDateEl.textContent = data.expires_date;
}
})
.catch(error => {
console.error('[huntarrUI] Error updating stateful expiration:', error);
});
},
// Add global event handler and method to track saved settings across all apps
// Auto-save enabled - unsaved changes handlers removed
// Add a proper hasFormChanges function to compare form values with original values
hasFormChanges: function(app) {
// If we don't have original settings or current app settings, we can't compare
if (!this.originalSettings || !this.originalSettings[app]) {
return false;
}
// Get current settings from the form
const currentSettings = this.getFormSettings(app);
// For complex objects like instances, we need to stringify them for comparison
const originalJSON = JSON.stringify(this.originalSettings[app]);
const currentJSON = JSON.stringify(currentSettings);
return originalJSON !== currentJSON;
},
// Apply timezone change immediately
applyTimezoneChange: function(timezone) {
if (window.HuntarrSettings && typeof window.HuntarrSettings.applyTimezoneChange === 'function') {
window.HuntarrSettings.applyTimezoneChange(timezone);
}
},
// Apply authentication mode change immediately
applyAuthModeChange: function(authMode) {
if (window.HuntarrSettings && typeof window.HuntarrSettings.applyAuthModeChange === 'function') {
window.HuntarrSettings.applyAuthModeChange(authMode);
}
},
// Apply update checking change immediately
applyUpdateCheckingChange: function(enabled) {
if (window.HuntarrSettings && typeof window.HuntarrSettings.applyUpdateCheckingChange === 'function') {
window.HuntarrSettings.applyUpdateCheckingChange(enabled);
}
},
applyShowTrendingChange: function(enabled) {
if (window.HuntarrSettings && typeof window.HuntarrSettings.applyShowTrendingChange === 'function') {
window.HuntarrSettings.applyShowTrendingChange(enabled);
}
},
// Refresh time displays after timezone change
refreshTimeDisplays: function() {
if (window.HuntarrStateful) {
window.HuntarrStateful.refreshTimeDisplays();
}
},
// Reset the app cycle for a specific app
resetAppCycle: function(app, button) {
// Make sure we have the app and button elements
if (!app || !button) {
console.error('[huntarrUI] Missing app or button for resetAppCycle');
return;
}
// First, disable the button to prevent multiple clicks
button.disabled = true;
button.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Resetting...';
// Per-instance reset for *arr apps (card has data-instance-name)
const instanceName = button.getAttribute('data-instance-name');
let endpoint = `./api/cycle/reset/${app}`;
if (instanceName && app !== 'swaparr') {
endpoint += '?instance_name=' + encodeURIComponent(instanceName);
}
HuntarrUtils.fetchWithTimeout(endpoint, {
method: 'POST'
})
.then(response => {
if (!response.ok) {
throw new Error(`Failed to reset ${app} cycle`);
}
return response.json();
})
.then(data => {
this.showNotification(`Successfully reset ${this.capitalizeFirst(app)} cycle`, 'success');
console.log(`[huntarrUI] Reset ${app} cycle response:`, data);
// Re-enable the button with original text
button.disabled = false;
button.innerHTML = `<i class="fas fa-sync-alt"></i> Reset`;
})
.catch(error => {
console.error(`[huntarrUI] Error resetting ${app} cycle:`, error);
this.showNotification(`Error resetting ${this.capitalizeFirst(app)} cycle: ${error.message}`, 'error');
// Re-enable the button with original text
button.disabled = false;
button.innerHTML = `<i class="fas fa-sync-alt"></i> Reset`;
});
},
showDashboard: function() {
if (window.HuntarrDOM) {
window.HuntarrDOM.showDashboard();
}
},
applyFilterToSingleEntry: function(logEntry, selectedLevel) {
if (window.HuntarrLogs) {
window.HuntarrLogs.applyFilterToSingleEntry(logEntry, selectedLevel);
}
},
filterLogsByLevel: function(selectedLevel) {
if (window.HuntarrLogs) {
window.HuntarrLogs.filterLogsByLevel(selectedLevel);
}
},
// Helper method to detect JSON fragments that shouldn't be displayed as log entries
isJsonFragment: function(logString) {
if (!logString || typeof logString !== 'string') return false;
const trimmed = logString.trim();
// Check for common JSON fragment patterns
const jsonPatterns = [
/^"[^"]*":\s*"[^"]*",?$/, // "key": "value",
/^"[^"]*":\s*\d+,?$/, // "key": 123,
/^"[^"]*":\s*true|false,?$/, // "key": true,
/^"[^"]*":\s*null,?$/, // "key": null,
/^"[^"]*":\s*\[[^\]]*\],?$/, // "key": [...],
/^"[^"]*":\s*\{[^}]*\},?$/, // "key": {...},
/^\s*\{?\s*$/, // Just opening brace or whitespace
/^\s*\}?,?\s*$/, // Just closing brace
/^\s*\[?\s*$/, // Just opening bracket
/^\s*\]?,?\s*$/, // Just closing bracket
/^,?\s*$/, // Just comma or whitespace
/^[^"]*':\s*[^,]*,.*':/, // Mid-object fragments like "g_items': 1, 'hunt_upgrade_items': 0"
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*\d+,/, // Property names starting without quotes
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*True|False,/, // Boolean properties without opening quotes
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*'[^']*',/, // String properties without opening quotes
/.*':\s*\d+,.*':\s*\d+,/, // Multiple numeric properties in sequence
/.*':\s*True,.*':\s*False,/, // Multiple boolean properties in sequence
/.*':\s*'[^']*',.*':\s*'[^']*',/, // Multiple string properties in sequence
/^"[^"]*":\s*\[$/, // JSON key with opening bracket: "global": [
/^[a-zA-Z_][a-zA-Z0-9_\s]*:\s*\[$/, // Property key with opening bracket: global: [
/^[a-zA-Z_][a-zA-Z0-9_\s]*:\s*\{$/, // Property key with opening brace: config: {
/^[a-zA-Z_]+\s+(Mode|Setting|Config|Option):\s*(True|False|\d+)$/i, // Config fragments: "ug Mode: False"
/^[a-zA-Z_]+\s*Mode:\s*(True|False)$/i, // Mode fragments: "Debug Mode: False"
/^[a-zA-Z_]+\s*Setting:\s*.*$/i, // Setting fragments
/^[a-zA-Z_]+\s*Config:\s*.*$/i // Config fragments
];
return jsonPatterns.some(pattern => pattern.test(trimmed));
},
// Helper method to detect other invalid log lines
isInvalidLogLine: function(logString) {
if (!logString || typeof logString !== 'string') return true;
const trimmed = logString.trim();
// Skip empty lines or lines with only whitespace
if (trimmed.length === 0) return true;
// Skip lines that are clearly not log entries
if (trimmed.length < 10) return true; // Too short to be a meaningful log
// Skip lines that look like HTTP headers or other metadata
if (/^(HTTP\/|Content-|Connection:|Host:|User-Agent:)/i.test(trimmed)) return true;
// Skip partial words or fragments that don't form complete sentences
if (/^[a-zA-Z]{1,5}\s+(Mode|Setting|Config|Debug|Info|Error|Warning):/i.test(trimmed)) return true;
// Skip single words that are clearly fragments
if (/^[a-zA-Z]{1,8}$/i.test(trimmed)) return true;
// Skip lines that start with partial words and contain colons (config fragments)
if (/^[a-z]{1,8}\s*[A-Z]/i.test(trimmed) && trimmed.includes(':')) return true;
return false;
},
// Load instance-specific state management information
loadInstanceStateInfo: function(appType, instanceIndex) {
if (window.HuntarrStateful) {
window.HuntarrStateful.loadInstanceStateInfo(appType, instanceIndex);
}
},
// Update the instance state management display
updateInstanceStateDisplay: function(appType, instanceIndex, summaryData, instanceName, customHours) {
if (window.HuntarrStateful) {
window.HuntarrStateful.updateInstanceStateDisplay(appType, instanceIndex, summaryData, instanceName, customHours);
}
},
// Refresh state management timezone displays when timezone changes
refreshStateManagementTimezone: function() {
if (window.HuntarrStateful) {
window.HuntarrStateful.refreshStateManagementTimezone();
}
},
// Reload state management displays after timezone change
reloadStateManagementDisplays: function() {
if (window.HuntarrStateful) {
window.HuntarrStateful.reloadStateManagementDisplays();
}
},
// Load state management data for a specific instance
loadStateManagementForInstance: function(appType, instanceIndex, instanceName) {
if (window.HuntarrStateful) {
window.HuntarrStateful.loadStateManagementForInstance(appType, instanceIndex, instanceName);
}
},
updateRequestarrSidebarActive: function() {
if (window.HuntarrRequestarr) {
window.HuntarrRequestarr.updateRequestarrSidebarActive();
}
},
updateRequestarrNavigation: function(view) {
if (window.HuntarrRequestarr) {
window.HuntarrRequestarr.updateRequestarrNavigation(view);
}
},
setupRequestarrNavigation: function() {
if (window.HuntarrRequestarr) {
window.HuntarrRequestarr.setupRequestarrNavigation();
}
},
setupMovieHuntNavigation: function() {
if (window.HuntarrNavigation && window.HuntarrNavigation.setupMovieHuntNavigation) {
window.HuntarrNavigation.setupMovieHuntNavigation();
}
},
setupTVHuntNavigation: function() {
if (window.HuntarrNavigation && window.HuntarrNavigation.setupTVHuntNavigation) {
window.HuntarrNavigation.setupTVHuntNavigation();
}
},
setupNzbHuntNavigation: function() {
if (window.HuntarrNavigation && window.HuntarrNavigation.setupNzbHuntNavigation) {
window.HuntarrNavigation.setupNzbHuntNavigation();
}
},
updateAppsSidebarActive: function() {
if (window.HuntarrNavigation) {
window.HuntarrNavigation.updateAppsSidebarActive();
}
},
updateSettingsSidebarActive: function() {
if (window.HuntarrNavigation) {
window.HuntarrNavigation.updateSettingsSidebarActive();
}
},
setupAppsNavigation: function() {
if (window.HuntarrNavigation) {
window.HuntarrNavigation.setupAppsNavigation();
}
},
setupSettingsNavigation: function() {
if (window.HuntarrNavigation) {
window.HuntarrNavigation.setupSettingsNavigation();
}
},
setupSystemNavigation: function() {
if (window.HuntarrNavigation && window.HuntarrNavigation.setupSystemTabs) {
window.HuntarrNavigation.setupSystemTabs();
}
},
initializeLogsSettings: function() {
if (window.HuntarrInit) {
window.HuntarrInit.initializeLogsSettings();
}
},
initializeSettings: function() {
if (window.HuntarrInit) {
window.HuntarrInit.initializeSettings();
}
},
initializeNotifications: function() {
if (window.HuntarrInit) {
window.HuntarrInit.initializeNotifications();
}
},
initializeBackupRestore: function() {
if (window.HuntarrInit) {
window.HuntarrInit.initializeBackupRestore();
}
},
initializeProwlarr: function() {
if (window.HuntarrInit) {
window.HuntarrInit.initializeProwlarr();
}
},
initializeUser: function() {
if (window.HuntarrInit) {
window.HuntarrInit.initializeUser();
}
},
initializeSwaparr: function() {
if (window.HuntarrInit) {
window.HuntarrInit.initializeSwaparr();
}
},
loadSwaparrApps: function() {
if (window.HuntarrSwaparr) {
window.HuntarrSwaparr.loadSwaparrApps();
}
},
setupProwlarrStatusPolling: function() { if (window.HuntarrProwlarr) window.HuntarrProwlarr.setupProwlarrStatusPolling(); },
setupIndexerHuntHome: function() { if (window.HuntarrIndexerHuntHome) window.HuntarrIndexerHuntHome.setup(); },
};
// Note: redirectToSwaparr function removed - Swaparr now has its own dedicated section
// Initialize when document is ready
document.addEventListener('DOMContentLoaded', function() {
// Initialize TMDB image cache first
if (typeof tmdbImageCache !== 'undefined') {
tmdbImageCache.init().catch(error => {
console.error('[app.js] Failed to initialize TMDB image cache:', error);
});
}
huntarrUI.init();
// Initialize our enhanced UI features
if (typeof StatsTooltips !== 'undefined') {
StatsTooltips.init();
}
if (typeof CardHoverEffects !== 'undefined') {
CardHoverEffects.init();
}
if (typeof CircularProgress !== 'undefined') {
CircularProgress.init();
}
if (typeof BackgroundPattern !== 'undefined') {
BackgroundPattern.init();
}
// Initialize per-instance reset button listeners
if (typeof SettingsForms !== 'undefined' && typeof SettingsForms.setupInstanceResetListeners === 'function') {
SettingsForms.setupInstanceResetListeners();
}
// Initialize UserModule when available (guard against duplicate construction)
if (typeof UserModule !== 'undefined' && !window.userModule) {
console.log('[huntarrUI] UserModule available, initializing...');
window.userModule = new UserModule();
}
});
// Expose huntarrUI to the global scope for access by app modules
window.huntarrUI = huntarrUI;
// Expose state management timezone refresh function globally for settings forms
window.refreshStateManagementTimezone = function() {
if (window.huntarrUI && typeof window.huntarrUI.refreshStateManagementTimezone === 'function') {
window.huntarrUI.refreshStateManagementTimezone();
} else {
console.warn('[huntarrUI] refreshStateManagementTimezone function not available');
}
};