mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-04-20 19:46:57 -04:00
588 lines
29 KiB
JavaScript
588 lines
29 KiB
JavaScript
(function() {
|
|
/**
|
|
* Huntarr - Apps Module
|
|
* Handles displaying and managing app settings for media server applications
|
|
*/
|
|
|
|
var appsModule = {
|
|
// State
|
|
currentApp: null,
|
|
isLoading: false,
|
|
settingsChanged: false, // Legacy flag (auto-save enabled)
|
|
originalSettings: {}, // Store original settings to compare
|
|
|
|
// DOM elements
|
|
elements: {},
|
|
|
|
// Initialize the apps module for a specific app
|
|
init: function(appType) {
|
|
// Initialize state
|
|
this.currentApp = appType || null;
|
|
this.settingsChanged = false; // Legacy flag (auto-save enabled)
|
|
this.originalSettings = {}; // Store original settings to compare
|
|
|
|
// Set a global flag to indicate we've loaded
|
|
window._appsModuleLoaded = true;
|
|
|
|
// Add global variable to track if we're in the middle of saving
|
|
window._appsCurrentlySaving = false;
|
|
|
|
// Add global variable to disable change detection temporarily
|
|
window._appsSuppressChangeDetection = false;
|
|
|
|
// Cache DOM elements
|
|
this.cacheElements();
|
|
|
|
// Set up event listeners
|
|
this.setupEventListeners();
|
|
|
|
// Initialize state
|
|
this.settingsChanged = false;
|
|
|
|
// Load the specific app if provided
|
|
if (appType) {
|
|
this.loadAppSettings(appType);
|
|
}
|
|
|
|
// Auto-save enabled - no unsaved changes detection needed
|
|
},
|
|
|
|
// Auto-save enabled - unsaved changes handlers removed
|
|
|
|
// Cache DOM elements
|
|
cacheElements: function() {
|
|
this.elements = {
|
|
// Apps panels - now individual sections
|
|
appAppsPanels: document.querySelectorAll('.app-apps-panel'),
|
|
|
|
// Individual app sections
|
|
sonarrSection: document.getElementById('sonarrSection'),
|
|
radarrSection: document.getElementById('radarrSection'),
|
|
lidarrSection: document.getElementById('lidarrSection'),
|
|
readarrSection: document.getElementById('readarrSection'),
|
|
whisparrSection: document.getElementById('whisparrSection'),
|
|
erosSection: document.getElementById('erosSection'),
|
|
|
|
// Controls - auto-save enabled, no save button needed
|
|
};
|
|
},
|
|
|
|
// Set up event listeners
|
|
setupEventListeners: function() {
|
|
// No dropdown needed anymore - apps have individual sections
|
|
// Auto-save enabled - no save button needed
|
|
},
|
|
|
|
// Load specific app settings
|
|
loadApp: function(appType) {
|
|
this.currentApp = appType;
|
|
this.loadAppSettings(appType);
|
|
},
|
|
|
|
// Load app settings
|
|
loadAppSettings: function(app) {
|
|
console.log(`[Apps] Loading settings for ${app}`);
|
|
|
|
// Get the container to put the settings in - now using individual app sections
|
|
const appPanel = document.getElementById(app + 'Apps');
|
|
if (!appPanel) {
|
|
console.error(`App panel not found for ${app}`);
|
|
return;
|
|
}
|
|
|
|
// Clear existing content
|
|
appPanel.innerHTML = '<div class="loading-panel"><i class="fas fa-spinner fa-spin"></i> Loading settings...</div>';
|
|
|
|
// Fetch settings for this app
|
|
HuntarrUtils.fetchWithTimeout(`./api/settings/${app}`)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(appSettings => {
|
|
console.log(`[Apps] Received settings for ${app}:`, appSettings);
|
|
|
|
// Clear loading message
|
|
appPanel.innerHTML = '';
|
|
|
|
// Create a form container with the app-type attribute
|
|
const formElement = document.createElement('form');
|
|
formElement.classList.add('settings-form');
|
|
formElement.setAttribute('data-app-type', app);
|
|
appPanel.appendChild(formElement);
|
|
|
|
// Generate the form using SettingsForms module
|
|
if (typeof SettingsForms !== 'undefined') {
|
|
// Update global settings store for modal access
|
|
if (window.huntarrUI) {
|
|
if (!window.huntarrUI.originalSettings) {
|
|
window.huntarrUI.originalSettings = {};
|
|
}
|
|
window.huntarrUI.originalSettings[app] = appSettings;
|
|
}
|
|
|
|
const formFunction = SettingsForms[`generate${app.charAt(0).toUpperCase()}${app.slice(1)}Form`];
|
|
if (typeof formFunction === 'function') {
|
|
// Use .call() to set the 'this' context correctly
|
|
formFunction.call(SettingsForms, formElement, appSettings);
|
|
|
|
// Update duration displays for this app
|
|
if (typeof SettingsForms.updateDurationDisplay === 'function') {
|
|
SettingsForms.updateDurationDisplay();
|
|
}
|
|
|
|
// Explicitly ensure connection status checking is set up for all supported apps
|
|
const supportedApps = ['radarr', 'sonarr', 'lidarr', 'readarr', 'whisparr', 'eros'];
|
|
if (supportedApps.includes(app) && typeof SettingsForms.setupInstanceManagement === 'function') {
|
|
// Find the instances container and set up connection status checking
|
|
// The container might have class 'instances-container' or ID like 'sonarr-instances-grid'
|
|
const instancesContainer = formElement.querySelector('.instances-container') ||
|
|
formElement.querySelector('.instance-card-grid') ||
|
|
document.getElementById(`${app}-instances-grid`);
|
|
|
|
if (instancesContainer) {
|
|
const instanceCount = appSettings.instances ? appSettings.instances.length : 0;
|
|
console.log(`[Apps] Setting up connection status checking for ${app} with ${instanceCount} instances`);
|
|
// Add a small delay to ensure all instance cards are rendered before testing connections
|
|
setTimeout(() => {
|
|
SettingsForms.testAllInstanceConnections(app);
|
|
}, 100);
|
|
} else {
|
|
console.warn(`[Apps] No instances container found for ${app}, connection status checking may not work`);
|
|
}
|
|
} else {
|
|
console.log(`[Apps] Skipping connection status setup for ${app} (supported: ${supportedApps.includes(app)}, function available: ${typeof SettingsForms.setupInstanceManagement})`);
|
|
}
|
|
|
|
// Store original form values after form is generated
|
|
// Add a small delay to ensure all form elements are fully populated
|
|
setTimeout(() => {
|
|
this.storeOriginalFormValues(appPanel);
|
|
console.log(`[Apps] Original values stored for ${app} after form generation`);
|
|
console.log(`[Apps] Stored ${Object.keys(this.originalSettings).length} original values for ${app}`);
|
|
}, 50);
|
|
|
|
// Add change listener to detect modifications
|
|
this.addFormChangeListeners(formElement);
|
|
} else {
|
|
console.warn(`Form generation function not found for ${app}`);
|
|
appPanel.innerHTML = `<div class="settings-message">Settings for ${app.charAt(0).toUpperCase() + app.slice(1)} are not available.</div>`;
|
|
}
|
|
} else {
|
|
console.error('SettingsForms module not found');
|
|
appPanel.innerHTML = '<div class="error-panel">Unable to generate settings form. Please reload the page.</div>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error(`Error loading ${app} settings:`, error);
|
|
appPanel.innerHTML = `<div class="error-panel"><i class="fas fa-exclamation-triangle"></i> Error loading settings: ${error.message}</div>`;
|
|
});
|
|
},
|
|
|
|
// Add change listeners to form elements (auto-save removed - now using manual save)
|
|
addFormChangeListeners: function(form) {
|
|
if (!form) return;
|
|
|
|
const appType = form.getAttribute('data-app-type');
|
|
console.log(`[Apps] Skipping auto-save listeners for ${appType} - now using manual save`);
|
|
|
|
// Auto-save has been removed - apps now use manual save buttons
|
|
// No longer adding change listeners or mutation observers for auto-save functionality
|
|
},
|
|
|
|
// Auto-save settings silently in background
|
|
autoSaveSettings: function(appType, form) {
|
|
console.log(`[Apps] Auto-saving settings for ${appType}`);
|
|
|
|
// Get the app panel
|
|
const appPanel = form.closest('.app-apps-panel') || document.getElementById(`${appType}Apps`);
|
|
if (!appPanel) {
|
|
console.error(`[Apps] Could not find app panel for ${appType}`);
|
|
return;
|
|
}
|
|
|
|
let settings;
|
|
try {
|
|
// Get settings from the form
|
|
settings = SettingsForms.getFormSettings(appPanel, appType);
|
|
console.log(`[Apps] Collected settings for auto-save (${appType}):`, settings);
|
|
} catch (error) {
|
|
console.error(`[Apps] Error collecting settings for auto-save (${appType}):`, error);
|
|
return;
|
|
}
|
|
|
|
// Send settings to the server silently
|
|
HuntarrUtils.fetchWithTimeout(`./api/settings/${appType}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(settings)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log(`[Apps] ${appType} settings auto-saved successfully:`, data);
|
|
})
|
|
.catch(error => {
|
|
console.error(`[Apps] Error auto-saving ${appType} settings:`, error);
|
|
// Only show error notifications for failed saves, not success
|
|
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.showNotification === 'function') {
|
|
huntarrUI.showNotification(`Error auto-saving ${appType} settings`, 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
// Check if the form has actual changes compared to original values
|
|
hasFormChanges: function(form) {
|
|
if (!form) {
|
|
console.log('[Apps] hasFormChanges: No form found');
|
|
return false;
|
|
}
|
|
|
|
if (!this.originalSettings || Object.keys(this.originalSettings).length === 0) {
|
|
console.log('[Apps] hasFormChanges: No original settings found, checking if form has any values');
|
|
// If we don't have original settings yet, check if the form has any non-default values
|
|
// This handles the case where user makes changes before original values are stored
|
|
const formElements = form.querySelectorAll('input, select, textarea');
|
|
let hasNonDefaultValues = false;
|
|
formElements.forEach(element => {
|
|
if (element.type === 'button' || element.type === 'submit' || !element.id) return;
|
|
const currentValue = element.type === 'checkbox' ? element.checked : element.value;
|
|
// If there's any meaningful value, consider it a change
|
|
if (currentValue && currentValue !== '' && currentValue !== false) {
|
|
hasNonDefaultValues = true;
|
|
}
|
|
});
|
|
console.log(`[Apps] Form has non-default values: ${hasNonDefaultValues}`);
|
|
return hasNonDefaultValues;
|
|
}
|
|
|
|
let hasChanges = false;
|
|
const formElements = form.querySelectorAll('input, select, textarea');
|
|
|
|
console.log(`[Apps] Checking ${formElements.length} form elements for changes`);
|
|
console.log(`[Apps] Original settings keys:`, Object.keys(this.originalSettings));
|
|
|
|
formElements.forEach(element => {
|
|
// Skip buttons and elements without IDs
|
|
if (element.type === 'button' || element.type === 'submit' || !element.id) return;
|
|
|
|
const originalValue = this.originalSettings[element.id];
|
|
const currentValue = element.type === 'checkbox' ? element.checked : element.value;
|
|
|
|
// Only compare if we have an original value stored for this element
|
|
if (originalValue !== undefined) {
|
|
// Direct comparison for checkboxes (both should be boolean)
|
|
// String comparison for everything else
|
|
let valuesMatch;
|
|
if (element.type === 'checkbox') {
|
|
valuesMatch = originalValue === currentValue;
|
|
} else {
|
|
valuesMatch = String(originalValue) === String(currentValue);
|
|
}
|
|
|
|
if (!valuesMatch) {
|
|
console.log(`[Apps] Element changed: ${element.id}, Original: ${originalValue} (${typeof originalValue}), Current: ${currentValue} (${typeof currentValue})`);
|
|
hasChanges = true;
|
|
}
|
|
} else {
|
|
// If we don't have an original value for this element, check if it has a meaningful current value
|
|
if (element.type === 'checkbox' && currentValue === true) {
|
|
console.log(`[Apps] Checkbox ${element.id} is checked but no original value stored - considering as change`);
|
|
hasChanges = true;
|
|
} else if (element.type !== 'checkbox' && currentValue && currentValue.trim() !== '') {
|
|
console.log(`[Apps] Element ${element.id} has value '${currentValue}' but no original value stored - considering as change`);
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`[Apps] hasFormChanges result: ${hasChanges}`);
|
|
return hasChanges;
|
|
},
|
|
|
|
// Show specific app panel and hide others
|
|
showAppPanel: function(app) {
|
|
console.log(`Showing app panel for ${app}`);
|
|
// Hide all app panels
|
|
this.elements.appAppsPanels.forEach(panel => {
|
|
panel.style.display = 'none';
|
|
panel.classList.remove('active');
|
|
});
|
|
|
|
// Show the selected app panel
|
|
const appPanel = document.getElementById(`${app}Apps`);
|
|
if (appPanel) {
|
|
appPanel.style.display = 'block';
|
|
appPanel.classList.add('active');
|
|
|
|
// Ensure the panel has the correct data-app-type attribute
|
|
appPanel.setAttribute('data-app-type', app);
|
|
|
|
console.log(`App panel for ${app} is now active`);
|
|
} else {
|
|
console.error(`App panel for ${app} not found`);
|
|
}
|
|
},
|
|
|
|
// Handle app selection changes
|
|
handleAppsAppChange: function(selectedApp) {
|
|
// If called with an event, extract the value
|
|
if (selectedApp && selectedApp.target && typeof selectedApp.target.value === 'string') {
|
|
selectedApp = selectedApp.target.value;
|
|
}
|
|
if (!selectedApp || selectedApp === this.currentApp) return;
|
|
|
|
// Auto-save enabled - no navigation checks needed
|
|
// Update the select value
|
|
const appsAppSelect = document.getElementById('appsAppSelect');
|
|
if (appsAppSelect) appsAppSelect.value = selectedApp;
|
|
// Show the selected app's panel
|
|
this.showAppPanel(selectedApp);
|
|
this.currentApp = selectedApp;
|
|
// Load the newly selected app's settings
|
|
this.loadAppSettings(selectedApp);
|
|
// Reset changed state (auto-save enabled)
|
|
this.settingsChanged = false;
|
|
},
|
|
|
|
// Save apps settings - completely rewritten for reliability
|
|
saveApps: function(event) {
|
|
if (event) event.preventDefault();
|
|
|
|
console.log('[Apps] Save button clicked');
|
|
|
|
// Set a flag that we're in the middle of saving
|
|
window._appsCurrentlySaving = true;
|
|
|
|
// Get the current app from module state
|
|
const appType = this.currentApp;
|
|
if (!appType) {
|
|
console.error('No current app selected');
|
|
|
|
// Emergency fallback - try to find the visible app panel
|
|
const visiblePanel = document.querySelector('.app-apps-panel[style*="display: block"]');
|
|
if (visiblePanel && visiblePanel.id) {
|
|
// Extract app type from panel ID (e.g., "sonarrApps" -> "sonarr")
|
|
const extractedType = visiblePanel.id.replace('Apps', '');
|
|
console.log(`Fallback: Found visible panel with ID ${visiblePanel.id}, extracted app type: ${extractedType}`);
|
|
|
|
if (extractedType) {
|
|
// Continue with the extracted app type
|
|
return this.saveAppSettings(extractedType, visiblePanel);
|
|
}
|
|
}
|
|
|
|
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.showNotification === 'function') {
|
|
huntarrUI.showNotification('Error: Could not determine which app settings to save', 'error');
|
|
} else {
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) window.huntarrUI.showNotification('Error: Could not determine which app settings to save', 'error');
|
|
else alert('Error: Could not determine which app settings to save');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Direct DOM access to find the app panel
|
|
const appPanel = document.getElementById(`${appType}Apps`);
|
|
if (!appPanel) {
|
|
console.error(`App panel not found for ${appType}`);
|
|
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.showNotification === 'function') {
|
|
huntarrUI.showNotification(`Error: App panel not found for ${appType}`, 'error');
|
|
} else {
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) window.huntarrUI.showNotification('Error: App panel not found for ' + appType, 'error');
|
|
else alert('Error: App panel not found for ' + appType);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Proceed with saving for the found app panel
|
|
this.saveAppSettings(appType, appPanel);
|
|
},
|
|
|
|
// Helper function to save settings for a specific app
|
|
saveAppSettings: function(appType, appPanel) {
|
|
console.log(`Saving settings for ${appType}`);
|
|
|
|
// For Whisparr, ensure we indicate we're working with V2
|
|
let apiVersion = "";
|
|
if (appType === "whisparr") {
|
|
console.log("Saving Whisparr V2 settings");
|
|
apiVersion = "V2";
|
|
} else if (appType === "eros") {
|
|
console.log("Saving Eros (Whisparr V3) settings");
|
|
}
|
|
|
|
let settings;
|
|
try {
|
|
// Make sure the app type is set on the panel for SettingsForms
|
|
appPanel.setAttribute('data-app-type', appType);
|
|
|
|
// Get settings from the form
|
|
settings = SettingsForms.getFormSettings(appPanel, appType);
|
|
console.log(`Collected settings for ${appType}:`, settings);
|
|
} catch (error) {
|
|
console.error(`Error collecting settings for ${appType}:`, error);
|
|
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.showNotification === 'function') {
|
|
huntarrUI.showNotification(`Error collecting settings: ${error.message}`, 'error');
|
|
} else {
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) window.huntarrUI.showNotification('Error collecting settings: ' + error.message, 'error');
|
|
else alert('Error collecting settings: ' + error.message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Add specific logging for settings critical to stateful management
|
|
if (appType === 'general') {
|
|
console.log('Stateful management settings being saved:', {
|
|
statefulExpirationHours: settings.statefulExpirationHours,
|
|
command_wait_delay: settings.command_wait_delay,
|
|
command_wait_attempts: settings.command_wait_attempts
|
|
});
|
|
}
|
|
|
|
// Send settings to the server
|
|
console.log(`Sending ${appType} settings to server...`);
|
|
|
|
// Debug: Log the settings being sent, especially for general
|
|
if (appType === 'general') {
|
|
console.log('General settings being sent:', settings);
|
|
console.log('Apprise URLs being sent:', settings.apprise_urls);
|
|
}
|
|
|
|
HuntarrUtils.fetchWithTimeout(`./api/settings/${appType}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(settings)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log(`${appType} settings saved successfully:`, data);
|
|
|
|
// Temporarily suppress change detection
|
|
window._appsSuppressChangeDetection = true;
|
|
|
|
// Store the current form values as the new "original" values
|
|
this.storeOriginalFormValues(appPanel);
|
|
|
|
// Auto-save completed - reset state
|
|
this.settingsChanged = false;
|
|
|
|
// Reset the saving flag
|
|
window._appsCurrentlySaving = false;
|
|
|
|
// Ensure form elements are properly updated to reflect saved state
|
|
this.markFormAsUnchanged(appPanel);
|
|
|
|
// After a short delay, re-enable change detection
|
|
setTimeout(() => {
|
|
window._appsSuppressChangeDetection = false;
|
|
}, 1000);
|
|
|
|
// Settings auto-save notification removed per user request
|
|
})
|
|
.catch(error => {
|
|
console.error(`Error saving ${appType} settings:`, error);
|
|
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.showNotification === 'function') {
|
|
huntarrUI.showNotification(`Error saving settings: ${error.message}`, 'error');
|
|
} else {
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) window.huntarrUI.showNotification('Error saving settings: ' + error.message, 'error');
|
|
else alert('Error saving settings: ' + error.message);
|
|
}
|
|
// Reset the saving flag
|
|
window._appsCurrentlySaving = false;
|
|
});
|
|
},
|
|
|
|
// Store the current form values as the new "original" values
|
|
storeOriginalFormValues: function(appPanel) {
|
|
const form = appPanel.querySelector('form');
|
|
if (!form) return;
|
|
|
|
const originalValues = {};
|
|
const formElements = form.querySelectorAll('input, select, textarea');
|
|
formElements.forEach(element => {
|
|
// Store the appropriate value based on element type
|
|
if (element.type === 'checkbox') {
|
|
originalValues[element.id] = element.checked;
|
|
} else {
|
|
originalValues[element.id] = element.value;
|
|
}
|
|
});
|
|
|
|
this.originalSettings = originalValues;
|
|
console.log('Original form values stored:', this.originalSettings);
|
|
},
|
|
|
|
// Mark form as unchanged
|
|
markFormAsUnchanged: function(appPanel) {
|
|
const form = appPanel.querySelector('form');
|
|
if (!form) return;
|
|
|
|
// First, remove the 'changed' class from all form elements
|
|
const formElements = form.querySelectorAll('input, select, textarea');
|
|
formElements.forEach(element => {
|
|
element.classList.remove('changed');
|
|
});
|
|
|
|
// Get the app type to properly handle app-specific flags
|
|
const appType = appPanel.getAttribute('data-app-type') || '';
|
|
console.log(`Marking form as unchanged for app type: ${appType}`);
|
|
|
|
// Clear app-specific change flags
|
|
if (window._hasAppChanges && typeof window._hasAppChanges === 'object') {
|
|
window._hasAppChanges[appType] = false;
|
|
}
|
|
|
|
// Ensure we reset all change tracking for this app
|
|
try {
|
|
// Reset any form change flags
|
|
if (form.dataset) {
|
|
form.dataset.hasChanges = 'false';
|
|
}
|
|
|
|
// Clear any app-specific data attributes that might be tracking changes
|
|
appPanel.querySelectorAll('[data-changed="true"]').forEach(el => {
|
|
el.setAttribute('data-changed', 'false');
|
|
});
|
|
|
|
// Auto-save enabled - no change tracking needed
|
|
|
|
// Explicitly handle Readarr, Lidarr, and Whisparr which seem to have issues
|
|
if (appType === 'readarr' || appType === 'lidarr' || appType === 'whisparr' || appType === 'whisparrv2') {
|
|
console.log(`Special handling for ${appType} to ensure changes are cleared`);
|
|
// Force additional global state updates
|
|
if (window.huntarrUI && window.huntarrUI.formChanged) {
|
|
window.huntarrUI.formChanged[appType] = false;
|
|
}
|
|
// Auto-save enabled - no global state tracking needed
|
|
// Force immediate re-evaluation of the form state
|
|
setTimeout(() => {
|
|
this.hasFormChanges(form);
|
|
}, 10);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error in markFormAsUnchanged for ${appType}:`, error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Expose to window
|
|
window.appsModule = appsModule;
|
|
console.log('[Apps] appsModule defined and exposed to window');
|
|
})();
|