const editorHandler = (() => { let editorChannels = []; let selectedChannelId = null; let selectedRowIds = new Set(); let currentGroupFilter = ''; let currentSort = { column: null, direction: 'asc' }; let sortableInstance = null; let groupOrder = []; const dom = {}; function cacheDom() { const modal = document.getElementById('editorModal'); if (!modal) return false; dom.modal = modal; dom.tableContainer = modal.querySelector('#editor-table-container'); dom.tableBody = modal.querySelector('#editor-table-body'); dom.selectAllCheckbox = modal.querySelector('#editor-select-all'); dom.searchInput = modal.querySelector('#editor-search-input'); dom.groupFilterSelect = modal.querySelector('#editor-group-filter'); dom.fileNameDisplay = modal.querySelector('#file-name-display'); dom.editorPanel = modal.querySelector('#editor-panel'); dom.editorPlaceholder = modal.querySelector('#editor-placeholder'); dom.editorFormContent = modal.querySelector('#editor-form-content'); dom.editorChannelNameInput = modal.querySelector('#editor-channel-name'); dom.editorChannelTvgIdInput = modal.querySelector('#editor-channel-tvg-id'); dom.editorChannelChNumInput = modal.querySelector('#editor-channel-ch-num'); dom.editorChannelLogoInput = modal.querySelector('#editor-channel-logo'); dom.editorLogoPreview = modal.querySelector('#editor-logo-preview'); dom.editorChannelUrlInput = modal.querySelector('#editor-channel-url'); dom.editorChannelGroupInput = modal.querySelector('#editor-channel-group'); dom.groupSuggestionsDatalist = modal.querySelector('#group-suggestions'); dom.editorFavCheckbox = modal.querySelector('#editor-fav-channel'); dom.editorHideChannelCheckbox = modal.querySelector('#editor-hide-channel'); dom.editorKodiLicenseTypeInput = modal.querySelector('#editor-kodi-license-type'); dom.editorKodiLicenseKeyInput = modal.querySelector('#editor-kodi-license-key'); dom.editorKodiStreamHeadersInput = modal.querySelector('#editor-kodi-stream-headers'); dom.editorVlcUserAgentInput = modal.querySelector('#editor-vlc-user-agent'); dom.editorSaveBtn = modal.querySelector('#editor-save-btn'); dom.editorPlayBtn = modal.querySelector('#editor-play-btn'); dom.editorDeleteBtn = modal.querySelector('#editor-delete-btn'); dom.closeEditorBtn = modal.querySelector('#close-editor-btn'); dom.multiEditBtn = modal.querySelector('#multi-edit-btn'); dom.deleteSelectedBtn = modal.querySelector('#delete-selected-btn'); dom.clearSelectionBtn = modal.querySelector('#clear-selection-btn'); const multiEditModal = document.getElementById('multiEditModal'); dom.multiEditModal = multiEditModal; dom.multiEditChannelCount = multiEditModal.querySelector('#multiEditChannelCount'); dom.multiEditEnableGroup = multiEditModal.querySelector('#multiEditEnableGroup'); dom.multiEditGroupInput = multiEditModal.querySelector('#multiEditGroupInput'); dom.multiEditEnableFavorite = multiEditModal.querySelector('#multiEditEnableFavorite'); dom.multiEditFavoriteSelect = multiEditModal.querySelector('#multiEditFavoriteSelect'); dom.multiEditEnableHidden = multiEditModal.querySelector('#multiEditEnableHidden'); dom.multiEditHiddenSelect = multiEditModal.querySelector('#multiEditHiddenSelect'); dom.multiEditEnableUserAgent = multiEditModal.querySelector('#multiEditEnableUserAgent'); dom.multiEditUserAgentInput = multiEditModal.querySelector('#multiEditUserAgentInput'); dom.multiEditEnableStreamHeaders = multiEditModal.querySelector('#multiEditEnableStreamHeaders'); dom.multiEditStreamHeadersInput = multiEditModal.querySelector('#multiEditStreamHeadersInput'); dom.multiEditStreamHeadersMode = multiEditModal.querySelector('#multiEditStreamHeadersMode'); dom.applyMultiEditBtn = multiEditModal.querySelector('#applyMultiEditBtn'); return true; } function init(channelsData, fileName) { if (!cacheDom()) { console.error("Editor DOM not found. Cannot initialize."); return; } editorChannels = JSON.parse(JSON.stringify(channelsData)); editorChannels.forEach((ch, idx) => { if (ch) { ch.editorId = `editor-ch-${idx}-${Date.now()}`; } }); dom.fileNameDisplay.textContent = fileName || "Lista Actual"; dom.fileNameDisplay.classList.add('loaded'); updateGroupOrder(); renderTable(); bindEvents(); showEditorPlaceholder(); clearMultiSelection(); } function bindEvents() { if (dom.editorSaveBtn.dataset.initialized) return; dom.editorSaveBtn.addEventListener('click', handleEditorSave); dom.editorPlayBtn.addEventListener('click', handleEditorPlay); dom.editorDeleteBtn.addEventListener('click', handleEditorDelete); dom.closeEditorBtn.addEventListener('click', showEditorPlaceholder); dom.tableBody.addEventListener('click', handleTableBodyClick); dom.selectAllCheckbox.addEventListener('change', handleSelectAllVisible); dom.tableBody.addEventListener('change', handleRowCheckboxChange); dom.searchInput.addEventListener('input', debounce(() => { currentSort.column = null; renderTable(); }, 300)); dom.groupFilterSelect.addEventListener('change', (e) => { currentGroupFilter = e.target.value; renderTable(); }); dom.deleteSelectedBtn.addEventListener('click', deleteSelectedChannels); dom.clearSelectionBtn.addEventListener('click', clearMultiSelection); dom.multiEditBtn.addEventListener('click', openMultiEditModal); dom.modal.querySelectorAll('th.sortable').forEach(th => { th.addEventListener('click', () => handleSort(th.dataset.sort)); }); bindMultiEditEvents(); dom.editorSaveBtn.dataset.initialized = 'true'; } function bindMultiEditEvents() { dom.applyMultiEditBtn.addEventListener('click', handleApplyMultiEdit); const multiEditToggles = [ { check: dom.multiEditEnableGroup, input: dom.multiEditGroupInput }, { check: dom.multiEditEnableFavorite, input: dom.multiEditFavoriteSelect }, { check: dom.multiEditEnableHidden, input: dom.multiEditHiddenSelect }, { check: dom.multiEditEnableUserAgent, input: dom.multiEditUserAgentInput }, { check: dom.multiEditEnableStreamHeaders, input: dom.multiEditStreamHeadersInput, extra: dom.multiEditStreamHeadersMode }, ]; multiEditToggles.forEach(({check, input, extra}) => { check.addEventListener('change', (e) => { input.disabled = !e.target.checked; if (extra) extra.disabled = !e.target.checked; }); }); } function updateGroupOrder() { const currentGroups = [...new Set(editorChannels.filter(ch => ch).map(ch => ch['group-title'] || ''))]; const newGroupOrder = (groupOrder.length > 0 ? groupOrder : []).filter(group => currentGroups.includes(group)); currentGroups.forEach(group => { if (!newGroupOrder.includes(group)) newGroupOrder.push(group); }); groupOrder = newGroupOrder; updateGroupFilter(); updateGroupSuggestions(); } function renderTable() { const fragment = document.createDocumentFragment(); dom.tableBody.innerHTML = ''; const searchTerm = dom.searchInput.value.toLowerCase().trim(); if (currentGroupFilter === '' && !searchTerm && !currentSort.column) { const groupCounts = editorChannels.reduce((acc, channel) => { if (!channel || channel.attributes?.hidden === 'true') return acc; const groupKey = channel['group-title'] || ''; acc[groupKey] = (acc[groupKey] || 0) + 1; return acc; }, {}); groupOrder.forEach(groupKey => { const count = groupCounts[groupKey] || 0; if (count > 0) { fragment.appendChild(createGroupHeaderRow(groupKey, count)); } }); } else { const filteredChannels = getFilteredAndSortedChannels(); if (currentGroupFilter) { if (filteredChannels.length > 0) { const groupKey = filteredChannels[0]['group-title'] || ''; fragment.appendChild(createGroupHeaderRow(groupKey, filteredChannels.length)); filteredChannels.forEach(channel => fragment.appendChild(createRow(channel))); } } else { filteredChannels.forEach(channel => fragment.appendChild(createRow(channel))); } } dom.tableBody.appendChild(fragment); updateSortableForCurrentView(); updateSelectAllCheckboxState(); updateSortIcons(); } function getFilteredAndSortedChannels() { const searchTerm = dom.searchInput.value.toLowerCase().trim(); let channelsToProcess = editorChannels.filter(ch => { if (!ch || ch.attributes?.hidden === 'true') { return false; } if (currentGroupFilter && (ch['group-title'] || '') !== currentGroupFilter) { return false; } if (searchTerm) { if (!( ch.name?.toLowerCase().includes(searchTerm) || ch.url?.toLowerCase().includes(searchTerm) || ch['group-title']?.toLowerCase().includes(searchTerm) || ch['tvg-id']?.toLowerCase().includes(searchTerm) )) { return false; } } return true; }); if (currentSort.column) { channelsToProcess.sort((a, b) => { let valA, valB; switch (currentSort.column) { case 'ch-number': valA = parseInt(a.attributes?.['ch-number'], 10) || Infinity; valB = parseInt(b.attributes?.['ch-number'], 10) || Infinity; break; default: valA = (a[currentSort.column] || '').toLowerCase(); valB = (b[currentSort.column] || '').toLowerCase(); break; } let comparison = valA < valB ? -1 : (valA > valB ? 1 : 0); return comparison * (currentSort.direction === 'asc' ? 1 : -1); }); } return channelsToProcess; } function createRow(channel) { const row = document.createElement('tr'); row.dataset.editorId = channel.editorId; row.dataset.groupParent = channel['group-title'] || ''; row.className = `channel-row ${channel.editorId === selectedChannelId ? 'selected-row' : ''}`; const logoHtml = channel['tvg-logo'] ? `` : `-`; const nameHtml = `${channel.favorite ? '' : ''}${escapeHtml(channel.name || '')}`; row.innerHTML = ` ${logoHtml} ${nameHtml} ${escapeHtml(channel.url || '')} ${escapeHtml(channel['tvg-id'] || '-')} ${escapeHtml(channel.attributes?.['ch-number'] || '-')} `; return row; } function createGroupHeaderRow(group, count) { const headerRow = document.createElement('tr'); headerRow.className = `group-header-row`; headerRow.dataset.group = group; const displayName = group === '' ? '(Sin Grupo)' : group; headerRow.innerHTML = ` ${escapeHtml(displayName)} (${count}) `; return headerRow; } function handleTableBodyClick(e) { const btn = e.target.closest('.btn-action'); if (btn) { const row = e.target.closest('tr'); if (btn.classList.contains('play')) { handleEditorPlay(row.dataset.editorId); } else if (btn.classList.contains('rename')) { handleRenameGroup(row.dataset.group); } return; } if (e.target.closest('.row-checkbox, .drag-handle, .group-header-row')) return; const row = e.target.closest('tr.channel-row'); if (row && row.dataset.editorId) { displayChannelInEditor(row.dataset.editorId); } } function displayChannelInEditor(editorId) { const channel = editorChannels.find(ch => ch && ch.editorId === editorId); if (!channel) { showEditorPlaceholder(); return; } selectedChannelId = editorId; dom.editorFormContent.classList.remove('hidden'); dom.editorPlaceholder.classList.add('hidden'); dom.modal.classList.add('editor-visible'); if (!dom.editorChannelIdInput) { dom.editorChannelIdInput = document.createElement('input'); dom.editorChannelIdInput.type = 'hidden'; dom.editorChannelIdInput.id = 'editor-channel-id'; dom.editorFormContent.insertBefore(dom.editorChannelIdInput, dom.editorFormContent.firstChild); } dom.editorChannelIdInput.value = editorId; dom.editorChannelNameInput.value = channel.name || ''; dom.editorChannelTvgIdInput.value = channel['tvg-id'] || ''; dom.editorChannelChNumInput.value = channel.attributes?.['ch-number'] || ''; dom.editorChannelLogoInput.value = channel['tvg-logo'] || ''; dom.editorLogoPreview.src = channel['tvg-logo'] || ''; dom.editorLogoPreview.style.display = channel['tvg-logo'] ? 'block' : 'none'; dom.editorChannelUrlInput.value = channel.url || ''; dom.editorChannelGroupInput.value = channel['group-title'] || ''; dom.editorFavCheckbox.checked = channel.favorite || false; dom.editorHideChannelCheckbox.checked = channel.attributes?.hidden === 'true'; const kodiProps = channel.kodiProps || {}; dom.editorKodiLicenseTypeInput.value = kodiProps['inputstream.adaptive.license_type'] || ''; dom.editorKodiLicenseKeyInput.value = kodiProps['inputstream.adaptive.license_key'] || ''; dom.editorKodiStreamHeadersInput.value = kodiProps['inputstream.adaptive.stream_headers'] || ''; dom.editorVlcUserAgentInput.value = (channel.vlcOptions || {})['http-user-agent'] || ''; renderTable(); } function showEditorPlaceholder() { selectedChannelId = null; dom.editorFormContent.classList.add('hidden'); dom.editorPlaceholder.classList.remove('hidden'); dom.modal.classList.remove('editor-visible'); if (dom.tableBody) dom.tableBody.querySelectorAll('.selected-row').forEach(r => r.classList.remove('selected-row')); } function handleEditorSave() { if (!selectedChannelId) return; const index = editorChannels.findIndex(ch => ch && ch.editorId === selectedChannelId); if (index === -1) return; const channelData = editorChannels[index]; const oldGroup = channelData['group-title']; channelData.name = dom.editorChannelNameInput.value.trim(); channelData.url = dom.editorChannelUrlInput.value.trim(); channelData['group-title'] = dom.editorChannelGroupInput.value.trim(); channelData['tvg-logo'] = dom.editorChannelLogoInput.value.trim(); channelData['tvg-id'] = dom.editorChannelTvgIdInput.value.trim(); if (!channelData.attributes) channelData.attributes = {}; channelData.attributes['ch-number'] = dom.editorChannelChNumInput.value.trim(); channelData.favorite = dom.editorFavCheckbox.checked; channelData.attributes['hidden'] = dom.editorHideChannelCheckbox.checked ? 'true' : 'false'; channelData.kodiProps = channelData.kodiProps || {}; channelData.kodiProps['inputstream.adaptive.license_type'] = dom.editorKodiLicenseTypeInput.value.trim(); channelData.kodiProps['inputstream.adaptive.license_key'] = dom.editorKodiLicenseKeyInput.value.trim(); channelData.kodiProps['inputstream.adaptive.stream_headers'] = dom.editorKodiStreamHeadersInput.value.trim(); channelData.vlcOptions = channelData.vlcOptions || {}; channelData.vlcOptions['http-user-agent'] = dom.editorVlcUserAgentInput.value.trim(); if (oldGroup !== channelData['group-title']) { updateGroupOrder(); } renderTable(); showToast('Canal guardado.', 'success'); } function handleEditorPlay(id) { const editorId = id || selectedChannelId; if (!editorId) return; const channel = editorChannels.find(ch => ch.editorId === editorId); if (channel && typeof createPlayerWindow === 'function') { createPlayerWindow(channel); } } function handleEditorDelete() { if(!selectedChannelId) return; const index = editorChannels.findIndex(ch => ch && ch.editorId === selectedChannelId); if (index > -1) { editorChannels.splice(index, 1); showEditorPlaceholder(); renderTable(); } } function deleteSelectedChannels() { if (selectedRowIds.size === 0) return; editorChannels = editorChannels.filter(ch => !selectedRowIds.has(ch.editorId)); selectedRowIds.clear(); showEditorPlaceholder(); renderTable(); updateGroupOrder(); } function handleRenameGroup(oldGroupName) { const newGroupName = prompt(`Renombrar grupo "${escapeHtml(oldGroupName || '(Sin Grupo)')}":`, oldGroupName); if (newGroupName === null || newGroupName === oldGroupName) return; editorChannels.forEach(ch => { if ((ch['group-title'] || '') === oldGroupName) { ch['group-title'] = newGroupName; } }); const groupIndex = groupOrder.indexOf(oldGroupName); if (groupIndex > -1) { groupOrder[groupIndex] = newGroupName; } updateGroupOrder(); renderTable(); showToast('Grupo renombrado.', 'success'); } function updateGroupFilter() { const currentFilterValue = dom.groupFilterSelect.value; dom.groupFilterSelect.innerHTML = ''; groupOrder.forEach(group => { const displayName = group === '' ? '(Sin Grupo)' : group; dom.groupFilterSelect.insertAdjacentHTML('beforeend', ``); }); dom.groupFilterSelect.value = currentFilterValue; } function updateSortableForCurrentView() { if (sortableInstance) { sortableInstance.destroy(); } sortableInstance = new Sortable(dom.tableBody, { animation: 150, handle: '.drag-handle', draggable: currentGroupFilter === '' ? '.group-header-row' : '.channel-row', forceFallback: true, ghostClass: 'sortable-ghost', fallbackClass: 'sortable-fallback', onStart: () => { document.body.classList.add('editor-is-dragging'); }, onEnd: (evt) => { document.body.classList.remove('editor-is-dragging'); const { oldIndex, newIndex, item } = evt; if (oldIndex === newIndex) return; if (item.classList.contains('group-header-row')) { const [movedGroup] = groupOrder.splice(oldIndex, 1); groupOrder.splice(newIndex, 0, movedGroup); } else if (item.classList.contains('channel-row') && currentGroupFilter !== '') { const allVisibleIdsInOrder = Array.from(dom.tableBody.querySelectorAll('.channel-row')).map(row => row.dataset.editorId); const channelsInCurrentGroup = editorChannels.filter(ch => (ch['group-title'] || '') === currentGroupFilter); const channelsInOtherGroups = editorChannels.filter(ch => (ch['group-title'] || '') !== currentGroupFilter); const channelMap = new Map(channelsInCurrentGroup.map(ch => [ch.editorId, ch])); const reorderedGroup = allVisibleIdsInOrder.map(id => channelMap.get(id)); editorChannels = [...channelsInOtherGroups, ...reorderedGroup]; } currentSort.column = null; renderTable(); } }); } function handleSelectAllVisible(e) { const isChecked = e.target.checked; dom.tableBody.querySelectorAll('tr:not(.hidden) .row-checkbox').forEach(cb => { const editorId = cb.dataset.editorId; if (isChecked) selectedRowIds.add(editorId); else selectedRowIds.delete(editorId); cb.checked = isChecked; }); updateMultiEditButtonState(); } function handleRowCheckboxChange(e) { if (!e.target.classList.contains('row-checkbox')) return; const editorId = e.target.dataset.editorId; if (e.target.checked) selectedRowIds.add(editorId); else selectedRowIds.delete(editorId); updateSelectAllCheckboxState(); updateMultiEditButtonState(); } function updateSelectAllCheckboxState() { const visibleCheckboxes = Array.from(dom.tableBody.querySelectorAll('tr:not(.hidden) .row-checkbox')); if (visibleCheckboxes.length === 0) { dom.selectAllCheckbox.checked = false; dom.selectAllCheckbox.indeterminate = false; return; } const numSelectedVisible = visibleCheckboxes.filter(cb => cb.checked).length; dom.selectAllCheckbox.checked = numSelectedVisible > 0 && numSelectedVisible === visibleCheckboxes.length; dom.selectAllCheckbox.indeterminate = numSelectedVisible > 0 && numSelectedVisible < visibleCheckboxes.length; } function updateMultiEditButtonState() { const hasSelection = selectedRowIds.size > 0; dom.multiEditBtn.disabled = !hasSelection; dom.deleteSelectedBtn.disabled = !hasSelection; } function clearMultiSelection() { selectedRowIds.clear(); if (dom.tableBody) { dom.tableBody.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = false); } updateSelectAllCheckboxState(); updateMultiEditButtonState(); } function showToast(message, type = 'info', duration = 3000) { if (typeof window.showNotification === 'function') { window.showNotification(message, type, duration); } } function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } function updateGroupSuggestions() { if (!dom.groupSuggestionsDatalist) return; dom.groupSuggestionsDatalist.innerHTML = ''; groupOrder.forEach(group => { if (group) { dom.groupSuggestionsDatalist.insertAdjacentHTML('beforeend', ``); } }); } function handleSort(column) { if (currentSort.column === column) { currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; } else { currentSort.column = column; currentSort.direction = 'asc'; } renderTable(); } function updateSortIcons() { dom.modal.querySelectorAll('th.sortable i').forEach(icon => icon.className = 'fas fa-sort'); if (currentSort.column) { const th = dom.modal.querySelector(`th[data-sort="${currentSort.column}"]`); if (th) th.querySelector('i').className = `fas fa-sort-${currentSort.direction === 'asc' ? 'up' : 'down'}`; } } function openMultiEditModal() { dom.multiEditChannelCount.textContent = selectedRowIds.size; const modal = new bootstrap.Modal(dom.multiEditModal); modal.show(); } function handleApplyMultiEdit() { const changes = {}; if (dom.multiEditEnableGroup.checked) changes.group = dom.multiEditGroupInput.value.trim(); if (dom.multiEditEnableFavorite.checked) changes.favorite = dom.multiEditFavoriteSelect.value; if (dom.multiEditEnableHidden.checked) changes.hidden = dom.multiEditHiddenSelect.value; if (dom.multiEditEnableUserAgent.checked) changes.userAgent = dom.multiEditUserAgentInput.value.trim(); if (dom.multiEditEnableStreamHeaders.checked) { changes.streamHeaders = dom.multiEditStreamHeadersInput.value.trim(); changes.streamHeadersMode = dom.multiEditStreamHeadersMode.value; } selectedRowIds.forEach(id => { const channel = editorChannels.find(ch => ch.editorId === id); if (!channel) return; if (changes.group !== undefined) channel['group-title'] = changes.group; if (changes.favorite !== undefined) channel.favorite = changes.favorite === 'add'; if (changes.hidden !== undefined) { if (!channel.attributes) channel.attributes = {}; channel.attributes.hidden = changes.hidden === 'hide' ? 'true' : 'false'; } if (changes.userAgent !== undefined) { if (!channel.vlcOptions) channel.vlcOptions = {}; channel.vlcOptions['http-user-agent'] = changes.userAgent; } if (changes.streamHeaders !== undefined) { if (!channel.kodiProps) channel.kodiProps = {}; if (changes.streamHeadersMode === 'replace' || !channel.kodiProps['inputstream.adaptive.stream_headers']) { channel.kodiProps['inputstream.adaptive.stream_headers'] = changes.streamHeaders; } else { const existingHeaders = new Map(channel.kodiProps['inputstream.adaptive.stream_headers'].split('|').map(h => { const p = h.split('='); return [p[0], p.slice(1).join('=')]; })); changes.streamHeaders.split('|').forEach(h => { const p = h.split('='); if(p[0]) existingHeaders.set(p[0], p.slice(1).join('=')); }); channel.kodiProps['inputstream.adaptive.stream_headers'] = Array.from(existingHeaders).map(([k,v]) => `${k}=${v}`).join('|'); } } }); updateGroupOrder(); renderTable(); showToast(`${selectedRowIds.size} canales actualizados.`, 'success'); const modal = bootstrap.Modal.getInstance(dom.multiEditModal); modal.hide(); } function getFinalData() { const groupIndexMap = new Map(groupOrder.map((group, index) => [group, index])); const channelsByGroup = {}; editorChannels.forEach(ch => { const group = ch['group-title'] || ''; if (!channelsByGroup[group]) channelsByGroup[group] = []; channelsByGroup[group].push(ch); }); const finalOrderedChannels = []; groupOrder.forEach(group => { if (channelsByGroup[group]) { finalOrderedChannels.push(...channelsByGroup[group]); } }); const remainingChannels = editorChannels.filter(ch => !groupIndexMap.has(ch['group-title'] || '')); finalOrderedChannels.push(...remainingChannels); return { channels: finalOrderedChannels, groupOrder: groupOrder }; } return { init: init, getFinalData: getFinalData }; })();