/** * AniWorld - Edit Modal Component * * Modal dialog for viewing/editing anime metadata (key, tmdb_id, tvdb_id) * and NFO diagnostics with repair functionality. * * Dependencies: api-client.js, ui-utils.js */ var AniWorld = window.AniWorld || {}; AniWorld.EditModal = (function() { 'use strict'; let modalElement = null; let originalData = null; let currentKey = null; /** * Open the edit modal for a specific anime series. * @param {string} seriesKey - The series key to edit */ async function open(seriesKey) { currentKey = seriesKey; modalElement = document.getElementById('edit-metadata-modal'); if (!modalElement) return; // Show modal modalElement.classList.remove('hidden'); // Reset form state setLoading(true); clearErrors(); hideKeyWarning(); try { // Find series data from the local series list const seriesData = findSeriesData(seriesKey); originalData = { key: seriesKey, tmdb_id: seriesData ? seriesData.tmdb_id : null, tvdb_id: seriesData ? seriesData.tvdb_id : null, }; // Populate form fields setFieldValue('edit-key', originalData.key); setFieldValue('edit-tmdb-id', originalData.tmdb_id || ''); setFieldValue('edit-tvdb-id', originalData.tvdb_id || ''); // Load NFO diagnostics await loadDiagnostics(seriesKey); } catch (err) { AniWorld.UI.showToast('Failed to load series data', 'error'); console.error('Edit modal load error:', err); } finally { setLoading(false); } // Attach event listeners attachListeners(); } /** * Close the edit modal and reset state. */ function close() { if (modalElement) { modalElement.classList.add('hidden'); } originalData = null; currentKey = null; detachListeners(); } /** * Save changed metadata to the backend. */ async function save() { clearErrors(); const newKey = getFieldValue('edit-key').trim().toLowerCase(); const tmdbIdStr = getFieldValue('edit-tmdb-id').trim(); const tvdbIdStr = getFieldValue('edit-tvdb-id').trim(); // Validate key if (!newKey) { showFieldError('edit-key', 'Key cannot be empty'); return; } if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(newKey)) { showFieldError('edit-key', 'Key must contain only lowercase letters, numbers, and hyphens'); return; } // Validate IDs const tmdbId = tmdbIdStr ? parseInt(tmdbIdStr, 10) : null; const tvdbId = tvdbIdStr ? parseInt(tvdbIdStr, 10) : null; if (tmdbIdStr && (isNaN(tmdbId) || tmdbId < 1)) { showFieldError('edit-tmdb-id', 'TMDB ID must be a positive number'); return; } if (tvdbIdStr && (isNaN(tvdbId) || tvdbId < 1)) { showFieldError('edit-tvdb-id', 'TVDB ID must be a positive number'); return; } // Check if key changed — show confirmation if (newKey !== originalData.key) { const confirmed = await AniWorld.UI.showConfirmModal( 'Rename Series Key', `Changing the key from "${originalData.key}" to "${newKey}" will update the primary identifier. ` + 'This may affect provider linkage. Are you sure?' ); if (!confirmed) return; } // Build update payload (only changed fields) const payload = {}; if (newKey !== originalData.key) payload.key = newKey; if (tmdbId !== originalData.tmdb_id) payload.tmdb_id = tmdbId; if (tvdbId !== originalData.tvdb_id) payload.tvdb_id = tvdbId; if (Object.keys(payload).length === 0) { AniWorld.UI.showToast('No changes to save', 'info'); return; } // Send update setSaveLoading(true); try { const response = await AniWorld.ApiClient.put( '/api/anime/' + encodeURIComponent(currentKey), payload ); if (!response) return; if (response.ok) { const result = await response.json(); AniWorld.UI.showToast('Metadata updated successfully', 'success'); // Update local state const oldKey = currentKey; currentKey = result.key; originalData = { key: result.key, tmdb_id: result.tmdb_id, tvdb_id: result.tvdb_id, }; // Update the card in the DOM updateCardAfterSave(oldKey, result); // Update repair button state updateRepairButtonState(); } else if (response.status === 409) { showFieldError('edit-key', 'A series with this key already exists'); } else if (response.status === 422) { const err = await response.json(); AniWorld.UI.showToast('Validation error: ' + (err.detail || 'Invalid input'), 'error'); } else { AniWorld.UI.showToast('Failed to update metadata', 'error'); } } catch (err) { AniWorld.UI.showToast('Connection error. Check your network.', 'error'); console.error('Save error:', err); } finally { setSaveLoading(false); } } /** * Trigger NFO repair for the current series. */ async function repairNfo() { setRepairLoading(true); try { const response = await AniWorld.ApiClient.post( '/api/nfo/' + encodeURIComponent(currentKey) + '/repair', {} ); if (!response) return; if (response.ok) { const result = await response.json(); AniWorld.UI.showToast(result.message, 'success'); // Refresh diagnostics await loadDiagnostics(currentKey); } else if (response.status === 400) { const err = await response.json(); AniWorld.UI.showToast(err.detail || 'Cannot repair NFO', 'error'); } else { AniWorld.UI.showToast('Failed to repair NFO', 'error'); } } catch (err) { AniWorld.UI.showToast('Connection error during repair', 'error'); console.error('Repair error:', err); } finally { setRepairLoading(false); } } /** * Load NFO diagnostics for the current series. * @param {string} key - Series key */ async function loadDiagnostics(key) { const container = document.getElementById('nfo-diagnostics-container'); if (!container) return; try { const response = await AniWorld.ApiClient.get( '/api/nfo/' + encodeURIComponent(key) + '/diagnostics' ); if (!response || !response.ok) { container.innerHTML = '

Failed to load NFO diagnostics

'; return; } const data = await response.json(); renderDiagnostics(data); updateRepairButtonState(); } catch (err) { container.innerHTML = '

Error loading diagnostics

'; console.error('Diagnostics error:', err); } } /** * Render NFO diagnostics data into the modal. * @param {Object} data - NfoDiagnosticsResponse */ function renderDiagnostics(data) { const badge = document.getElementById('nfo-status-badge'); const tagsList = document.getElementById('nfo-missing-tags'); if (badge) { if (!data.has_nfo) { badge.className = 'nfo-status-badge nfo-missing'; badge.textContent = 'No NFO File'; } else if (data.missing_tags.length === 0) { badge.className = 'nfo-status-badge nfo-complete'; badge.textContent = 'Complete'; } else { badge.className = 'nfo-status-badge nfo-incomplete'; badge.textContent = data.missing_tags.length + ' Missing'; } } if (tagsList) { if (data.missing_tags.length === 0) { tagsList.innerHTML = '

All required tags present

'; } else { tagsList.innerHTML = data.missing_tags.map(function(tag) { return '' + escapeHtml(tag) + ''; }).join(''); } } } /** * Update repair button disabled state based on tmdb_id field. */ function updateRepairButtonState() { const btn = document.getElementById('btn-repair-nfo'); const hint = document.getElementById('repair-hint'); const tmdbValue = getFieldValue('edit-tmdb-id').trim(); if (btn) { // Enable repair even without tmdb_id — the service can search by name btn.disabled = false; } if (hint) { hint.style.display = tmdbValue ? 'none' : 'block'; } } // ---- Helpers ---- function findSeriesData(key) { // Access the series data from the series manager if available if (AniWorld.SeriesManager && AniWorld.SeriesManager.getSeriesData) { const allSeries = AniWorld.SeriesManager.getSeriesData(); if (allSeries) { return allSeries.find(function(s) { return s.key === key; }); } } return null; } function updateCardAfterSave(oldKey, result) { const card = document.querySelector('[data-series-id="' + oldKey + '"]'); if (card) { card.setAttribute('data-key', result.key); card.setAttribute('data-series-id', result.key); // Update checkbox data-key const checkbox = card.querySelector('.series-checkbox'); if (checkbox) { checkbox.setAttribute('data-key', result.key); } } // Update local series data array if (AniWorld.SeriesManager && AniWorld.SeriesManager.updateSeriesKey) { AniWorld.SeriesManager.updateSeriesKey(oldKey, result.key); } } function setFieldValue(id, value) { const el = document.getElementById(id); if (el) el.value = value !== null && value !== undefined ? value : ''; } function getFieldValue(id) { const el = document.getElementById(id); return el ? el.value : ''; } function showFieldError(fieldId, message) { const el = document.getElementById(fieldId); if (el) { const errorEl = el.parentElement.querySelector('.field-error'); if (errorEl) { errorEl.textContent = message; errorEl.style.display = 'block'; } el.classList.add('input-error'); } } function clearErrors() { if (!modalElement) return; modalElement.querySelectorAll('.field-error').forEach(function(el) { el.style.display = 'none'; el.textContent = ''; }); modalElement.querySelectorAll('.input-error').forEach(function(el) { el.classList.remove('input-error'); }); } function hideKeyWarning() { const warning = document.getElementById('key-change-warning'); if (warning) warning.style.display = 'none'; } function setLoading(loading) { const form = document.getElementById('edit-metadata-form'); if (form) { form.style.opacity = loading ? '0.5' : '1'; form.style.pointerEvents = loading ? 'none' : 'auto'; } } function setSaveLoading(loading) { const btn = document.getElementById('btn-save-metadata'); if (btn) { btn.disabled = loading; btn.innerHTML = loading ? ' Saving...' : ' Save'; } } function setRepairLoading(loading) { const btn = document.getElementById('btn-repair-nfo'); if (btn) { btn.disabled = loading; btn.innerHTML = loading ? ' Repairing...' : ' Repair NFO'; } } function escapeHtml(str) { var div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // Event listener management let listeners = []; function attachListeners() { detachListeners(); const saveBtn = document.getElementById('btn-save-metadata'); const cancelBtn = document.getElementById('btn-cancel-metadata'); const repairBtn = document.getElementById('btn-repair-nfo'); const overlay = modalElement ? modalElement.querySelector('.modal-overlay') : null; const keyInput = document.getElementById('edit-key'); if (saveBtn) { var saveFn = function() { save(); }; saveBtn.addEventListener('click', saveFn); listeners.push({ el: saveBtn, event: 'click', fn: saveFn }); } if (cancelBtn) { var cancelFn = function() { close(); }; cancelBtn.addEventListener('click', cancelFn); listeners.push({ el: cancelBtn, event: 'click', fn: cancelFn }); } if (repairBtn) { var repairFn = function() { repairNfo(); }; repairBtn.addEventListener('click', repairFn); listeners.push({ el: repairBtn, event: 'click', fn: repairFn }); } if (overlay) { var overlayFn = function() { close(); }; overlay.addEventListener('click', overlayFn); listeners.push({ el: overlay, event: 'click', fn: overlayFn }); } if (keyInput) { var keyFn = function() { var warning = document.getElementById('key-change-warning'); if (warning) { warning.style.display = keyInput.value !== originalData.key ? 'block' : 'none'; } }; keyInput.addEventListener('input', keyFn); listeners.push({ el: keyInput, event: 'input', fn: keyFn }); } } function detachListeners() { listeners.forEach(function(l) { l.el.removeEventListener(l.event, l.fn); }); listeners = []; } return { open: open, close: close, save: save, repairNfo: repairNfo }; })();