function switchFilter(filterType) { if (currentFilter === filterType) return; currentFilter = filterType; currentPage = 1; $('#groupFilterSidebar').val("").trigger('change'); if (userSettings.persistFilters) { userSettings.lastSelectedFilterTab = currentFilter; localStorage.setItem('zenithUserSettings', JSON.stringify(userSettings)); } updateActiveFilterButton(); filterAndRenderChannels(); } function updateActiveFilterButton() { $('.filter-tab-btn').removeClass('active'); if (currentFilter === 'all') $('#showAllChannels').addClass('active'); else if (currentFilter === 'favorites') $('#showFavorites').addClass('active'); else if (currentFilter === 'history') $('#showHistory').addClass('active'); } function getFilteredChannels() { const search = $('#searchInput').val().toLowerCase().trim(); const selectedGroup = $('#groupFilterSidebar').val() || ""; let baseChannels; if (currentFilter === 'favorites') { baseChannels = favorites.map(url => channels.find(c => c.url === url)).filter(Boolean); } else if (currentFilter === 'history') { baseChannels = appHistory.map(url => channels.find(c => c.url === url)).filter(Boolean); } else { baseChannels = channels; } const filtered = baseChannels.filter(c => c && typeof c.name === 'string' && typeof c.url === 'string' && c.name.toLowerCase().includes(search) && (selectedGroup === "" || c['group-title'] === selectedGroup) ); return filtered; } function getPaginatedChannels() { const filtered = getFilteredChannels(); const totalItems = filtered.length; const itemsPerPage = userSettings.channelsPerPage; const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage)); currentPage = Math.min(Math.max(1, currentPage), totalPages === 0 ? 1 : totalPages); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = Math.min(startIndex + itemsPerPage, totalItems); const paginated = filtered.slice(startIndex, endIndex); return paginated; } function filterAndRenderChannels() { renderChannels(); updatePaginationControls(); updateGroupSelectors(); checkIfChannelsExist(); if (typeof updateEPGProgressBarOnCards === 'function') { updateEPGProgressBarOnCards(); } } function renderChannels() { const grid = $('#channelGrid').empty(); const channelsToShow = getPaginatedChannels(); const noChannelsMessageEl = $('#noChannelsMessage'); document.documentElement.style.setProperty('--card-logo-aspect-ratio', userSettings.cardLogoAspectRatio === 'auto' ? '16/9' : userSettings.cardLogoAspectRatio); $('#channelGridTitle').text("Canales Disponibles"); if (channelsToShow.length > 0) { noChannelsMessageEl.hide(); grid.show(); const fragment = document.createDocumentFragment(); channelsToShow.forEach(channel => { const isFavorite = favorites.includes(channel.url); const card = document.createElement('div'); card.className = 'channel-card'; if (userSettings.compactCardView) { card.classList.add('compact'); } card.dataset.url = channel.url; let logoSrc = ''; const epgIdForLogo = channel.effectiveEpgId || (channel['tvg-id'] || '').toLowerCase().trim(); if (typeof getEpgChannelIcon === 'function' && getEpgChannelIcon(epgIdForLogo)) { logoSrc = getEpgChannelIcon(epgIdForLogo); } else if (channel['tvg-logo']) { logoSrc = channel['tvg-logo']; } let epgInfoHtml = ''; let hasCurrentProgramForProgressBar = false; if (userSettings.cardShowEpg && channel.effectiveEpgId && typeof getEpgDataForChannel === 'function') { const programsForChannel = getEpgDataForChannel(channel.effectiveEpgId); const now = new Date(); const currentProgram = programsForChannel.find(p => now >= p.startDt && now < p.stopDt); const nextProgramIndex = currentProgram ? programsForChannel.indexOf(currentProgram) + 1 : programsForChannel.findIndex(p => p.startDt > now); const nextProgram = (nextProgramIndex !== -1 && nextProgramIndex < programsForChannel.length) ? programsForChannel[nextProgramIndex] : null; if (currentProgram) { hasCurrentProgramForProgressBar = true; epgInfoHtml += `
${escapeHtml(currentProgram.title)}
`; } if (nextProgram && typeof formatEPGTime === 'function') { epgInfoHtml += `
Sig: ${escapeHtml(nextProgram.title)} (${formatEPGTime(nextProgram.startDt)})
`; } } let progressBarHtml = ''; if (userSettings.cardShowEpg && hasCurrentProgramForProgressBar) { progressBarHtml = ` `; } const channelNumber = channel.attributes['ch-number']; const channelNumberHtml = userSettings.cardShowChannelNumber && channelNumber ? `${escapeHtml(channelNumber)}` : ''; card.innerHTML = `
${logoSrc ? `` : ''} ${channelNumberHtml}

${escapeHtml(channel.name)}

${epgInfoHtml ? `
${epgInfoHtml}${progressBarHtml}
` : ''} ${userSettings.cardShowGroup ? `

${escapeHtml(channel['group-title'] || 'Sin Grupo')}

` : ''} ${userSettings.cardShowFavButton ? `` : ''}
`; fragment.appendChild(card); }); grid.append(fragment); } else { grid.hide(); noChannelsMessageEl.show(); } } function renderXtreamContent(items, title) { const grid = $('#channelGrid').empty(); const noChannelsMessageEl = $('#noChannelsMessage'); grid.show(); noChannelsMessageEl.hide(); $('#channelGridTitle').text(title); if (!items || items.length === 0) { noChannelsMessageEl.text("No se encontraron elementos para mostrar.").show(); grid.hide(); return; } const fragment = document.createDocumentFragment(); items.forEach(item => { const card = document.createElement('div'); card.className = 'channel-card'; if (item.season_number !== undefined) { card.dataset.seasonData = JSON.stringify(item); } else { card.dataset.episodeData = JSON.stringify(item); } card.innerHTML = `

${escapeHtml(item.name)}

${escapeHtml(item['group-title'] || '')}

`; fragment.appendChild(card); }); grid.append(fragment); $('#paginationControls').hide(); checkIfChannelsExist(); } function updateGroupSelectors() { const baseOrder = currentGroupOrder.filter(group => group && group.trim() !== ''); let relevantChannels; if (currentFilter === 'favorites') { relevantChannels = channels.filter(c => favorites.includes(c.url)); } else if (currentFilter === 'history') { relevantChannels = appHistory.map(url => channels.find(c => c.url === url)).filter(Boolean); } else { relevantChannels = channels; } const groupCounts = {}; relevantChannels.forEach(c => { const group = c['group-title'] || ''; groupCounts[group] = (groupCounts[group] || 0) + 1; }); const availableGroupsRaw = relevantChannels.map(c => c['group-title'] || ''); const uniqueSortedGroupsInView = getOrderedUniqueGroups(baseOrder, availableGroupsRaw); const currentSelectedGroup = $('#groupFilterSidebar').val(); populateGroupFilterDropdown('#groupFilterSidebar', uniqueSortedGroupsInView, '📂 Todos los grupos', groupCounts, currentSelectedGroup); populateSidebarGroupList('#sidebarGroupList', uniqueSortedGroupsInView, groupCounts, currentSelectedGroup); } function getOrderedUniqueGroups(preferredOrder, availableGroups) { const availableSet = new Set(availableGroups); const ordered = preferredOrder.filter(group => availableSet.has(group)); const unordered = Array.from(availableSet) .filter(group => !preferredOrder.includes(group)) .sort((a, b) => { const aNorm = a === '' ? 'Sin Grupo' : a; const bNorm = b === '' ? 'Sin Grupo' : b; return aNorm.localeCompare(bNorm, undefined, { sensitivity: 'base' }); }); return [...new Set([...ordered, ...unordered])]; } function populateGroupFilterDropdown(selectorId, groups, defaultOptionText, groupCounts = {}, valueToSelect) { const selector = $(selectorId); selector.empty().append(``); groups.forEach(group => { const count = groupCounts[group] || 0; const displayName = group === '' ? 'Sin Grupo' : group; selector.append(``); }); if (groups.includes(valueToSelect) || valueToSelect === "") { selector.val(valueToSelect); } else { selector.val(""); } } function populateSidebarGroupList(listId, groups, groupCounts = {}, valueToSelect) { const list = $(listId).empty(); const fragment = document.createDocumentFragment(); const allGroupsItem = document.createElement('li'); allGroupsItem.className = 'list-group-item'; allGroupsItem.dataset.groupName = ""; let totalChannelsInView = 0; if (currentFilter === 'favorites') { totalChannelsInView = favorites.map(url => channels.find(c => c.url === url)).filter(Boolean).length; } else if (currentFilter === 'history') { totalChannelsInView = appHistory.map(url => channels.find(c => c.url === url)).filter(Boolean).length; } else { totalChannelsInView = channels.length; } if (Object.keys(groupCounts).length > 0 && (currentFilter === 'all' || currentFilter === '')) { totalChannelsInView = Object.values(groupCounts).reduce((sum, count) => sum + count, 0); } allGroupsItem.textContent = `Todos los Grupos (${totalChannelsInView})`; if (valueToSelect === "") $(allGroupsItem).addClass('active'); fragment.appendChild(allGroupsItem); groups.forEach(group => { const item = document.createElement('li'); item.className = 'list-group-item'; item.dataset.groupName = group; const count = groupCounts[group] || 0; const displayName = group === '' ? 'Sin Grupo' : group; item.textContent = `${escapeHtml(displayName)} (${count})`; if (valueToSelect === group) $(item).addClass('active'); fragment.appendChild(item); }); list.append(fragment); } function updatePaginationControls() { const filtered = getFilteredChannels(); const totalItems = filtered.length; const itemsPerPage = userSettings.channelsPerPage; const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage)); currentPage = Math.min(Math.max(1, currentPage), totalPages === 0 ? 1 : totalPages); $('#pageInfo').text(`Pág ${currentPage} de ${totalPages} (${totalItems})`); $('#prevPage').prop('disabled', currentPage <= 1); $('#nextPage').prop('disabled', currentPage >= totalPages || totalPages === 0); $('#paginationControls').toggle(totalItems > itemsPerPage); } function changePage(newPage) { const filtered = getFilteredChannels(); const itemsPerPage = userSettings.channelsPerPage; const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); const targetPage = Math.min(Math.max(1, newPage), totalPages === 0 ? 1 : totalPages); if (targetPage !== currentPage) { currentPage = targetPage; renderChannels(); updatePaginationControls(); const mainContentEl = $('#main-content'); const channelGridEl = $('#channelGrid'); if (channelGridEl.length && mainContentEl.length && channelGridEl.is(":visible")) { const gridRect = channelGridEl[0].getBoundingClientRect(); const mainContentRect = mainContentEl[0].getBoundingClientRect(); let targetScrollPosition = mainContentEl.scrollTop() + gridRect.top - mainContentRect.top - (parseFloat(mainContentEl.css('padding-top')) || 0); targetScrollPosition = Math.max(0, targetScrollPosition); if (currentPage > 1 && (Math.abs(mainContentEl.scrollTop() - targetScrollPosition) > 20 || mainContentEl.scrollTop() > targetScrollPosition) ) { mainContentEl.animate({ scrollTop: targetScrollPosition }, 300); } else if (currentPage === 1 && mainContentEl.scrollTop() > 0) { mainContentEl.animate({ scrollTop: 0 }, 300); } } } } function checkIfChannelsExist() { const hasAnyChannelLoaded = channels.length > 0; const isMainView = currentView.type === 'main'; const filteredChannelsCount = isMainView ? getFilteredChannels().length : currentView.data?.length || 0; const noChannelsMsg = $('#noChannelsMessage'); const paginationControls = $('#paginationControls'); const channelGrid = $('#channelGrid'); const channelGridTitleContainer = $('#channelGridTitle').parent(); const filterTabs = $('.filter-tabs-container'); const downloadBtn = $('#downloadM3UBtnHeader'); if (!hasAnyChannelLoaded) { noChannelsMsg.text(currentM3UContent ? `No se encontraron canales válidos en "${escapeHtml(currentM3UName)}".` : 'Carga una lista M3U (URL o archivo)...').show(); channelGrid.hide(); paginationControls.hide(); channelGridTitleContainer.hide(); filterTabs.hide(); downloadBtn.prop('disabled', true).parent().addClass('disabled'); $('#groupFilterSidebar').prop('disabled', true).val(''); $('#sidebarGroupList').empty().append('
  • Carga una lista M3U
  • '); } else { filterTabs.toggle(isMainView); downloadBtn.prop('disabled', false).parent().removeClass('disabled'); $('#groupFilterSidebar').prop('disabled', !isMainView); if (filteredChannelsCount === 0) { let message = 'No hay canales que coincidan con los filtros/búsqueda.'; if (isMainView) { if (currentFilter === 'favorites' && favorites.length === 0) message = 'No tienes canales favoritos. Haz clic en ★ en una tarjeta para añadir.'; if (currentFilter === 'history' && appHistory.length === 0) message = 'El historial de reproducción está vacío.'; } else { message = "No se encontraron episodios para esta serie."; } noChannelsMsg.text(message).show(); channelGrid.hide(); paginationControls.hide(); channelGridTitleContainer.show(); } else { noChannelsMsg.hide(); channelGrid.show(); channelGridTitleContainer.show(); } } }