refactor: remove Edit Metadata right-click option
Removes the Edit Metadata feature from the anime context menu and
deletes all related backend/frontend code:
Backend:
- DELETE PUT /api/anime/{anime_key} endpoint
- DELETE AnimeMetadataUpdate model and KEY_PATTERN regex
- DELETE MAX_INPUT_LENGTH constant
Frontend:
- DELETE edit-modal.js component
- DELETE edit metadata modal HTML from index.html
- DELETE edit-modal.js script tag
- DELETE edit-only CSS (.edit-modal-content, .edit-section,
.field-error, .input-error, .key-warning, .info-grid,
.info-item, .input-with-action, .btn-fetch-tmdb, .tmdb-*)
- REMOVE 'Edit Metadata' item from context menu (NFO Diagnostics
preserved)
Tests:
- DELETE test_anime_edit_endpoints.py
- DELETE test_edit_modal.py
- DELETE test_anime_key_rename.py
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
* AniWorld - Context Menu Component
|
||||
*
|
||||
* Right-click context menu for anime series cards.
|
||||
* Provides quick access to edit metadata.
|
||||
* Provides quick access to NFO diagnostics.
|
||||
*
|
||||
* Dependencies: ui-utils.js, edit-modal.js
|
||||
* Dependencies: ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
@@ -67,10 +67,6 @@ AniWorld.ContextMenu = (function() {
|
||||
menuElement = document.createElement('div');
|
||||
menuElement.className = 'context-menu';
|
||||
menuElement.innerHTML = `
|
||||
<div class="context-menu-item" data-action="edit">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
<span>Edit Metadata</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="nfo-diagnostics">
|
||||
<i class="fa-solid fa-file-circle-check"></i>
|
||||
<span>NFO Diagnostics</span>
|
||||
@@ -100,13 +96,6 @@ AniWorld.ContextMenu = (function() {
|
||||
menuElement.style.top = posY + 'px';
|
||||
|
||||
// Attach action handlers
|
||||
menuElement.querySelector('[data-action="edit"]').addEventListener('click', function() {
|
||||
hide();
|
||||
if (AniWorld.EditModal) {
|
||||
AniWorld.EditModal.open(currentSeriesKey);
|
||||
}
|
||||
});
|
||||
|
||||
// NFO Diagnostics - opens the full NFO settings page
|
||||
menuElement.querySelector('[data-action="nfo-diagnostics"]').addEventListener('click', function() {
|
||||
hide();
|
||||
|
||||
@@ -1,630 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
const API = AniWorld.Constants ? AniWorld.Constants.API : {};
|
||||
|
||||
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 {
|
||||
// Always fetch fresh data from API for edit modal to ensure accuracy
|
||||
// This is more reliable than local cache which may be stale or missing
|
||||
let seriesData = await fetchSeriesDetails(seriesKey);
|
||||
|
||||
// Fallback: try local data if API fails
|
||||
if (!seriesData) {
|
||||
seriesData = findSeriesData(seriesKey);
|
||||
}
|
||||
|
||||
originalData = {
|
||||
key: seriesKey,
|
||||
tmdb_id: seriesData ? seriesData.tmdb_id : null,
|
||||
tvdb_id: seriesData ? seriesData.tvdb_id : null,
|
||||
name: seriesData ? seriesData.name : seriesKey,
|
||||
year: seriesData ? seriesData.year : null,
|
||||
status: seriesData ? seriesData.status : null,
|
||||
genres: seriesData ? (seriesData.genres || []) : [],
|
||||
studio: seriesData ? (seriesData.studio || []) : [],
|
||||
premiered: seriesData ? seriesData.premiered : null,
|
||||
rating: seriesData ? seriesData.rating : null,
|
||||
rating_votes: seriesData ? seriesData.rating_votes : null,
|
||||
};
|
||||
|
||||
// Populate form fields
|
||||
setFieldValue('edit-key', originalData.key);
|
||||
setFieldValue('edit-tmdb-id', originalData.tmdb_id || '');
|
||||
setFieldValue('edit-tvdb-id', originalData.tvdb_id || '');
|
||||
|
||||
// Populate display fields
|
||||
populateDisplayFields(originalData);
|
||||
|
||||
// Show/hide TMDB fetch button based on whether TMDB ID exists
|
||||
updateTmdbFetchButtonState();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch series details from the API.
|
||||
* @param {string} key - Series key
|
||||
* @returns {Promise<Object|null>} Series details or null on error
|
||||
*/
|
||||
async function fetchSeriesDetails(key) {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(
|
||||
'/api/anime/' + encodeURIComponent(key) + '/details'
|
||||
);
|
||||
if (response && response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch series details:', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate display-only fields in the modal.
|
||||
* @param {Object} data - Series data
|
||||
*/
|
||||
function populateDisplayFields(data) {
|
||||
const fields = [
|
||||
{ id: 'edit-name', value: data.name || '' },
|
||||
{ id: 'edit-year', value: data.year || '' },
|
||||
{ id: 'edit-status', value: data.status || '' },
|
||||
{ id: 'edit-genres', value: (data.genres || []).join(', ') },
|
||||
{ id: 'edit-studio', value: (data.studio || []).join(', ') },
|
||||
{ id: 'edit-premiered', value: data.premiered || '' },
|
||||
{ id: 'edit-rating', value: data.rating ? data.rating.toFixed(1) + '/10' : '' },
|
||||
];
|
||||
|
||||
fields.forEach(function(field) {
|
||||
const el = document.getElementById(field.id);
|
||||
if (el) el.textContent = field.value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the TMDB fetch button visibility.
|
||||
*/
|
||||
function updateTmdbFetchButtonState() {
|
||||
const btn = document.getElementById('btn-fetch-tmdb');
|
||||
const tmdbValue = getFieldValue('edit-tmdb-id').trim();
|
||||
|
||||
if (btn) {
|
||||
btn.style.display = tmdbValue ? 'none' : 'inline-flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch TMDB data for auto-fill.
|
||||
*/
|
||||
async function fetchTmdbData() {
|
||||
const btn = document.getElementById('btn-fetch-tmdb');
|
||||
const resultsContainer = document.getElementById('tmdb-search-results');
|
||||
if (!btn || !resultsContainer) return;
|
||||
|
||||
// Show loading state
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
|
||||
resultsContainer.innerHTML = '';
|
||||
resultsContainer.style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(
|
||||
'/api/anime/' + encodeURIComponent(currentKey) + '/tmdb-search'
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-error">Connection error</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 400) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-error">TMDB not configured</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-error">Search failed</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await response.json();
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-no-results">No matches found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Render results
|
||||
resultsContainer.innerHTML = results.slice(0, 5).map(function(r) {
|
||||
const year = r.year ? ' (' + r.year + ')' : '';
|
||||
const rating = r.vote_average ? ' ★' + r.vote_average.toFixed(1) : '';
|
||||
return '<div class="tmdb-result-item" data-tmdb-id="' + r.tmdb_id + '">' +
|
||||
'<span class="tmdb-result-title">' + escapeHtml(r.title) + year + rating + '</span>' +
|
||||
'<span class="tmdb-result-overview">' + escapeHtml(r.overview || '') + '</span>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// Attach click handlers to results
|
||||
resultsContainer.querySelectorAll('.tmdb-result-item').forEach(function(item) {
|
||||
item.addEventListener('click', function() {
|
||||
const tmdbId = this.getAttribute('data-tmdb-id');
|
||||
setFieldValue('edit-tmdb-id', tmdbId);
|
||||
resultsContainer.innerHTML = '<p class="tmdb-selected">TMDB ID ' + tmdbId + ' selected</p>';
|
||||
updateTmdbFetchButtonState();
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
resultsContainer.innerHTML = '<p class="tmdb-error">Search failed</p>';
|
||||
console.error('TMDB search error:', err);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-search"></i> Fetch from TMDB';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '<p class="nfo-error">Failed to load NFO diagnostics</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
renderDiagnostics(data);
|
||||
updateRepairButtonState();
|
||||
|
||||
} catch (err) {
|
||||
container.innerHTML = '<p class="nfo-error">Error loading diagnostics</p>';
|
||||
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');
|
||||
const pathDisplay = document.getElementById('nfo-path-display');
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// Show NFO path if available
|
||||
if (pathDisplay) {
|
||||
if (data.nfo_path) {
|
||||
// Extract just the relative path from the full path
|
||||
const parts = data.nfo_path.split('/');
|
||||
const relativePath = parts.slice(-3).join('/'); // folder/tvshow.nfo
|
||||
pathDisplay.textContent = relativePath;
|
||||
pathDisplay.title = data.nfo_path;
|
||||
} else {
|
||||
pathDisplay.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (tagsList) {
|
||||
if (data.missing_tags.length === 0) {
|
||||
tagsList.innerHTML = '<p class="nfo-all-good">All required tags present</p>';
|
||||
} else {
|
||||
tagsList.innerHTML = data.missing_tags.map(function(tag) {
|
||||
return '<span class="missing-tag-chip">' + escapeHtml(tag) + '</span>';
|
||||
}).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 diagnosticsLink = document.getElementById('btn-open-nfo-diagnostics');
|
||||
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';
|
||||
}
|
||||
// Show link to full diagnostics page
|
||||
if (diagnosticsLink && currentKey) {
|
||||
diagnosticsLink.href = '/settings/nfo?key=' + encodeURIComponent(currentKey);
|
||||
diagnosticsLink.style.display = 'inline-flex';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
? '<i class="fa-solid fa-spinner fa-spin"></i> Saving...'
|
||||
: '<i class="fa-solid fa-floppy-disk"></i> Save';
|
||||
}
|
||||
}
|
||||
|
||||
function setRepairLoading(loading) {
|
||||
const btn = document.getElementById('btn-repair-nfo');
|
||||
if (btn) {
|
||||
btn.disabled = loading;
|
||||
btn.innerHTML = loading
|
||||
? '<i class="fa-solid fa-spinner fa-spin"></i> Repairing...'
|
||||
: '<i class="fa-solid fa-wrench"></i> 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 fetchTmdbBtn = document.getElementById('btn-fetch-tmdb');
|
||||
const overlay = modalElement ? modalElement.querySelector('.modal-overlay') : null;
|
||||
const keyInput = document.getElementById('edit-key');
|
||||
const tmdbInput = document.getElementById('edit-tmdb-id');
|
||||
|
||||
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 (fetchTmdbBtn) {
|
||||
var fetchTmdbFn = function() { fetchTmdbData(); };
|
||||
fetchTmdbBtn.addEventListener('click', fetchTmdbFn);
|
||||
listeners.push({ el: fetchTmdbBtn, event: 'click', fn: fetchTmdbFn });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
if (tmdbInput) {
|
||||
var tmdbFn = function() {
|
||||
updateTmdbFetchButtonState();
|
||||
};
|
||||
tmdbInput.addEventListener('input', tmdbFn);
|
||||
listeners.push({ el: tmdbInput, event: 'input', fn: tmdbFn });
|
||||
}
|
||||
}
|
||||
|
||||
function detachListeners() {
|
||||
listeners.forEach(function(l) {
|
||||
l.el.removeEventListener(l.event, l.fn);
|
||||
});
|
||||
listeners = [];
|
||||
}
|
||||
|
||||
return {
|
||||
open: open,
|
||||
close: close,
|
||||
save: save,
|
||||
repairNfo: repairNfo
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user