import { state } from './state.js'; import { fetchTMDB, fetchAllStreamsFromPlex } from './api.js'; import { showNotification, getRelativeTime, fetchWithTimeout, _ } from './utils.js'; import { getFromDB, addItemsToStore } from './db.js'; let charts = {}; export async function loadInitialContent() { await Promise.all([loadGenres(), loadYears()]); await Promise.all([loadContent(), initializeHeroSection()]); setupScrollEffects(); } export function initializeUserData() { try { const savedHistory = localStorage.getItem('cineplex_userHistory'); state.userHistory = savedHistory ? JSON.parse(savedHistory) : []; if (!Array.isArray(state.userHistory)) state.userHistory = []; } catch { state.userHistory = []; } try { const savedPrefs = localStorage.getItem('cineplex_userPreferences'); state.userPreferences = savedPrefs ? JSON.parse(savedPrefs) : { genres: {}, keywords: {}, ratings: [], cast: {}, crew: {} }; } catch { state.userPreferences = { genres: {}, keywords: {}, ratings: [], cast: {}, crew: {} }; } } export async function loadLocalContent() { if (!state.db) return; try { const [movies, series, artists, photos] = await Promise.all([getFromDB('movies'), getFromDB('series'), getFromDB('artists'), getFromDB('photos')]); state.localMovies = movies; state.localSeries = series; state.localArtists = artists; state.localPhotos = photos; } catch (error) { showNotification(_("errorLoadingLocalContent"), "error"); } } export function resetView() { switchView('movies'); } export function switchView(viewType) { if (state.isLoading) return; const sidebar = document.getElementById('sidebar-nav'); if (sidebar.classList.contains('open')) { sidebar.classList.remove('open'); document.getElementById('main-container').classList.remove('sidebar-open'); } const mainContent = document.querySelector('.main-content'); const topBarHeight = document.querySelector('.top-bar')?.offsetHeight || 60; const targetScrollTop = mainContent ? mainContent.offsetTop - topBarHeight : 0; if (state.currentView === viewType && viewType !== 'search') { if (window.scrollY > targetScrollTop) { window.scrollTo({ top: targetScrollTop, behavior: 'smooth' }); } return; } state.currentView = viewType; state.currentPage = 1; state.currentParams.query = ''; document.getElementById('search-input').value = ''; state.lastScrollPosition = 0; const allSections = ['content-section', 'stats-section', 'history-section', 'recommendations-section', 'photos-section']; allSections.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); const filters = document.querySelector('.filters'); if (filters) filters.style.display = 'none'; switch(viewType) { case 'movies': case 'series': case 'search': document.getElementById('content-section').style.display = 'block'; filters.style.display = 'flex'; if (viewType !== 'search') { state.currentParams.contentType = viewType === 'movies' ? 'movie' : 'tv'; resetFilters(false); updateSortOptions(); loadGenres(); } break; case 'favorites': document.getElementById('content-section').style.display = 'block'; break; case 'history': document.getElementById('history-section').style.display = 'block'; break; case 'recommendations': document.getElementById('recommendations-section').style.display = 'block'; break; case 'stats': document.getElementById('stats-section').style.display = 'block'; document.getElementById('stats-filters').style.display = 'flex'; break; case 'photos': document.getElementById('photos-section').style.display = 'block'; break; } updateActiveNav(viewType); updateSectionTitle(); window.scrollTo({ top: targetScrollTop, behavior: 'auto' }); switch(viewType) { case 'movies': case 'series': case 'search': loadContent(); break; case 'favorites': loadFavorites(); break; case 'history': displayHistory(); break; case 'recommendations': loadRecommendations(); break; case 'stats': generateStatistics(); break; case 'photos': initPhotosView(); break; } if (document.getElementById('item-details-view').classList.contains('active')) { showMainView(); } } function updateActiveNav(activeView) { document.querySelectorAll('.nav-link, .footer-link').forEach(link => link.classList.remove('active')); let navId = (activeView === 'search') ? (state.currentParams.contentType === 'movie' ? 'movies' : 'series') : activeView; const activeLink = document.getElementById(`nav-${navId}`); const activeFooterLink = document.getElementById(`footer-${navId}`); if (activeLink) activeLink.classList.add('active'); if (activeFooterLink) activeFooterLink.classList.add('active'); } export function updateSectionTitle() { let title = ""; const mainTitleElement = document.getElementById('main-section-title'); switch (state.currentView) { case 'movies': case 'series': const sortMap = { 'popularity.desc': _('popularSort'), 'vote_average.desc': _('topRatedSort'), 'release_date.desc': _('recentSort'), 'first_air_date.desc': _('recentSort') }; const typeTitle = state.currentView === 'movies' ? _('moviesSectionTitle') : _('seriesSectionTitle'); title = `${typeTitle} ${sortMap[state.currentParams.sort] || ''}`; break; case 'stats': title = _('statsTitle'); break; case 'favorites': title = _('navFavorites'); break; case 'history': title = _('historyTitle'); break; case 'recommendations': title = _('recommendationsTitle'); break; case 'photos': title = _('navPhotos'); break; case 'search': title = state.currentParams.query.startsWith('actor:') ? _('contentFrom', state.currentParams.query.split(':')[1]) : _('searchResultsFor', state.currentParams.query); break; default: title = _('explore'); } let targetTitleElement; if (['stats', 'history', 'recommendations'].includes(state.currentView)) { targetTitleElement = document.querySelector(`#${state.currentView}-section .section-title`); } else { targetTitleElement = mainTitleElement; } if(targetTitleElement && state.currentView !== 'photos') { targetTitleElement.textContent = title; } } export function applyFilters() { state.currentParams.genre = document.getElementById('genre-filter').value; state.currentParams.year = document.getElementById('year-filter').value; state.currentParams.sort = document.getElementById('sort-filter').value; state.currentPage = 1; loadContent(); updateSectionTitle(); } function resetFilters(triggerLoad = true) { state.currentParams.genre = ''; state.currentParams.year = ''; state.currentParams.sort = 'popularity.desc'; document.getElementById('genre-filter').value = ''; document.getElementById('year-filter').value = ''; document.getElementById('sort-filter').value = 'popularity.desc'; if (triggerLoad) { state.currentPage = 1; loadContent(); } } function updateSortOptions() { const sortOption = document.getElementById('sort-release-date'); const isMovie = state.currentParams.contentType === 'movie'; sortOption.textContent = _('sortRecent'); sortOption.value = isMovie ? 'release_date.desc' : 'first_air_date.desc'; } async function loadGenres() { const type = state.currentParams.contentType; const select = document.getElementById('genre-filter'); select.innerHTML = ``; try { const data = await fetchTMDB(`genre/${type}/list`); select.innerHTML = ``; data.genres.forEach(genre => { const option = document.createElement('option'); option.value = genre.id; option.textContent = genre.name; select.appendChild(option); }); select.value = state.currentParams.genre || ""; } catch (error) { select.innerHTML = ``; } } function loadYears() { const select = document.getElementById('year-filter'); select.innerHTML = ``; const currentYear = new Date().getFullYear(); for (let year = currentYear; year >= 1900; year--) { const option = document.createElement('option'); option.value = year; option.textContent = year; select.appendChild(option); } select.value = state.currentParams.year || ""; } export async function loadContent(append = false) { if (state.currentContentFetchController) state.currentContentFetchController.abort(); state.currentContentFetchController = new AbortController(); const signal = state.currentContentFetchController.signal; if (state.isLoading) return; state.isLoading = true; const grid = document.getElementById('content-grid'); const loadMoreButton = document.getElementById('load-more'); if (!append) { grid.innerHTML = '
'; loadMoreButton.style.display = 'none'; } else { loadMoreButton.disabled = true; loadMoreButton.innerHTML = ` ${_('loading')}`; } try { let endpoint = ''; const type = state.currentParams.contentType; if (state.currentView === 'search' && state.currentParams.query) { endpoint = `search/${type}?query=${encodeURIComponent(state.currentParams.query)}&page=${state.currentPage}&include_adult=false`; } else { endpoint = `discover/${type}?page=${state.currentPage}&sort_by=${state.currentParams.sort}&include_adult=false&vote_count.gte=50`; if (state.currentParams.genre) endpoint += `&with_genres=${state.currentParams.genre}`; const yearParam = type === 'movie' ? 'primary_release_year' : 'first_air_date_year'; if (state.currentParams.year) endpoint += `&${yearParam}=${state.currentParams.year}`; } const data = await fetchTMDB(endpoint, signal); renderGrid(data.results, append); loadMoreButton.style.display = (data.page < data.total_pages) ? 'block' : 'none'; if (!append) setupScrollEffects(); } catch (error) { if (error.name !== 'AbortError') { if (!append) grid.innerHTML = `

${_('couldNotLoadContent')}

`; } } finally { state.isLoading = false; if (append) { loadMoreButton.disabled = false; loadMoreButton.textContent = _('loadMore'); } } } function buscarContenidoLocal(title, type) { if (!title || !type) return null; const normalizedTitle = title.toLowerCase().trim(); const source = type === 'movie' ? state.localMovies : state.localSeries; if (!Array.isArray(source)) return null; return source.find(server => server && Array.isArray(server.titulos) && server.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle) ) || null; } function renderGrid(items, append = false) { const grid = document.getElementById('content-grid'); if (!append) grid.innerHTML = ''; if (!items || items.length === 0) { if (!append) { const emptyStateTarget = document.getElementById('recommendations-grid') || grid; const emptyIcon = state.currentView === 'recommendations' ? 'fa-user-astronaut' : 'fa-film'; const emptyText = state.currentView === 'recommendations' ? _('noRecommendations') : _('noContentFound'); emptyStateTarget.innerHTML = `

${emptyText}

`; } document.getElementById('load-more').style.display = 'none'; return; } const fragment = document.createDocumentFragment(); items.forEach((item, index) => { if (!item || !item.id) return; const isMovie = item.media_type === 'movie' || (item.hasOwnProperty('title') && !item.hasOwnProperty('name')); const itemType = isMovie ? 'movie' : 'tv'; const title = isMovie ? item.title : item.name; const releaseDate = isMovie ? item.release_date : item.first_air_date; const year = releaseDate ? releaseDate.slice(0, 4) : 'N/A'; const posterPath = item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : 'img/no-poster.png'; const isAvailable = !!buscarContenidoLocal(title, itemType); const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === itemType); const voteAvg = item.vote_average ? item.vote_average.toFixed(1) : 'N/A'; const ratingClass = voteAvg >= 7.5 ? 'rating-good' : (voteAvg >= 5.0 ? 'rating-ok' : 'rating-bad'); const card = document.createElement('div'); card.className = `item-card`; card.dataset.id = item.id; card.dataset.type = itemType; card.innerHTML = `
${voteAvg >= 7.8 ? 'TOP' : ''} ${isAvailable ? ` ${_('local')}` : ''}
${isAvailable ? ` ` : ``}

${title}

${year} ${voteAvg !== 'N/A' ? ` ${voteAvg}` : ''}
`; fragment.appendChild(card); }); const targetGrid = state.currentView === 'recommendations' ? document.getElementById('recommendations-grid') : grid; if(!append) targetGrid.innerHTML = ''; targetGrid.appendChild(fragment); if (items.length > 0) setupScrollEffects(); } export function showMainView() { const detailsView = document.getElementById('item-details-view'); const mainView = document.getElementById('main-view'); const detailsContent = document.getElementById('item-details-content'); if (!state.lastClickedCardElement) { detailsView.classList.remove('active'); document.body.classList.remove('details-view-active'); mainView.style.display = 'block'; mainView.style.opacity = '1'; window.scrollTo({ top: state.lastScrollPosition, behavior: 'auto' }); state.currentItemId = null; state.currentItemType = null; return; } const targetRect = state.lastClickedCardElement.getBoundingClientRect(); mainView.style.display = 'block'; const tl = gsap.timeline({ onComplete: () => { detailsView.classList.remove('active'); document.body.classList.remove('details-view-active'); state.currentItemId = null; state.currentItemType = null; state.lastClickedCardElement = null; window.scrollTo({ top: state.lastScrollPosition, behavior: 'auto' }); gsap.set(mainView, { clearProps: 'all' }); gsap.set(detailsView, { clearProps: 'all', display: 'none' }); } }); tl.set(detailsView, { overflow: 'hidden' }) .to(detailsContent, { opacity: 0, duration: 0.3, ease: 'power2.in' }) .to(detailsView, { top: targetRect.top, left: targetRect.left, width: targetRect.width, height: targetRect.height, borderRadius: '18px', duration: 0.6, ease: 'power3.inOut' }, 0) .to(mainView, { opacity: 1, scale: 1, duration: 0.5, ease: 'power3.out' }, "-=0.4"); } export async function showItemDetails(itemId, contentType) { if (!itemId || !contentType) return; state.currentItemId = itemId; state.currentItemType = contentType; const detailsView = document.getElementById('item-details-view'); const mainView = document.getElementById('main-view'); const detailsContent = document.getElementById('item-details-content'); if (mainView.style.display !== 'none') state.lastScrollPosition = window.scrollY; document.body.classList.add('details-view-active'); detailsContent.innerHTML = '
'; detailsView.classList.add('active'); const startRect = state.lastClickedCardElement ? state.lastClickedCardElement.getBoundingClientRect() : { top: window.innerHeight / 2, left: window.innerWidth / 2, width: 0, height: 0 }; const tl = gsap.timeline({ onComplete: () => { mainView.style.display = 'none'; gsap.set(detailsView, { overflowY: 'auto' }); } }); tl.set(detailsView, { top: startRect.top, left: startRect.left, width: startRect.width, height: startRect.height, borderRadius: '18px', overflow: 'hidden', display: 'block' }) .to(detailsView, { top: 0, left: 0, width: '100vw', height: '100vh', borderRadius: '0px', duration: 0.7, ease: 'power3.inOut' }) .to(mainView, { opacity: 0, scale: 0.98, duration: 0.6, ease: 'power2.inOut' }, 0); gsap.fromTo(detailsContent, { opacity: 0 }, { opacity: 1, duration: 0.6, ease: 'power2.out', delay: 0.5 } ); state.isLoading = true; try { const isMovie = contentType === 'movie'; let appendToResponse = 'videos,credits,keywords,release_dates,external_ids,similar'; if (!isMovie) { appendToResponse += ',content_ratings,aggregate_credits'; } const item = await fetchTMDB(`${contentType}/${itemId}?append_to_response=${appendToResponse}`); if (!isMovie && item.seasons && item.seasons.length > 0) { const seasonPromises = item.seasons .filter(s => s.season_number > 0) .map(s => fetchTMDB(`${contentType}/${itemId}/season/${s.season_number}`).catch(() => null)); const seasonsData = await Promise.all(seasonPromises); item.seasons_with_episodes = seasonsData.filter(s => s !== null); } updateUserData(item); await renderItemDetails(item); } catch (error) { detailsContent.innerHTML = `

${_('errorLoadingDetails')}

${error.message}

`; } finally { state.isLoading = false; } } async function renderItemDetails(item) { const detailsContent = document.getElementById('item-details-content'); const isMovie = state.currentItemType === 'movie'; const title = isMovie ? item.title : item.name; const backdropPath = item.backdrop_path ? `https://image.tmdb.org/t/p/original${item.backdrop_path}` : ''; const posterPath = item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : 'img/no-poster.png'; const tagline = item.tagline || ''; const overview = item.overview || _('noSynopsis'); const releaseDate = isMovie ? item.release_date : item.first_air_date; const year = releaseDate ? releaseDate.slice(0, 4) : 'N/A'; const voteAverage = item.vote_average ? item.vote_average.toFixed(1) : 'N/A'; const genres = item.genres || []; const trailer = item.videos?.results?.find(v => v.site === 'YouTube' && v.type === 'Trailer'); const isAvailable = !!buscarContenidoLocal(title, state.currentItemType); const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === state.currentItemType); const imdbId = item.external_ids?.imdb_id; const crew = (isMovie ? item.credits?.crew : item.aggregate_credits?.crew) || []; const director = crew.find(c => c.job === 'Director'); const writer = crew.find(c => c.job === 'Screenplay' || c.job === 'Writer' || c.job === 'Story'); detailsContent.innerHTML = ` ${backdropPath ? `
` : ''}

${title}

${tagline ? `

"${tagline}"

` : ''}
${voteAverage !== 'N/A' ? ` ${voteAverage}/10` : ''} ${year} ${isMovie && item.runtime ? ` ${_('runtimeMinutes', String(item.runtime))}` : ''} ${!isMovie && item.number_of_seasons ? ` ${_('seasonsCount', String(item.number_of_seasons))}` : ''}
${genres.map(g => `${g.name}`).join('')}
${director ? `

${_('director')} ${director.name}

` : ''} ${writer ? `

${_('writer')} ${writer.name}

` : ''}
${imdbId ? `` : ''}

${_('synopsis')}

${overview}

${trailer ? `` : ''} ${isAvailable ? ` ` : ``}
${(isMovie ? item.credits?.cast?.length > 0 : item.aggregate_credits?.cast?.length > 0) ? `

${_('mainCast')}

${((isMovie ? item.credits.cast : item.aggregate_credits.cast) || []).slice(0, 12).map(actor => `
${actor.name}
${actor.roles ? actor.roles.map(r=>r.character).slice(0,2).join(', ') : actor.character}
`).join('')}
` : ''}
${!isMovie && item.seasons_with_episodes && item.seasons_with_episodes.length > 0 ? `

${_('seasonsAndEpisodes')}

${item.seasons_with_episodes.map((season, index) => `

${season.episodes.map(ep => `
${ep.episode_number}
${ep.name}
${ep.air_date || 'N/A'} ${ep.vote_average.toFixed(1)}/10

${ep.overview || _('noSynopsis')}

`).join('')}
`).join('')}
` : ''}
${item.similar?.results?.length > 0 ? `

${_('similarContent')}

${item.similar.results.slice(0, 8).map(similar => `
${similar.title || similar.name}
`).join('')}
` : ''}
`; } function updateUserData(item) { if (!item || !item.id || !state.currentItemType) return; const { userHistory, userPreferences } = state; const contentType = state.currentItemType; const historyItem = { id: item.id, type: contentType, title: contentType === 'movie' ? item.title : item.name, poster: item.poster_path, timestamp: Date.now() }; state.userHistory = [historyItem, ...userHistory.filter(h => !(h.id === item.id && h.type === contentType))].slice(0, 100); item.genres?.forEach(g => { userPreferences.genres[g.id] = (userPreferences.genres[g.id] || 0) + 1; }); (item.keywords?.keywords || item.keywords?.results || []).slice(0, 10).forEach(kw => { userPreferences.keywords[kw.id] = (userPreferences.keywords[kw.id] || 0) + 1; }); if (item.vote_average > 0) userPreferences.ratings.push(item.vote_average); if (userPreferences.ratings.length > 100) userPreferences.ratings.shift(); const credits = contentType === 'tv' && item.aggregate_credits ? item.aggregate_credits : item.credits; credits?.cast?.slice(0, 15).forEach(a => { userPreferences.cast[a.id] = (userPreferences.cast[a.id] || 0) + 1; }); credits?.crew?.slice(0, 15).forEach(c => { userPreferences.crew[c.id] = (userPreferences.crew[c.id] || 0) + 1; }); localStorage.setItem('cineplex_userHistory', JSON.stringify(state.userHistory)); localStorage.setItem('cineplex_userPreferences', JSON.stringify(userPreferences)); } export function initializeFavorites() { try { const saved = localStorage.getItem('cineplex_favorites'); state.favorites = saved ? JSON.parse(saved) : []; if (!Array.isArray(state.favorites)) state.favorites = []; } catch { state.favorites = []; } } export function toggleFavorite(itemId, itemType) { if (!itemId || !itemType) return; const index = state.favorites.findIndex(fav => fav.id === itemId && fav.type === itemType); let isFavoriteNow = false; if (index === -1) { state.favorites.push({ id: itemId, type: itemType }); showNotification(_('addedToFavorites'), 'success'); isFavoriteNow = true; } else { state.favorites.splice(index, 1); showNotification(_('removedFromFavorites'), 'info'); } localStorage.setItem('cineplex_favorites', JSON.stringify(state.favorites)); updateFavoriteButtonVisuals(itemId, itemType, isFavoriteNow); if (state.currentView === 'favorites') loadFavorites(); } function updateFavoriteButtonVisuals(itemId, itemType, isFavorite) { document.querySelectorAll(`.item-card[data-id="${itemId}"][data-type="${itemType}"] .favorites-btn`).forEach(btn => { btn.classList.toggle('active', isFavorite); btn.querySelector('i').className = `fas ${isFavorite ? 'fa-heart-broken' : 'fa-heart'} fa-lg`; }); if (document.getElementById('item-details-view').classList.contains('active') && state.currentItemId === itemId && state.currentItemType === itemType) { const detailsBtn = document.querySelector('#item-details-content .favorites-btn'); if(detailsBtn) { detailsBtn.classList.toggle('btn-danger', isFavorite); detailsBtn.classList.toggle('btn-outline-danger', !isFavorite); detailsBtn.querySelector('i').className = `fas ${isFavorite ? 'fa-heart-broken' : 'fa-heart'} me-1`; detailsBtn.lastChild.textContent = ` ${isFavorite ? _('removeFromFavorites') : _('addToFavorites')}`; } } } export async function loadFavorites() { const grid = document.getElementById('content-grid'); grid.innerHTML = '
'; if (state.favorites.length === 0) { grid.innerHTML = `

${_('noFavorites')}

`; return; } try { const favoritePromises = state.favorites.map(fav => fetchTMDB(`${fav.type}/${fav.id}`).catch(()=>null)); const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null); renderGrid(favoriteItems, false); } catch (error) { grid.innerHTML = `

${_('errorLoadingFavorites')}

`; } } export function displayHistory() { const listContainer = document.getElementById('history-list'); listContainer.innerHTML = ""; if (state.userHistory.length === 0) { listContainer.innerHTML = `

${_('historyEmpty')}

${_('historyEmptySub')}

`; document.getElementById('clear-history-btn').style.display = 'none'; return; } document.getElementById('clear-history-btn').style.display = 'inline-flex'; const fragment = document.createDocumentFragment(); [...state.userHistory].sort((a,b) => b.timestamp - a.timestamp).forEach(item => { const posterUrl = item.poster ? `https://image.tmdb.org/t/p/w92${item.poster}` : 'img/no-poster.png'; const isAvailable = !!buscarContenidoLocal(item.title, item.type); const historyItem = document.createElement('div'); historyItem.className = 'history-item'; historyItem.dataset.id = item.id; historyItem.dataset.type = item.type; historyItem.dataset.title = item.title; historyItem.innerHTML = `
${item.title}
${isAvailable ? `${_('local')}` : ''}
${_('viewed')} ${getRelativeTime(item.timestamp)}
`; fragment.appendChild(historyItem); }); listContainer.appendChild(fragment); } export function deleteHistoryItem(id, type, element) { const numericId = Number(id); state.userHistory = state.userHistory.filter(item => !(item.id === numericId && item.type === type)); localStorage.setItem('cineplex_userHistory', JSON.stringify(state.userHistory)); gsap.to(element, { height: 0, opacity: 0, paddingTop: 0, paddingBottom: 0, marginTop: 0, marginBottom: 0, duration: 0.5, ease: "power3.inOut", onComplete: () => { element.remove(); if (state.userHistory.length === 0) { displayHistory(); } } }); showNotification(_('historyItemDeleted'), "info"); } export function clearAllHistory() { if (confirm(_('confirmClearHistory'))) { state.userHistory = []; localStorage.setItem('cineplex_userHistory', JSON.stringify(state.userHistory)); displayHistory(); showNotification(_('historyCleared'), "success"); } } export async function getTrailerKey(id, type) { try { const data = await fetchTMDB(`${type}/${id}/videos`); const trailer = data.results.find(v => v.site === 'YouTube' && v.type === 'Trailer') || data.results.find(v => v.site === 'YouTube'); return trailer ? trailer.key : null; } catch (error) { return null; } } export async function loadRecommendations() { const grid = document.getElementById('recommendations-grid'); grid.innerHTML = '
'; const cachedRecs = sessionStorage.getItem('cineplex_recommendations'); if (cachedRecs) { const { timestamp, data } = JSON.parse(cachedRecs); if (Date.now() - timestamp < 1000 * 60 * 30) { renderGrid(data); return; } } const seedPool = [...state.favorites, ...state.userHistory.slice(0, 10)]; if (seedPool.length === 0) { renderGrid([]); return; } try { const seed = seedPool[Math.floor(Math.random() * seedPool.length)]; const endpoint = `${seed.type}/${seed.id}/recommendations?page=1`; const data = await fetchTMDB(endpoint); const seenIds = new Set(state.userHistory.map(item => item.id)); const recommendations = data.results.filter(rec => !seenIds.has(rec.id)).slice(0, 12); if (recommendations.length > 0) { renderGrid(recommendations); sessionStorage.setItem('cineplex_recommendations', JSON.stringify({ timestamp: Date.now(), data: recommendations })); } else { renderGrid([]); } } catch(error) { grid.innerHTML = `

${_('errorGeneratingRecommendations')}

`; } } async function populateStatsTokenFilter() { const select = document.getElementById('stats-token-filter'); const currentValue = select.value; select.innerHTML = ``; try { const tokensData = await getFromDB('tokens'); const primaryTokens = [...new Set(tokensData.map(t => t.token))]; const connections = await getFromDB('conexiones_locales'); const tokenNames = {}; connections.forEach(conn => { if (conn.tokenPrincipal && !tokenNames[conn.tokenPrincipal]) { tokenNames[conn.tokenPrincipal] = conn.nombre || `Token...${conn.tokenPrincipal.slice(-4)}`; } }); primaryTokens.forEach(token => { const option = document.createElement('option'); option.value = token; option.textContent = tokenNames[token] || `Token...${token.slice(-4)}`; select.appendChild(option); }); if (currentValue) { select.value = currentValue; } } catch(e) { select.innerHTML = ``; } } export async function generateStatistics() { const loader = document.getElementById('stats-loader'); const content = document.getElementById('stats-content'); loader.style.display = 'block'; content.style.display = 'none'; Object.values(charts).forEach(chart => chart.destroy()); charts = {}; await populateStatsTokenFilter(); try { const selectedToken = document.getElementById('stats-token-filter').value; const filterByToken = (data) => { if (selectedToken === 'all') return data; return data.filter(server => server.tokenPrincipal === selectedToken); }; const filteredMovies = filterByToken(state.localMovies); const filteredSeries = filterByToken(state.localSeries); const filteredArtists = filterByToken(state.localArtists); const allMovieItems = filteredMovies.flatMap(s => s.titulos); const allSeriesItems = filteredSeries.flatMap(s => s.titulos); const uniqueMovieTitles = new Set(allMovieItems.map(item => item.title)); const uniqueSeriesTitles = new Set(allSeriesItems.map(item => item.title)); const uniqueArtists = new Set(filteredArtists.flatMap(s => s.titulos.map(t => t.title))); animateValue('total-movies', 0, uniqueMovieTitles.size, 1000); animateValue('total-series', 0, uniqueSeriesTitles.size, 1000); animateValue('total-artists', 0, uniqueArtists.size, 1000); const allTokens = await getFromDB('tokens'); const allConnections = await getFromDB('conexiones_locales'); const filteredTokens = selectedToken === 'all' ? allTokens : allTokens.filter(t => t.token === selectedToken); const filteredConnections = selectedToken === 'all' ? allConnections : allConnections.filter(c => c.tokenPrincipal === selectedToken); animateValue('total-tokens', 0, filteredTokens.length, 1000); animateValue('total-servers', 0, filteredConnections.length, 1000); updateTokenDetailsCard(selectedToken, filteredConnections); const movieGenres = processLocalGenres(allMovieItems); const seriesGenres = processLocalGenres(allSeriesItems); const allDecades = processLocalDecades([...allMovieItems, ...allSeriesItems]); createGenreChart('movie-genres-chart', movieGenres, _('statsChartMoviesByGenre')); createGenreChart('series-genres-chart', seriesGenres, _('statsChartSeriesByGenre')); createDecadeChart('decade-chart', allDecades, _('statsChartByDecade')); loader.style.display = 'none'; content.style.display = 'grid'; gsap.from(".stats-card, .chart-container", { duration: 0.7, y: 30, opacity: 0, stagger: 0.1, ease: "power3.out" }); } catch (error) { loader.innerHTML = `

${_('errorGeneratingStats')}

`; } } function updateTokenDetailsCard(selectedToken, allConnections) { const card = document.getElementById('token-details-card'); const serverList = document.getElementById('token-server-list'); if (selectedToken === 'all') { card.style.display = 'none'; return; } const associatedServers = allConnections.filter(c => c.tokenPrincipal === selectedToken); if (associatedServers.length === 0) { serverList.innerHTML = `
  • ${_('noServersForToken')}
  • `; } else { serverList.innerHTML = associatedServers.map(s => `
  • ${s.nombre}: ${s.token.slice(0, 5)}...${s.token.slice(-5)}
  • ` ).join(''); } card.style.display = 'block'; } function processLocalGenres(items) { const genreCounts = {}; items.forEach(item => { const genre = item.genre || _('noGenre'); genreCounts[genre] = (genreCounts[genre] || 0) + 1; }); return genreCounts; } function processLocalDecades(items) { const decadeCounts = {}; items.forEach(item => { if (item.year && !isNaN(item.year)) { const decade = Math.floor(parseInt(item.year, 10) / 10) * 10; decadeCounts[decade] = (decadeCounts[decade] || 0) + 1; } }); return decadeCounts; } async function createGenreChart(canvasId, genreData, label) { const sortedGenres = Object.entries(genreData) .sort(([, a], [, b]) => b - a) .slice(0, 15); const labels = sortedGenres.map(([name]) => name); const data = sortedGenres.map(([, count]) => count); const ctx = document.getElementById(canvasId).getContext('2d'); charts[canvasId] = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: label, data: data, backgroundColor: 'rgba(0, 224, 255, 0.6)', borderColor: 'rgba(0, 224, 255, 1)', borderWidth: 1, borderRadius: 5, }] }, options: { responsive: true, indexAxis: 'y', scales: { y: { ticks: { color: 'rgba(240, 240, 245, 0.8)' } }, x: { ticks: { color: 'rgba(240, 240, 245, 0.8)' } } }, plugins: { legend: { display: false }, tooltip: { bodyFont: { size: 14 }, titleFont: { size: 16 } } } } }); } function createDecadeChart(canvasId, decadeData, label) { const sortedDecades = Object.entries(decadeData).sort(([a], [b]) => a - b); const labels = sortedDecades.map(([decade]) => `${decade}s`); const data = sortedDecades.map(([, count]) => count); const ctx = document.getElementById(canvasId).getContext('2d'); charts[canvasId] = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: label, data: data, fill: true, backgroundColor: 'rgba(0, 114, 255, 0.2)', borderColor: 'rgba(0, 114, 255, 1)', tension: 0.3, pointBackgroundColor: 'rgba(0, 114, 255, 1)', pointBorderColor: '#fff', pointHoverRadius: 7, }] }, options: { responsive: true, scales: { y: { ticks: { color: 'rgba(240, 240, 245, 0.8)', beginAtZero: true } }, x: { ticks: { color: 'rgba(240, 240, 245, 0.8)' } } }, plugins: { legend: { display: false } } } }); } function animateValue(id, start, end, duration) { const obj = document.getElementById(id); if (!obj) return; let startTimestamp = null; const step = (timestamp) => { if (!startTimestamp) startTimestamp = timestamp; const progress = Math.min((timestamp - startTimestamp) / duration, 1); obj.innerHTML = Math.floor(progress * (end - start) + start).toLocaleString(state.settings.language); if (progress < 1) { window.requestAnimationFrame(step); } }; window.requestAnimationFrame(step); } export async function searchByActor(actorId, actorName) { if (state.isLoading) return; showNotification(_('searchingActorContent', actorName), 'info'); if (document.getElementById('item-details-view').classList.contains('active')) { const mainContent = document.querySelector('.main-content'); const topBarHeight = document.querySelector('.top-bar')?.offsetHeight || 60; const targetScrollTop = mainContent ? mainContent.offsetTop - topBarHeight : 0; state.lastScrollPosition = targetScrollTop; showMainView(); } state.currentView = 'search'; document.getElementById('search-input').value = ``; state.currentParams.query = `actor:${actorName}`; state.currentPage = 1; updateSectionTitle(); updateActiveNav(state.currentParams.contentType); const grid = document.getElementById('content-grid'); grid.innerHTML = '
    '; document.querySelector('.filters').style.display = 'none'; try { const endpoint = `discover/${state.currentParams.contentType}?with_cast=${actorId}&sort_by=popularity.desc&page=1`; const data = await fetchTMDB(endpoint); renderGrid(data.results, false); document.getElementById('load-more').style.display = (data.page < data.total_pages) ? 'block' : 'none'; } catch (error) { grid.innerHTML = `

    ${_('errorLoadingActorContent', actorName)}

    `; } } export async function initializeHeroSection() { const heroSection = document.getElementById('hero-section'); if (heroSection.style.display === 'none') return; try { const type = Math.random() > 0.5 ? 'movie' : 'tv'; const data = await fetchTMDB(`${type}/popular?page=1`); const popularItems = data.results.filter(i => i.backdrop_path && i.overview).slice(0, 8); if (popularItems.length === 0) { heroSection.style.display = 'none'; return; } const bg1 = document.querySelector('.hero-background-1'); const bg2 = document.querySelector('.hero-background-2'); const content = document.querySelector('.hero-content'); let currentBg = bg1; let nextBg = bg2; let currentIndex = -1; function changeHeroSlide(isFirst = false) { currentIndex = (currentIndex + 1) % popularItems.length; const item = popularItems[currentIndex]; const nextImage = new Image(); nextImage.src = `https://image.tmdb.org/t/p/original${item.backdrop_path}`; nextImage.onload = () => { updateHeroContent(item); const heroElements = [ content.querySelector('.hero-title'), content.querySelector('.hero-subtitle'), ...content.querySelectorAll('.hero-meta-item'), content.querySelector('.hero-buttons') ]; if (isFirst) { gsap.set(currentBg, { backgroundImage: `url(${nextImage.src})` }); gsap.to(currentBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.out' }); gsap.to(content, { autoAlpha: 1, duration: 1, delay: 0.5 }); gsap.fromTo(currentBg, { scale: 1.15, transformOrigin: 'center center' }, { scale: 1, duration: 12, ease: 'none' }); } else { const tl = gsap.timeline({ onComplete: () => { gsap.set(currentBg, { autoAlpha: 0 }); const temp = currentBg; currentBg = nextBg; nextBg = temp; } }); tl.to(heroElements, { autoAlpha: 0, y: 30, stagger: 0.08, duration: 0.6, ease: 'power3.in' }); gsap.set(nextBg, { backgroundImage: `url(${nextImage.src})` }); tl.to(nextBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.inOut' }, '-=0.5'); gsap.fromTo(nextBg, { scale: 1.15, transformOrigin: 'center center' }, { scale: 1, duration: 12, ease: 'none' }); tl.fromTo(heroElements, { y: -30, autoAlpha: 0 }, { y: 0, autoAlpha: 1, stagger: 0.1, duration: 0.8, ease: 'power3.out' }, '>-1'); } }; } gsap.set(content, { autoAlpha: 0 }); gsap.set([bg1, bg2], { autoAlpha: 0 }); changeHeroSlide(true); setInterval(() => changeHeroSlide(false), 12000); } catch (error) { console.error("Error initializing hero section:", error); heroSection.style.display = 'none'; } } function updateHeroContent(item) { const heroTitle = document.querySelector('.hero-title'); const heroSubtitle = document.querySelector('.hero-subtitle'); const heroRating = document.querySelector('#hero-rating'); const heroYear = document.querySelector('#hero-year'); const heroExtra = document.querySelector('#hero-extra'); const heroPlayBtn = document.getElementById('hero-play-btn'); const heroInfoBtn = document.getElementById('hero-info-btn'); const type = item.title ? 'movie' : 'tv'; const title = item.title || item.name; const isAvailable = !!buscarContenidoLocal(title, type); if (heroTitle) heroTitle.textContent = title; if (heroSubtitle) heroSubtitle.textContent = item.overview.substring(0, 200) + (item.overview.length > 200 ? '...' : ''); if (heroRating) heroRating.innerHTML = ` ${item.vote_average.toFixed(1)}/10`; if (heroYear) heroYear.innerHTML = ` ${(item.release_date || item.first_air_date).slice(0, 4)}`; if (heroExtra) heroExtra.innerHTML = ` ${type === 'movie' ? _('moviesSectionTitle') : _('seriesSectionTitle')}`; if (heroPlayBtn) { heroPlayBtn.onclick = () => addStreamToList(title, type, heroPlayBtn); heroPlayBtn.disabled = !isAvailable; } if (heroInfoBtn) { heroInfoBtn.onclick = () => showItemDetails(item.id, type); heroInfoBtn.disabled = false; } } export async function addStreamToList(title, type, buttonElement = null) { if (state.isAddingStream) return; state.isAddingStream = true; let originalButtonContent = null; if (buttonElement) { originalButtonContent = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = ``; } if (!state.settings.phpScriptUrl || !state.settings.phpScriptUrl.trim().startsWith('http')) { showNotification(_('phpUrlNotConfigured'), 'warning'); state.isAddingStream = false; if (buttonElement && originalButtonContent) { buttonElement.innerHTML = originalButtonContent; buttonElement.disabled = false; } return; } showNotification(_('searchingStreams', title), 'info'); try { const streamData = await fetchAllStreamsFromPlex(title, type); if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message); showNotification(_('sendingStreams', String(streamData.streams.length)), 'info'); const payload = { streams: streamData.streams.map(s => ({ url: s.url, extinf: s.extinf.replace(/[\r\n]+/g, '') })) }; const response = await fetch(state.settings.phpScriptUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); if (!response.ok || !result.success) throw new Error(result.message || _('errorServerResponse')); showNotification(result.message || _('streamAddedSuccess'), 'success'); } catch (error) { showNotification(_('errorAddingStream', error.message), "error"); } finally { state.isAddingStream = false; if (buttonElement && originalButtonContent) { buttonElement.innerHTML = originalButtonContent; buttonElement.disabled = false; } } } export async function downloadM3U(title, type, buttonElement = null) { if (state.isDownloadingM3U) return; state.isDownloadingM3U = true; let originalButtonContent = null; if (buttonElement) { originalButtonContent = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = ``; } showNotification(_('generatingM3U', title), "info"); try { const streamData = await fetchAllStreamsFromPlex(title, type); if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message); let m3uContent = "#EXTM3U\n"; streamData.streams.forEach(stream => { m3uContent += `${stream.extinf}\n${stream.url}\n`; }); const blob = new Blob([m3uContent], { type: "audio/x-mpegurl;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${title.replace(/[^a-z0-9]/gi, '_')}.m3u`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification(_('m3uDownloaded', title), 'success'); } catch (error) { showNotification(_('errorGeneratingM3U', error.message), "error"); } finally { state.isDownloadingM3U = false; if (buttonElement && originalButtonContent) { buttonElement.innerHTML = originalButtonContent; buttonElement.disabled = false; } } } export function showTrailer(key) { const lightbox = document.getElementById('video-lightbox'); const iframe = document.getElementById('video-iframe'); iframe.src = `https://www.youtube.com/embed/${key}?autoplay=1&modestbranding=1&rel=0`; lightbox.classList.add('active'); document.body.style.overflow = 'hidden'; } export function closeTrailer() { const lightbox = document.getElementById('video-lightbox'); const iframe = document.getElementById('video-iframe'); iframe.src = ''; lightbox.classList.remove('active'); document.body.style.overflow = ''; } function setupScrollEffects() { gsap.utils.toArray('.item-card:not(.gsap-anim), .photo-card:not(.gsap-anim), .album-card:not(.gsap-anim)').forEach((el) => { el.classList.add('gsap-anim'); gsap.from(el, { autoAlpha: 0, y: 50, duration: 0.6, ease: "power2.out", scrollTrigger: { trigger: el, start: "top 95%", toggleActions: "play none none none", }, }); }); } export function activateSettingsTab(tabId) { const tabButtons = document.querySelectorAll('#settingsTabs .nav-link'); const tabPanes = document.querySelectorAll('#settingsTabsContent .tab-pane'); tabButtons.forEach(button => { if (button.id === `${tabId}-tab`) { button.classList.add('active'); button.setAttribute('aria-selected', 'true'); } else { button.classList.remove('active'); button.setAttribute('aria-selected', 'false'); } }); tabPanes.forEach(pane => { if (pane.id === tabId) { pane.classList.add('show', 'active'); } else { pane.classList.remove('show', 'active'); } }); } export function openSettingsModal() { document.getElementById('tmdbApiKey').value = state.settings.apiKey; document.getElementById('appLanguage').value = state.settings.language; document.getElementById('phpScriptUrl').value = state.settings.phpScriptUrl || ''; document.getElementById('lightModeToggle').checked = state.settings.theme === 'light'; document.getElementById('showHeroToggle').checked = state.settings.showHero; document.getElementById('phpSecretKeyCheck').checked = state.settings.phpUseSecretKey; document.getElementById('phpSecretKey').value = state.settings.phpSecretKey || ''; document.getElementById('phpSavePath').value = state.settings.phpSavePath || ''; document.getElementById('phpFilename').value = state.settings.phpFilename || 'CinePlex_Playlist.m3u'; document.getElementById('phpFileActionAppend').checked = state.settings.phpFileAction === 'append'; document.getElementById('phpFileActionOverwrite').checked = state.settings.phpFileAction === 'overwrite'; activateSettingsTab('general'); const modal = new bootstrap.Modal(document.getElementById('settingsModal')); modal.show(); } export async function saveSettings() { const oldLanguage = state.settings.language; const newSettings = { id: 'user_settings', apiKey: document.getElementById('tmdbApiKey').value.trim(), language: document.getElementById('appLanguage').value, theme: document.getElementById('lightModeToggle').checked ? 'light' : 'dark', showHero: document.getElementById('showHeroToggle').checked, phpScriptUrl: document.getElementById('phpScriptUrl').value.trim(), phpUseSecretKey: document.getElementById('phpSecretKeyCheck').checked, phpSecretKey: document.getElementById('phpSecretKey').value.trim(), phpSavePath: document.getElementById('phpSavePath').value.trim(), phpFilename: document.getElementById('phpFilename').value.trim(), phpFileAction: document.getElementById('phpFileActionAppend').checked ? 'append' : 'overwrite' }; state.settings = { ...state.settings, ...newSettings }; try { await addItemsToStore('settings', [state.settings]); showNotification(_('settingsSavedSuccess'), 'success'); applyTheme(state.settings.theme); applyHeroVisibility(state.settings.showHero); bootstrap.Modal.getInstance(document.getElementById('settingsModal'))?.hide(); if (newSettings.language !== oldLanguage) { showNotification(_('languageChangeReload'), 'info', 4000); setTimeout(() => window.location.reload(), 4000); } } catch (error) { showNotification(_('errorSavingSettings'), 'error'); } } export function applyTheme(theme) { document.body.classList.toggle('light-theme', theme === 'light'); } export function applyHeroVisibility(show) { const hero = document.getElementById('hero-section'); if(hero) hero.style.display = show ? 'flex' : 'none'; } export const phpScriptGenerator = (() => { let dom = {}; function cacheDom() { const settingsModal = document.getElementById('settingsModal'); if (!settingsModal) return false; dom.secretKeyCheck = settingsModal.querySelector('#phpSecretKeyCheck'); dom.secretKey = settingsModal.querySelector('#phpSecretKey'); dom.savePath = settingsModal.querySelector('#phpSavePath'); dom.filename = settingsModal.querySelector('#phpFilename'); dom.fileActionAppendRadio = settingsModal.querySelector('#phpFileActionAppend'); dom.generatedCode = settingsModal.querySelector('#generatedPhpCode'); dom.generateBtn = settingsModal.querySelector('#generatePhpScriptBtn'); dom.copyBtn = settingsModal.querySelector('#copyPhpScriptBtn'); return dom.generateBtn && dom.copyBtn; } function init() { if (!cacheDom()) { return; } dom.generateBtn.addEventListener('click', generatePhpScript); dom.copyBtn.addEventListener('click', copyScript); } function generatePhpScript() { const useSecretKey = dom.secretKeyCheck.checked; const secretKey = dom.secretKey.value.trim(); const savePath = dom.savePath.value.trim(); const filename = dom.filename.value.trim() || 'CinePlex_Playlist.m3u'; const appendToFile = dom.fileActionAppendRadio.checked; let script = ` $success, 'message' => $message, 'filename' => $filename]); exit; } `; if (useSecretKey) { script += ` $auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : ''; if (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) { sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403); } `; } script += ` $json_data = file_get_contents('php://input'); $data = json_decode($json_data, true); if (json_last_error() !== JSON_ERROR_NONE) { sendResponse(false, 'Error: Datos JSON inválidos.'); } if (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) { sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.'); } $save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\\\') : __DIR__; if (!is_dir($save_dir) || !is_writable($save_dir)) { sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500); } $safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME)); $safe_filename = preg_replace('/\\s+/', '_', $safe_filename); $target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename; $content_to_write = ""; if (FILE_ACTION_APPEND) { $file_exists = file_exists($target_path); if (!$file_exists) { $content_to_write .= "#EXTM3U\\n"; } foreach ($data['streams'] as $stream) { if (isset($stream['extinf'], $stream['url'])) { $content_to_write .= trim($stream['extinf']) . "\\n"; $content_to_write .= trim($stream['url']) . "\\n"; } } if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) { sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200); } else { sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500); } } else { // Overwrite mode $content_to_write = "#EXTM3U\\n"; foreach ($data['streams'] as $stream) { if (isset($stream['extinf'], $stream['url'])) { $content_to_write .= trim($stream['extinf']) . "\\n"; $content_to_write .= trim($stream['url']) . "\\n"; } } if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) { sendResponse(true, 'Archivo de streams sobrescrito correctamente.', $safe_filename, 201); } else { sendResponse(false, 'Error del servidor: No se pudo escribir el archivo.', '', 500); } } ?>`; dom.generatedCode.value = script; showNotification(_("scriptGenerated"), "success"); } function copyScript() { if (!dom.generatedCode.value || dom.generatedCode.value.trim() === '') { showNotification(_("errorGeneratingScript"), "warning"); return; } navigator.clipboard.writeText(dom.generatedCode.value).then(() => { showNotification(_("scriptCopied"), "success"); }).catch(err => { showNotification(_("errorCopyingScript"), "error"); }); } return { init }; })(); export function initPhotosView() { const select = document.getElementById('photos-token-select'); select.innerHTML = ``; const photoServers = [...new Map(state.localPhotos.map(item => [item.tokenPrincipal, item])).values()]; if (photoServers.length === 0) { select.innerHTML = ``; document.getElementById('photos-empty-state').style.display = 'block'; document.getElementById('photos-grid').innerHTML = ''; return; } document.getElementById('photos-empty-state').style.display = 'none'; select.innerHTML = ``; photoServers.forEach(server => { const option = document.createElement('option'); option.value = server.tokenPrincipal; option.textContent = server.serverName || `Servidor ${server.tokenPrincipal.slice(-4)}`; select.appendChild(option); }); if (state.currentPhotoToken && photoServers.some(s => s.tokenPrincipal === state.currentPhotoToken)) { select.value = state.currentPhotoToken; } else { select.value = photoServers[0].tokenPrincipal; } handlePhotoTokenChange(); } export function handlePhotoTokenChange() { const select = document.getElementById('photos-token-select'); const selectedToken = select.value; if (!selectedToken) { document.getElementById('photos-grid').innerHTML = ''; document.getElementById('photos-empty-state').style.display = 'block'; return; } state.currentPhotoToken = selectedToken; state.currentPhotoServer = state.localPhotos.find(s => s.tokenPrincipal === state.currentPhotoToken); state.photoStack = []; renderPhotoBreadcrumb(); renderPhotoGrid(state.currentPhotoServer.titulos, 'album'); } async function fetchPhotoAlbumContent(albumData) { const { id, token, ip, puerto, protocolo } = albumData; const url = `${protocolo}://${ip}:${puerto}/library/metadata/${id}/children?X-Plex-Token=${token}`; try { const response = await fetchWithTimeout(url, {}, 10000); if (!response.ok) throw new Error(_('errorPlexApi', String(response.status))); const data = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(data, "text/xml"); if (xmlDoc.querySelector('parsererror')) throw new Error(_('errorParsingPlexXml')); const photos = Array.from(xmlDoc.querySelectorAll('Photo')).map(el => { const media = el.querySelector('Media Part'); if (!media) return null; const key = media.getAttribute('key'); return { type: 'photo', id: el.getAttribute('ratingKey'), title: el.getAttribute('title') || _('untitled'), thumbUrl: `${protocolo}://${ip}:${puerto}/photo/:/transcode?width=400&height=400&minSize=1&upscale=1&url=${encodeURIComponent(key)}&X-Plex-Token=${token}`, fullUrl: `${protocolo}://${ip}:${puerto}/photo/:/transcode?width=2000&height=2000&minSize=1&upscale=1&url=${encodeURIComponent(key)}&X-Plex-Token=${token}` }; }).filter(p => p !== null); const subAlbums = Array.from(xmlDoc.querySelectorAll('Directory[type="photo"]')).map(el => ({ type: 'album', id: el.getAttribute('ratingKey'), title: el.getAttribute('title'), count: el.getAttribute('leafCount') || el.getAttribute('childCount'), })); return [...subAlbums, ...photos]; } catch (error) { showNotification(_('errorLoadingAlbum', error.message), 'error'); return []; } } async function loadPhotoAlbum(albumData) { const loader = document.getElementById('photos-loader'); const grid = document.getElementById('photos-grid'); grid.innerHTML = ''; loader.style.display = 'block'; document.getElementById('photos-empty-state').style.display = 'none'; const items = await fetchPhotoAlbumContent(albumData); loader.style.display = 'none'; if (items.length > 0) { renderPhotoGrid(items); } else { document.getElementById('photos-empty-state').style.display = 'block'; } } export async function handlePhotoGridClick(card) { const { type, id, title } = card.dataset; if (type === 'album') { if (!state.currentPhotoServer) { showNotification(_('noPhotoServerSelected'), "error"); return; } const albumData = { ...state.currentPhotoServer, id: id, title: title }; state.photoStack.push({ id, title }); renderPhotoBreadcrumb(); await loadPhotoAlbum(albumData); } else if (type === 'photo') { const photoIndex = state.currentPhotoItems.findIndex(p => p.id === id); if (photoIndex > -1) { openPhotoLightbox(photoIndex); } } } function renderPhotoGrid(items, forceType = null) { const grid = document.getElementById('photos-grid'); grid.innerHTML = ''; document.getElementById('photos-empty-state').style.display = 'none'; state.currentPhotoItems = []; if (!items || items.length === 0) { document.getElementById('photos-empty-state').style.display = 'block'; return; } const fragment = document.createDocumentFragment(); items.forEach(item => { const type = forceType || item.type; const card = document.createElement('div'); if (type === 'album') { card.className = 'album-card'; card.innerHTML = `
    ${item.title}
    ${item.count ? `
    ${_('itemCount', String(item.count))}
    ` : ''} `; card.dataset.type = 'album'; } else if (type === 'photo') { state.currentPhotoItems.push(item); card.className = 'photo-card'; card.innerHTML = ` ${item.title}
    ${item.title}
    `; card.dataset.type = 'photo'; } card.dataset.id = item.id; card.dataset.title = item.title; fragment.appendChild(card); }); grid.appendChild(fragment); setupScrollEffects(); } function renderPhotoBreadcrumb() { const breadcrumb = document.getElementById('photos-breadcrumb'); breadcrumb.innerHTML = ''; const rootItem = document.createElement('li'); rootItem.className = 'breadcrumb-item'; const rootLink = document.createElement('a'); rootLink.href = '#'; rootLink.innerHTML = ` ${_('photosBreadcrumbHome')}`; rootLink.onclick = (e) => { e.preventDefault(); handlePhotoTokenChange(); }; rootItem.appendChild(rootLink); breadcrumb.appendChild(rootItem); state.photoStack.forEach((item, index) => { const divider = document.createElement('li'); divider.className = 'breadcrumb-divider'; divider.innerHTML = ''; breadcrumb.appendChild(divider); const breadcrumbItem = document.createElement('li'); breadcrumbItem.className = 'breadcrumb-item'; if (index === state.photoStack.length - 1) { breadcrumbItem.classList.add('active'); breadcrumbItem.textContent = item.title; } else { const link = document.createElement('a'); link.href = '#'; link.textContent = item.title; link.onclick = async (e) => { e.preventDefault(); state.photoStack = state.photoStack.slice(0, index + 1); renderPhotoBreadcrumb(); if (!state.currentPhotoServer) return; const albumData = { ...state.currentPhotoServer, id: item.id, title: item.title }; await loadPhotoAlbum(albumData); }; breadcrumbItem.appendChild(link); } breadcrumb.appendChild(breadcrumbItem); }); } function openPhotoLightbox(startIndex) { if (state.currentPhotoItems.length === 0 || startIndex < 0) return; state.currentPhotoLightboxIndex = startIndex; const lightbox = document.getElementById('photo-lightbox'); document.body.style.overflow = 'hidden'; updatePhotoLightbox(); gsap.fromTo(lightbox, { autoAlpha: 0 }, { display: 'flex', autoAlpha: 1, duration: 0.3 }); } export function closePhotoLightbox() { const lightbox = document.getElementById('photo-lightbox'); document.body.style.overflow = ''; gsap.to(lightbox, { autoAlpha: 0, duration: 0.3, onComplete: () => lightbox.style.display = 'none' }); } function updatePhotoLightbox() { const photo = state.currentPhotoItems[state.currentPhotoLightboxIndex]; if (!photo) return; const img = document.getElementById('photo-lightbox-img'); const caption = document.getElementById('photo-lightbox-caption'); gsap.to([img, caption], { autoAlpha: 0, duration: 0.15, onComplete: () => { img.src = photo.fullUrl; caption.textContent = photo.title; gsap.to([img, caption], { autoAlpha: 1, duration: 0.15 }); } }); } export function showNextPhoto() { state.currentPhotoLightboxIndex = (state.currentPhotoLightboxIndex + 1) % state.currentPhotoItems.length; updatePhotoLightbox(); } export function showPrevPhoto() { state.currentPhotoLightboxIndex = (state.currentPhotoLightboxIndex - 1 + state.currentPhotoItems.length) % state.currentPhotoItems.length; updatePhotoLightbox(); }