/**
* Manages modal display logic for content details and video playback.
*
* This module handles the opening and dynamic rendering of modals for both films and series,
* including displaying metadata, trailers, IMDb links, cast, and watched status. It also manages
* playback of videos in a dedicated modal with resume support and watched progress tracking.
* Series episodes are displayed per season with interactive controls, and the module keeps track
* of the user's playback context to update localStorage as needed.
*
* @module modal
*/
let currentVideoContext = null;
let lastOpenedContent = null;
/**
* Shows the film detail modal for a given film.
*
* @param film - The film object containing metadata like title, description, genres, etc.
*/
function showFilmModal(film) {
if (film) openModal(film.title, "film");
}
/**
* Shows the series detail modal for a given series.
*
* @param series - The series object containing metadata like title, description, seasons, etc.
*/
function showSeriesModal(series) {
if (series) openModal(series.title, "series");
}
/**
* Opens the content detail modal for a given film or series.
*
* Renders the modal content using the provided title and type, and sets up additional UI
* for series (like seasons and episodes). Prevents background scrolling while the modal is open.
*
* @function
* @param {string} title - The title of the content item.
* @param {string} type - The content type: "film" or "series".
*/
function openModal(title, type) {
const item = type === "film" ? filmsData[title] : seriesData[title];
if (!item) return;
const modalBody = document.getElementById("modalBody");
modalBody.innerHTML = createModalContent(item, type);
modal.classList.add("active");
document.body.style.overflow = "hidden";
if (type === "series" && item.seasons) {
setupSeriesModal(item);
}
currentVideoContext = null;
}
/**
* Generates the HTML content of the modal based on the item’s metadata.
*
* Builds a visual layout with poster, title, rating, genres, year, description,
* cast, and available actions like watch/trailer/IMDb buttons.
*
* @function
* @param {Object} item - The content item (film or series).
* @param {string} type - The content type: "film" or "series".
* @returns {string} HTML string to be injected into the modal.
*/
function createModalContent(item, type) {
const genres = item.genres ? item.genres.map((g) => `<span class="genre-tag">${g}</span>`).join("") : "";
const rating = item.IMDb ? `<div class="modal-rating"><i class="fas fa-star"></i> ${item.IMDb}/10</div>` : "";
const year = item.year ? `<span class="year-tag">${item.year}</span>` : "";
const directors = item.directors ? `<p><strong>Réalisateurs:</strong> ${item.directors.join(", ")}</p>` : "";
const writers = item.writers ? `<p><strong>Scénaristes:</strong> ${item.writers.join(", ")}</p>` : "";
const stars = item.stars ? `<p><strong>Acteurs:</strong> ${item.stars.join(", ")}</p>` : "";
const creators = item.creators ? `<p><strong>Créateurs:</strong> ${item.creators.join(", ")}</p>` : "";
let watchedInfo = "";
if (type === "film") {
const watchData = getFilmWatchData(item.title);
if (watchData.watched) {
watchedInfo = `<span class="watched-badge" title="Déjà vu"><i class="fas fa-eye"></i> Vu</span>`;
}
}
const watchButton = type === "film" && item.video ? `<button class="btn btn-primary" onclick="playVideo('${item.video}', 'film', '${escapeForHTML(item.title)}')">
<i class="fas fa-play"></i> Regarder
</button>` : "";
const trailerButton = item.trailer ? `<a href="${item.trailer}" target="_blank" class="btn btn-secondary">
<i class="fas fa-external-link-alt"></i> Bande-annonce
</a>` : "";
const imdbButton = item.IMDb_link ? `<a href="${item.IMDb_link}" target="_blank" class="btn btn-secondary">
<i class="fas fa-external-link-alt"></i> IMDb
</a>` : "";
return `
<div class="modal-body">
<div class="modal-header">
<div class="modal-poster">
${item.banner ? `<img src="${item.banner}" alt="${item.title}" class="modal-poster">` : '<div class="modal-poster" style="display: flex; align-items: center; justify-content: center; background-color: var(--bg-card);"><i class="fas fa-film" style="font-size: 3rem; color: var(--text-secondary);"></i></div>'}
</div>
<div class="modal-info">
<h2 class="modal-title">${item.title} ${watchedInfo}</h2>
<div class="modal-meta">
${rating}
${year}
<div class="card-genres">${genres}</div>
</div>
<p class="modal-description">${item.description || "Aucune description disponible."}</p>
<div class="modal-cast">
${directors}
${creators}
${writers}
${stars}
</div>
<div class="action-buttons">
${watchButton}
${trailerButton}
${imdbButton}
</div>
</div>
</div>
${type === "series" ? '<div id="seasonsContainer"></div>' : ""}
</div>
`;
}
/**
* Sets up the modal UI for a series, including seasons navigation and episodes display.
*
* Builds buttons for each season and loads the episodes for the first one by default.
*
* @function
* @param {Object} series - The series object with seasons and episodes.
*/
function setupSeriesModal(series) {
if (!series.seasons) return;
const seasonsContainer = document.getElementById("seasonsContainer");
const seasons = Object.keys(series.seasons);
seasonsContainer.innerHTML = `
<div class="seasons-section">
<h3>Saisons et Épisodes</h3>
<div class="seasons-nav">
${seasons
.map((season, index) => `<button class="season-btn ${index === 0 ? "active" : ""}" data-season="${season}">
Saison ${season}
</button>`)
.join("")}
</div>
<div id="episodesContainer"></div>
</div>
`;
const seasonBtns = seasonsContainer.querySelectorAll(".season-btn");
seasonBtns.forEach((btn) => {
btn.addEventListener("click", () => {
seasonBtns.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
displayEpisodes(series, btn.dataset.season);
});
});
displayEpisodes(series, seasons[0]);
}
/**
* Displays the list of episodes for a given season in the modal.
*
* Generates episode cards with titles, descriptions, watched status,
* and a click handler to start playback.
*
* @function
* @param {Object} series - The series object.
* @param {string} seasonNumber - The selected season number to display.
*/
function displayEpisodes(series, seasonNumber) {
const episodes = series.seasons[seasonNumber];
const episodesContainer = document.getElementById("episodesContainer");
if (!episodes || episodes.length === 0) {
episodesContainer.innerHTML = "<p>Aucun épisode disponible pour cette saison.</p>";
return;
}
episodesContainer.innerHTML = episodes
.map((episode, index) => {
const watchData = getEpisodeWatchData(series.title, seasonNumber, index);
const watchedClass = watchData.watched ? "watched-episode" : "";
const watchedBadge = watchData.watched ? `<span class="watched-badge" title="Déjà vu"><i class="fas fa-eye"></i></span>` : "";
return `
<div class="episode-item ${watchedClass}" onclick="playVideo('${episode.video}', 'series', '${escapeForHTML(series.title)}', '${seasonNumber}', ${index})">
<div class="episode-number">E${index + 1}</div>
<div class="episode-info">
<div class="episode-title">${episode.title} ${watchedBadge}</div>
<div class="episode-description">${episode.desc}</div>
</div>
<div style="display: flex; align-items: center;">
<i class="fas fa-play" style="color: var(--accent);"></i>
</div>
</div>
`;
})
.join("");
}
/**
* Starts video playback in the video modal and initializes playback context.
*
* Sets the video source, handles resume playback from last watched time,
* and updates `currentVideoContext` for tracking progress.
*
* @function
* @param {string} videoPath - The path to the video file.
* @param {string} type - "film" or "series".
* @param {string} title - Title of the content.
* @param {string} [season] - Season number (only for series).
* @param {number} [epIndex] - Episode index (only for series).
*/
function playVideo(videoPath, type, title, season, epIndex) {
if (!videoPath) {
alert("Vidéo non disponible");
return;
}
lastOpenedContent = { type, title, season, epIndex };
videoPlayer.src = videoPath;
videoModal.classList.add("active");
modal.classList.remove("active");
document.body.style.overflow = "hidden";
let startTime = 0;
if (type === "film") {
const watchData = getFilmWatchData(title);
startTime = watchData.time || 0;
currentVideoContext = {type, title};
} else if (type === "series") {
const watchData = getEpisodeWatchData(title, season, epIndex);
startTime = watchData.time || 0;
currentVideoContext = {type, title, season, epIndex};
}
videoPlayer.currentTime = startTime;
setTimeout(() => {
videoPlayer.currentTime = startTime;
}, 100);
videoPlayer.play();
}
/**
* Updates watch progress as the video plays.
*
* Saves the current time to localStorage and marks the item as fully watched
* if more than 90% of the video is completed.
*
* @function
*/
function handleVideoTimeUpdate() {
if (!currentVideoContext) return;
const {type, title, season, epIndex} = currentVideoContext;
const duration = videoPlayer.duration || 1;
const current = videoPlayer.currentTime;
if (type === "film") {
markFilmWatched(title, false, current);
} else if (type === "series") {
markEpisodeWatched(title, season, epIndex, false, current);
}
const remaining = duration - current;
if (remaining <= 180) {
if (type === "film") {
markFilmWatched(title, true, 0);
} else if (type === "series") {
markEpisodeWatched(title, season, epIndex, true, 0);
}
}
}
/**
* Marks the content as fully watched when the video ends.
*
* Called automatically when video playback reaches the end.
*
* @function
*/
function handleVideoEnded() {
if (!currentVideoContext) return;
const {type, title, season, epIndex} = currentVideoContext;
if (type === "film") {
markFilmWatched(title, true, 0);
} else if (type === "series") {
markEpisodeWatched(title, season, epIndex, true, 0);
}
}
/**
* Saves the current playback position when the video is paused.
*
* Updates watch progress in localStorage without marking as fully watched.
*
* @function
*/
function handleVideoPause() {
if (!currentVideoContext) return;
const {type, title, season, epIndex} = currentVideoContext;
const current = videoPlayer.currentTime;
if (type === "film") {
markFilmWatched(title, false, current);
} else if (type === "series") {
markEpisodeWatched(title, season, epIndex, false, current);
}
}
/**
* Closes all open modals and resets video playback state.
*
* Stops the video, clears playback context, and re-enables page scrolling.
*
* @function
*/
function closeModals() {
const wasVideoModalActive = videoModal.classList.contains("active");
modal.classList.remove("active");
videoModal.classList.remove("active");
document.body.style.overflow = "auto";
if (videoPlayer) {
videoPlayer.pause();
videoPlayer.src = "";
}
currentVideoContext = null;
if (wasVideoModalActive && lastOpenedContent) {
if (lastOpenedContent.type === "film") {
showFilmModal(getFilmByTitle(lastOpenedContent.title));
} else if (lastOpenedContent.type === "series") {
showSeriesModal(getSeriesByTitle(lastOpenedContent.title));
}
}
}