Files
Huntarr-Sonarr/frontend/static/js/apps.js

621 lines
27 KiB
JavaScript

/**
* Huntarr - Apps Module
* Handles displaying and managing app settings for media server applications
*/
const 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
init: function() {
// Initialize state
this.currentApp = 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 apps for initial display
this.loadApps();
// Auto-save enabled - no unsaved changes detection needed
},
// Auto-save enabled - unsaved changes handlers removed
// Cache DOM elements
cacheElements: function() {
this.elements = {
// Apps dropdown
appsOptions: document.querySelectorAll('#appsSection .log-option'),
currentAppsApp: document.getElementById('current-apps-app'),
appsDropdownBtn: document.querySelector('#appsSection .log-dropdown-btn'),
appsDropdownContent: document.querySelector('#appsSection .log-dropdown-content'),
// Apps panels
appAppsPanels: document.querySelectorAll('.app-apps-panel'),
// Controls - auto-save enabled, no save button needed
};
},
// Set up event listeners
setupEventListeners: function() {
// App selection via <select>
const appsAppSelect = document.getElementById('appsAppSelect');
if (appsAppSelect) {
appsAppSelect.addEventListener('change', (e) => {
const app = e.target.value;
this.handleAppsAppChange(app);
});
}
// Dropdown toggle
if (this.elements.appsDropdownBtn) {
this.elements.appsDropdownBtn.addEventListener('click', () => {
this.elements.appsDropdownContent.classList.toggle('show');
// Close all other dropdowns
document.querySelectorAll('.log-dropdown-content.show').forEach(dropdown => {
if (dropdown !== this.elements.appsDropdownContent) {
dropdown.classList.remove('show');
}
});
});
}
// Close dropdown when clicking outside
document.addEventListener('click', e => {
if (!e.target.matches('#appsSection .log-dropdown-btn') &&
!e.target.closest('#appsSection .log-dropdown-btn')) {
if (this.elements.appsDropdownContent && this.elements.appsDropdownContent.classList.contains('show')) {
this.elements.appsDropdownContent.classList.remove('show');
}
}
});
// Auto-save enabled - no save button needed
},
// Load apps for initial display
loadApps: function() {
// Set default app if none is selected
if (!this.currentApp) {
this.currentApp = 'sonarr'; // Default to Sonarr
// Update the dropdown text to show current app
if (this.elements.currentAppsApp) {
this.elements.currentAppsApp.textContent = 'Sonarr';
}
// Mark the sonarr option as active in the dropdown
if (this.elements.appsOptions) {
this.elements.appsOptions.forEach(option => {
option.classList.remove('active');
if (option.getAttribute('data-app') === 'sonarr') {
option.classList.add('active');
}
});
}
}
// Load the currently selected app
this.loadAppSettings(this.currentApp);
},
// Load app settings
loadAppSettings: function(app) {
console.log(`[Apps] Loading settings for ${app}`);
// Get the container to put the settings in
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') {
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
const instancesContainer = formElement.querySelector('.instances-container');
if (instancesContainer) {
const instanceCount = appSettings.instances ? appSettings.instances.length : 0;
console.log(`[Apps] Setting up connection status checking for ${app} with ${instanceCount} instances`);
SettingsForms.setupInstanceManagement(instancesContainer.parentElement, app, instanceCount);
} 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 {
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 {
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 {
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,
api_timeout: settings.api_timeout,
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 {
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);
}
}
};
// Initialize when document is ready
document.addEventListener('DOMContentLoaded', () => {
appsModule.init();
// Auto-save enabled - no save button needed
});