Skip to content

Commit

Permalink
feat(webpage): add episode selection button/popover (#22)
Browse files Browse the repository at this point in the history
* feat(webpage): add episode selection buttons

* feat(webpage,backend): episode selection backend

* feat(webpage): add functional popover container for episode Selection

* style(episodeSelectPopover): improve episode popover selection style and functionalities

* style(popover): mobile layout and open in current season

* feat(popover): add episode and season details

* docs: update README for episode selection
  • Loading branch information
TomasTNunes authored Feb 18, 2025
1 parent 2600f04 commit 8de12c8
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 43 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@ Here are some handy tips and tricks to enhance your experience with the extensio
### 3. Choose Your Default Streaming Server
- Customize your experience by selecting your preferred default server in the extension popup. This ensures your streaming webpage always opens with your chosen server.

### 4. Quick Navigation Back to TMDB
### 4. Streaming Webpage Features
- Clicking on the movie/tv show title on the streaming webpage will redirect you to the corresponding TMDB page, making it easier to switch between these two.
- "Next Episode" button will be available for TV shows, enabling users to continue watching the next episode in the series without interruption.
- Clicking on the "Episode Selection" button will open a user-friendly popover, allowing viewers to choose their desired season and episode.

### 5. Android: App-Like Experience
- For a more seamless, app-like experience on Android, add the TMDB homepage to your device’s home screen. This allows quick access without opening a browser.
Expand Down Expand Up @@ -151,6 +153,8 @@ If you have any questions, encounter issues, or have suggestions for improvement

![Streaming Servers](assets/screenshots/player_show.png)

![Streaming Servers](assets/screenshots/episodeSelection.png)

### Popup:

![Popup](assets/screenshots/popup.png)
Binary file added assets/screenshots/episodeSelection.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 40 additions & 9 deletions webpage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<link rel="icon" href="./icons/icon128.png" sizes="128x128">

<link href="https://fonts.googleapis.com/css2?family=Ubuntu&family=El+Messiri&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <!-- Font Awesome for GitHub icon -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="styles.css">
</head>
<body>
Expand All @@ -19,18 +19,49 @@
<header class="player-header">
<div class="header-buttons">
<a href="https://github.com/TomasTNunes/TMDB-Player/tree/master?tab=readme-ov-file#tmdb-player" target="_blank" class="git-button" title="GitHub">
<i class="fab fa-github"></i> <!-- GitHub icon -->
<i class="fab fa-github"></i>
</a>
<a href="https://github.com/TomasTNunes/TMDB-Player/issues" target="_blank" class="bug-button" title="Report Bug">🐛</a>
</div>
<h2 class="show-title" id="title"></h2>
<button class="nextep-button" title="Next Episode" id="nextep-button">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M22 3H20V21H22V3ZM4.28615 3.61729C3.28674 3.00228 2 3.7213 2 4.89478V19.1052C2 20.2787 3.28674 20.9977 4.28615 20.3827L15.8321 13.2775C16.7839 12.6918 16.7839 11.3082 15.8321 10.7225L4.28615 3.61729ZM4 18.2104V5.78956L14.092 12L4 18.2104Z"
fill="white"/>
</svg>
</button>
<div class="header-buttons">
<div class="popover-container">
<button class="epselect-button" title="Episode Selection" id="epselect-button">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" role="img" viewBox="0 0 24 24" width="24" height="24" data-icon="EpisodesStandard" aria-hidden="true">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8 5H22V13H24V5C24 3.89543 23.1046 3 22 3H8V5ZM18 9H4V7H18C19.1046 7 20 7.89543 20 9V17H18V9ZM0 13C0 11.8954 0.895431 11 2 11H14C15.1046 11 16 11.8954 16 13V19C16 20.1046 15.1046 21 14 21H2C0.895431 21 0 20.1046 0 19V13ZM14 19V13H2V19H14Z"
fill="white">
</path>
</svg>
</button>
<div class="popover-content">
<div class="popover-header">
<div class="popover-back-button">
<svg stroke="#fff" fill="#fff" stroke-width="0" viewBox="0 0 16 16" height="28px" width="28px" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" style="filter: drop-shadow(rgba(0, 0, 0, 0.4) 1px 1px 1px);">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"></path>
</svg>
</div>
<div class="popover-header-title"></div>
<div class="popover-close-button" onclick="popoverContainer.classList.remove('active')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="28" height="28" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
</div>
<div class="popover-list-container">
<ul class="seasons-list"></ul>
<ul class="episodes-list" style="display: none;"></ul>
</div>
</div>
</div>
<button class="nextep-button" title="Next Episode" id="nextep-button">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M22 3H20V21H22V3ZM4.28615 3.61729C3.28674 3.00228 2 3.7213 2 4.89478V19.1052C2 20.2787 3.28674 20.9977 4.28615 20.3827L15.8321 13.2775C16.7839 12.6918 16.7839 11.3082 15.8321 10.7225L4.28615 3.61729ZM4 18.2104V5.78956L14.092 12L4 18.2104Z"
fill="white"/>
</svg>
</button>
</div>
</header>
<main class="player-content">
<iframe id="videoFrame" allowfullscreen></iframe>
Expand Down
188 changes: 163 additions & 25 deletions webpage/script.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
// Define the elements
const iframe = document.getElementById('videoFrame');
const title = document.getElementById('title');
const server_buttons = document.querySelectorAll('.server-grid button');
const nextEpButton = document.getElementById('nextep-button');
const epSelectButton = document.querySelector('.epselect-button');
const popoverContainer = document.querySelector('.popover-container');
const popoverContent = document.querySelector('.popover-content');
const seasonsList = document.querySelector('.seasons-list');
const episodesList = document.querySelector('.episodes-list');
const popoverTitle = document.querySelector('.popover-header-title');
const popoverBackButton = document.querySelector('.popover-back-button');
const popoverCloseButton = document.querySelector('.popover-close-button');
const popoverListContainer = document.querySelector('.popover-list-container');

// Utility Functions
function getURLParams() {
const params = new URLSearchParams(window.location.search);
const type = params.get('type');
Expand Down Expand Up @@ -27,11 +43,8 @@ function getURLParams() {
}

function getSelectedServerButtonId() {
// Get all buttons in the server grid
const buttons = document.querySelectorAll('.server-grid button');

// Loop through the buttons to find the one with the 'selected' class
for (const button of buttons) {
for (const button of server_buttons) {
if (button.classList.contains('selected')) {
const id = button.id.replace('server', '');
return parseInt(id, 10); // Convert the extracted string to a number
Expand All @@ -41,15 +54,10 @@ function getSelectedServerButtonId() {
return null; // Return null if no button is selected
}

function redirectTowebsite() {
window.location.href = "https://github.com/TomasTNunes/TMDB-Player?tab=readme-ov-file#tmdb-player";
}

function changeServer(serverNumber) {
const params = getURLParams();
if (!params) return;

const iframe = document.getElementById('videoFrame');
iframe.src = '';

let src = '';
Expand All @@ -76,8 +84,7 @@ function changeServer(serverNumber) {
iframe.src = src;

// Highlight the selected server button
const buttons = document.querySelectorAll('.server-grid button');
buttons.forEach(button => button.classList.remove('selected'));
server_buttons.forEach(button => button.classList.remove('selected'));
document.getElementById(`server${serverNumber}`).classList.add('selected');
}

Expand Down Expand Up @@ -106,10 +113,15 @@ async function fetchTMDBData(params) {
throw new Error('Network response was not ok');
}
const data = await response.json();
result['title'] = data.name;
result.title = data.name;
const seasons = data.seasons;
result.seasons = [];
for (const season of seasons) {
result[season.season_number] = season.episode_count;
// Exclude Season 0 (Specials) from the list
if (season.season_number !== 0) {
result.seasons.push(season.season_number);
}
}
} else {
throw new Error('Invalid type specified');
Expand All @@ -133,16 +145,116 @@ function getNextEp(currentSeason, currentEpisode, tmdbData) {
return [null, null];
}

// Fetch TV show episodes data from TMDB API
async function fetchEpSelectionData(params, tmdbData) {
const result = {};
let url;
const headers = {
'Authorization': `Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIwYTk1NzRmZDcxMjRkNmI5ZTUyNjA4ZWEzNWQ2NzdiNCIsIm5iZiI6MTczNzU5MDQ2NC4zMjUsInN1YiI6IjY3OTE4NmMwZThiNjdmZjgzM2ZhNjM4OCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.kWqK74FSN41PZO7_ENZelydTtX0u2g6dCkAW0vFs4jU`,
'accept': 'application/json'
};

for (const season of tmdbData.seasons) {
url = `https://api.themoviedb.org/3/tv/${params.id}/season/${season}?language=en-US`;
const response = await fetch(url, { method: 'GET', headers: headers });
if (!response.ok) {
throw new Error('Network response was not ok');
}
const seasonData = await response.json();

result[season] = {};
result[season].name = seasonData.name;
result[season].air_date = seasonData.air_date;
result[season].poster_path = seasonData.poster_path;
result[season].episodes = [];

for (const ep of seasonData.episodes) {
const episode = {};
episode.name = ep.name;
episode.episode_number = ep.episode_number;
episode.season_number = ep.season_number;
episode.air_date = ep.air_date;
episode.runtime = ep.runtime;
episode.still_path = ep.still_path;
result[season].episodes.push(episode);
}
}
return result;
}

// Episode Selection Popover show: seasons list
function showSeasons(tvShowTitle) {
seasonsList.style.display = 'block';
episodesList.style.display = 'none';
popoverBackButton.style.display = 'none';
popoverTitle.innerText = tvShowTitle;
popoverListContainer.scrollTop = 0;
}

// Episode Selection Popover show: episodes list
function showEpisodes(seasonName) {
seasonsList.style.display = 'none';
episodesList.style.display = 'block';
popoverBackButton.style.display = 'block';
popoverTitle.innerText = seasonName;
popoverListContainer.scrollTop = 0;
}

// Load Popover container with seasons and episodes
async function loadPopoverSelectEpisode(params, tmdbData) {
// Get episode data
const epSelectionData = await fetchEpSelectionData(params, tmdbData);

// Populate seasons list
seasonsList.innerHTML = tmdbData.seasons.map(season => `
<li data-season="${season}">
<div class="season-name">${epSelectionData[season].name}</div>
<div class="season-details">${epSelectionData[season].air_date ? epSelectionData[season].air_date : ""}</div>
</li>
`).join('');

// Handle season click
seasonsList.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) {
const season = li.getAttribute('data-season');
const episodes = epSelectionData[season].episodes;
episodesList.innerHTML = episodes.map(ep => `
<li data-season="${season}" data-episode="${ep.episode_number}">
<div class="episode-name">E${ep.episode_number} - ${ep.name}</div>
<div class="episode-details">${ep.air_date ? ep.air_date : ""}&nbsp;&nbsp;&nbsp;${ep.runtime ? `(${ep.runtime}m)` : ""}</div>
</li>
`).join('');
showEpisodes(epSelectionData[season].name);
}
});

// Handle episode click
episodesList.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) {
const season = li.getAttribute('data-season');
const episode = li.getAttribute('data-episode');
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('s', season);
currentUrl.searchParams.set('e', episode);
currentUrl.searchParams.set('server', getSelectedServerButtonId());
window.location.href = currentUrl.toString();
}
});
showSeasons(tmdbData.title);
}

// Initialize popover data
window.onload = async () => {
const params = getURLParams();
if (!params) {
redirectTowebsite();
window.location.href = "https://github.com/TomasTNunes/TMDB-Player?tab=readme-ov-file#tmdb-player";
return;
}

try {
const tmdbData = await fetchTMDBData(params);
const title = document.getElementById('title');

title.addEventListener('click', () => {
window.location.href = `https://www.themoviedb.org/${params.type}/${params.id}`;
Expand All @@ -153,13 +265,13 @@ window.onload = async () => {
} else {
title.innerText = `${tmdbData.title} S${params.season} E${params.episode}`;

// Next Episode
const [nextEpS, nextEpE] = getNextEp(params.season, params.episode, tmdbData);
if (nextEpS !== null) {
const nextEpButton = document.getElementById('nextep-button');
nextEpButton.title = `Next Episode: S${nextEpS} E${nextEpE}`;
nextEpButton.style.display = 'flex';
nextEpButton.style.display = 'flex';
nextEpButton.style.cursor = 'pointer';
nextEpButton.style.opacity = 1;
nextEpButton.style.visibility = 'visible';
nextEpButton.disabled = false;
nextEpButton.addEventListener('click', () => {
const currentUrl = new URL(window.location.href);
Expand All @@ -168,20 +280,46 @@ window.onload = async () => {
currentUrl.searchParams.set('server', getSelectedServerButtonId());
window.location.href = currentUrl.toString();
});
}
else {
const nextEpButton = document.getElementById('nextep-button');
} else {
nextEpButton.title = `No Next Episode`;
nextEpButton.style.display = 'flex'; // Ensure the button is visible
nextEpButton.disabled = true; // Disable the button
nextEpButton.style.display = 'flex';
nextEpButton.style.visibility = 'visible';
nextEpButton.disabled = true;
}
}

// Episode Selection
epSelectButton.style.display = 'flex';
epSelectButton.style.cursor = 'pointer';
epSelectButton.style.visibility = 'visible';
epSelectButton.disabled = false;
// Open popover in current season when clicking the button
epSelectButton.addEventListener('click', (e) => {
e.stopPropagation();
const currentSeasonLi = seasonsList.querySelector(`li[data-season="${params.season}"]`);
if (currentSeasonLi) {
currentSeasonLi.click();
}
popoverContainer.classList.toggle('active');
});
// Close popover when clicking outside
document.addEventListener('click', (e) => {
if (!popoverContainer.contains(e.target)) {
popoverContainer.classList.remove('active');
showSeasons(tmdbData.title);
}
});
// Show seasons list when click Back Button
popoverBackButton.addEventListener('click', (e) => {
showSeasons(tmdbData.title);
});
loadPopoverSelectEpisode(params, tmdbData); // dont await to not block the page load

}
} catch (error) {
console.error('Error loading data:', error);
// Optionally, display an error message to the user
document.getElementById('title').innerText = 'Title';
title.innerText = 'Title';
}

if (params.server) {
changeServer(parseInt(params.server));
} else {
Expand Down
Loading

0 comments on commit 8de12c8

Please sign in to comment.