/** * AniWorld - Delete Anime Modal Module * * Handles the delete confirmation modal for anime series. * Users must type "delete" to enable the confirm button. * * Dependencies: constants.js, api-client.js, ui-utils.js */ var AniWorld = window.AniWorld || {}; AniWorld.DeleteModal = (function() { 'use strict'; const API = '/api/anime'; let currentKey = null; let currentSeriesName = null; let modalElement = null; let confirmBtn = null; let confirmInput = null; let deleteDbCheckbox = null; let deleteFolderCheckbox = null; let errorElement = null; let isSubmitting = false; /** * Initialize the modal — inject HTML into body if not already present. */ function init() { injectModalHTML(); cacheElements(); bindEvents(); console.info('[DeleteModal] initialized'); } /** * Build the modal HTML once and append to document.body. */ function injectModalHTML() { if (document.getElementById('delete-modal')) return; var div = document.createElement('div'); div.id = 'delete-modal'; div.className = 'modal hidden'; div.innerHTML = '
' + ''; document.body.appendChild(div); } /** * Cache DOM element references. */ function cacheElements() { modalElement = document.getElementById('delete-modal'); confirmBtn = document.getElementById('delete-confirm-btn'); confirmInput = document.getElementById('delete-confirm-input'); deleteDbCheckbox = document.getElementById('delete-db-checkbox'); deleteFolderCheckbox = document.getElementById('delete-folder-checkbox'); errorElement = document.getElementById('delete-error'); } /** * Bind event listeners on the modal. */ function bindEvents() { // Guard against missing modal if (!modalElement) return; // Cancel button var cancelBtn = document.getElementById('delete-cancel-btn'); if (cancelBtn) cancelBtn.addEventListener('click', hide); // Close on backdrop click var overlay = modalElement.querySelector('.modal-overlay'); if (overlay) overlay.addEventListener('click', hide); // Escape key to close document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && !isSubmitting && modalElement && !modalElement.classList.contains('hidden')) { hide(); } }); // Folder checkbox toggle — show/hide warning if (deleteFolderCheckbox) { deleteFolderCheckbox.addEventListener('change', function() { var warning = document.getElementById('delete-folder-warning'); if (warning) { warning.style.display = deleteFolderCheckbox.checked ? 'flex' : 'none'; } }); } // Confirm input — validate and update button state if (confirmInput) { confirmInput.addEventListener('input', function() { var value = confirmInput.value; var isMatch = value === 'delete'; if (confirmBtn) confirmBtn.disabled = !isMatch || isSubmitting; confirmInput.classList.toggle('matched', isMatch); }); } // Confirm button if (confirmBtn) confirmBtn.addEventListener('click', handleConfirm); // Click outside modal content to close modalElement.addEventListener('click', function(e) { if (e.target === modalElement) { hide(); } }); } /** * Show the delete modal for a given series. * @param {string} key - Series key */ function show(key) { console.info('[DeleteModal] Opening for key:', key); // Ensure elements are cached (in case DOM was replaced) cacheElements(); // Guard against missing elements if (!modalElement || !confirmInput || !confirmBtn) { console.error('[DeleteModal] Modal elements not found in DOM. Re-injecting.'); injectModalHTML(); cacheElements(); if (!modalElement) { console.error('[DeleteModal] Failed to create modal element.'); return; } } // Get series info from SeriesManager if available var seriesData = null; if (AniWorld.SeriesManager && AniWorld.SeriesManager.findByKey) { seriesData = AniWorld.SeriesManager.findByKey(key); } currentKey = key; currentSeriesName = seriesData ? (seriesData.name || key) : key; // Populate modal — guard against missing elements var seriesNameEl = document.getElementById('delete-modal-series-name'); var seriesKeyEl = document.getElementById('delete-modal-series-key'); var folderWarningEl = document.getElementById('delete-folder-warning'); if (seriesNameEl) seriesNameEl.textContent = currentSeriesName; if (seriesKeyEl) seriesKeyEl.textContent = 'Key: ' + key; // Reset state confirmInput.value = ''; confirmInput.classList.remove('matched'); confirmBtn.disabled = true; isSubmitting = false; if (errorElement) { errorElement.classList.add('hidden'); errorElement.textContent = ''; } if (deleteDbCheckbox) deleteDbCheckbox.checked = true; if (deleteFolderCheckbox) deleteFolderCheckbox.checked = false; if (folderWarningEl) folderWarningEl.style.display = 'none'; // Show modal modalElement.classList.remove('hidden'); confirmInput.focus(); } /** * Hide the modal and reset state. */ function hide() { if (isSubmitting) return; // Don't close while submitting modalElement.classList.add('hidden'); currentKey = null; currentSeriesName = null; } /** * Handle confirm button click. */ async function handleConfirm() { if (confirmBtn.disabled || isSubmitting) return; var deleteDb = deleteDbCheckbox.checked; var deleteFolder = deleteFolderCheckbox.checked; var confirmText = confirmInput.value.trim(); // Validate confirm text if (confirmText !== 'delete') { showError('You must type exactly "delete" to confirm.'); return; } // Validate at least one option selected if (!deleteDb && !deleteFolder) { showError('Please select at least one delete option.'); return; } isSubmitting = true; confirmBtn.disabled = true; confirmBtn.textContent = 'Deleting...'; errorElement.classList.add('hidden'); console.info('[DeleteModal] Initiating delete:', { key: currentKey, delete_database: deleteDb, delete_folder: deleteFolder }); try { var response = await AniWorld.ApiClient.request( API + '/' + encodeURIComponent(currentKey), { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ delete_database: deleteDb, delete_folder: deleteFolder, confirm_text: confirmText }) } ); if (!response) { // Auth failure already redirected hide(); return; } if (response.status === 401) { AniWorld.Auth.removeToken(); window.location.href = '/login'; hide(); return; } if (response.status === 400) { var body = await response.json().catch(function() { return {}; }); showError(body.detail || 'Invalid request: ' + response.status); isSubmitting = false; confirmBtn.textContent = 'Delete'; confirmBtn.disabled = false; return; } if (response.status === 404) { showError('Series not found: ' + currentKey); isSubmitting = false; confirmBtn.textContent = 'Delete'; return; } if (!response.ok) { var text = await response.text(); showError('Delete failed: HTTP ' + response.status + ' ' + text); isSubmitting = false; confirmBtn.textContent = 'Delete'; confirmBtn.disabled = false; return; } var result = await response.json(); console.info('[DeleteModal] Delete succeeded:', result); // Capture key before hide() nulls currentKey var deletedKey = currentKey; // Show success message based on what was deleted var msgParts = []; if (result.deleted_from_database) msgParts.push('removed from database'); if (result.deleted_folder) msgParts.push('folder deleted from filesystem'); if (result.database_error) msgParts.push('DB error: ' + result.database_error); if (result.folder_error) msgParts.push('Folder error: ' + result.folder_error); var successMsg = msgParts.length > 0 ? msgParts.join('; ') : 'Delete completed.'; AniWorld.UI.showToast(successMsg, result.success ? 'success' : 'warning'); // Close modal and reset submission state together — isSubmitting must // be cleared before hide(), otherwise hide() bails out (early return // on the !isSubmitting guard) and the modal stays visible. isSubmitting = false; if (confirmBtn) confirmBtn.textContent = 'Delete'; hide(); // Remove the card from the grid directly if (AniWorld.SeriesManager && AniWorld.SeriesManager.removeSeries) { AniWorld.SeriesManager.removeSeries(deletedKey); } } catch (err) { console.error('[DeleteModal] Delete request failed:', err); showError('Network error: ' + (err && err.message ? err.message : String(err))); isSubmitting = false; confirmBtn.textContent = 'Delete'; confirmBtn.disabled = false; } } /** * Show an error message in the modal. * @param {string} msg */ function showError(msg) { errorElement.textContent = msg; errorElement.classList.remove('hidden'); } // Public API return { init: init, show: show, hide: hide }; })();