Skip to content

Commit 922b065

Browse files
authored
Merge pull request #23 from TomasTNunes/dev
feat(webpage): add episode selection button/popover (#22)
2 parents 2b447a3 + 8de12c8 commit 922b065

File tree

5 files changed

+382
-43
lines changed

5 files changed

+382
-43
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@ Here are some handy tips and tricks to enhance your experience with the extensio
103103
### 3. Choose Your Default Streaming Server
104104
- Customize your experience by selecting your preferred default server in the extension popup. This ensures your streaming webpage always opens with your chosen server.
105105

106-
### 4. Quick Navigation Back to TMDB
106+
### 4. Streaming Webpage Features
107107
- 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.
108+
- "Next Episode" button will be available for TV shows, enabling users to continue watching the next episode in the series without interruption.
109+
- Clicking on the "Episode Selection" button will open a user-friendly popover, allowing viewers to choose their desired season and episode.
108110

109111
### 5. Android: App-Like Experience
110112
- 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.
@@ -151,6 +153,8 @@ If you have any questions, encounter issues, or have suggestions for improvement
151153

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

156+
![Streaming Servers](assets/screenshots/episodeSelection.png)
157+
154158
### Popup:
155159

156160
![Popup](assets/screenshots/popup.png)
1.25 MB
Loading

webpage/index.html

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<link rel="icon" href="./icons/icon128.png" sizes="128x128">
1111

1212
<link href="https://fonts.googleapis.com/css2?family=Ubuntu&family=El+Messiri&display=swap" rel="stylesheet">
13-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <!-- Font Awesome for GitHub icon -->
13+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
1414
<link rel="stylesheet" href="styles.css">
1515
</head>
1616
<body>
@@ -19,18 +19,49 @@
1919
<header class="player-header">
2020
<div class="header-buttons">
2121
<a href="https://github.com/TomasTNunes/TMDB-Player/tree/master?tab=readme-ov-file#tmdb-player" target="_blank" class="git-button" title="GitHub">
22-
<i class="fab fa-github"></i> <!-- GitHub icon -->
22+
<i class="fab fa-github"></i>
2323
</a>
2424
<a href="https://github.com/TomasTNunes/TMDB-Player/issues" target="_blank" class="bug-button" title="Report Bug">🐛</a>
2525
</div>
2626
<h2 class="show-title" id="title"></h2>
27-
<button class="nextep-button" title="Next Episode" id="nextep-button">
28-
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
29-
<path fill-rule="evenodd" clip-rule="evenodd"
30-
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"
31-
fill="white"/>
32-
</svg>
33-
</button>
27+
<div class="header-buttons">
28+
<div class="popover-container">
29+
<button class="epselect-button" title="Episode Selection" id="epselect-button">
30+
<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">
31+
<path fill-rule="evenodd" clip-rule="evenodd"
32+
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"
33+
fill="white">
34+
</path>
35+
</svg>
36+
</button>
37+
<div class="popover-content">
38+
<div class="popover-header">
39+
<div class="popover-back-button">
40+
<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);">
41+
<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>
42+
</svg>
43+
</div>
44+
<div class="popover-header-title"></div>
45+
<div class="popover-close-button" onclick="popoverContainer.classList.remove('active')">
46+
<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">
47+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
48+
</svg>
49+
</div>
50+
</div>
51+
<div class="popover-list-container">
52+
<ul class="seasons-list"></ul>
53+
<ul class="episodes-list" style="display: none;"></ul>
54+
</div>
55+
</div>
56+
</div>
57+
<button class="nextep-button" title="Next Episode" id="nextep-button">
58+
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
59+
<path fill-rule="evenodd" clip-rule="evenodd"
60+
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"
61+
fill="white"/>
62+
</svg>
63+
</button>
64+
</div>
3465
</header>
3566
<main class="player-content">
3667
<iframe id="videoFrame" allowfullscreen></iframe>

webpage/script.js

Lines changed: 163 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
// Define the elements
2+
const iframe = document.getElementById('videoFrame');
3+
const title = document.getElementById('title');
4+
const server_buttons = document.querySelectorAll('.server-grid button');
5+
const nextEpButton = document.getElementById('nextep-button');
6+
const epSelectButton = document.querySelector('.epselect-button');
7+
const popoverContainer = document.querySelector('.popover-container');
8+
const popoverContent = document.querySelector('.popover-content');
9+
const seasonsList = document.querySelector('.seasons-list');
10+
const episodesList = document.querySelector('.episodes-list');
11+
const popoverTitle = document.querySelector('.popover-header-title');
12+
const popoverBackButton = document.querySelector('.popover-back-button');
13+
const popoverCloseButton = document.querySelector('.popover-close-button');
14+
const popoverListContainer = document.querySelector('.popover-list-container');
15+
16+
// Utility Functions
117
function getURLParams() {
218
const params = new URLSearchParams(window.location.search);
319
const type = params.get('type');
@@ -27,11 +43,8 @@ function getURLParams() {
2743
}
2844

2945
function getSelectedServerButtonId() {
30-
// Get all buttons in the server grid
31-
const buttons = document.querySelectorAll('.server-grid button');
32-
3346
// Loop through the buttons to find the one with the 'selected' class
34-
for (const button of buttons) {
47+
for (const button of server_buttons) {
3548
if (button.classList.contains('selected')) {
3649
const id = button.id.replace('server', '');
3750
return parseInt(id, 10); // Convert the extracted string to a number
@@ -41,15 +54,10 @@ function getSelectedServerButtonId() {
4154
return null; // Return null if no button is selected
4255
}
4356

44-
function redirectTowebsite() {
45-
window.location.href = "https://github.com/TomasTNunes/TMDB-Player?tab=readme-ov-file#tmdb-player";
46-
}
47-
4857
function changeServer(serverNumber) {
4958
const params = getURLParams();
5059
if (!params) return;
5160

52-
const iframe = document.getElementById('videoFrame');
5361
iframe.src = '';
5462

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

7886
// Highlight the selected server button
79-
const buttons = document.querySelectorAll('.server-grid button');
80-
buttons.forEach(button => button.classList.remove('selected'));
87+
server_buttons.forEach(button => button.classList.remove('selected'));
8188
document.getElementById(`server${serverNumber}`).classList.add('selected');
8289
}
8390

@@ -106,10 +113,15 @@ async function fetchTMDBData(params) {
106113
throw new Error('Network response was not ok');
107114
}
108115
const data = await response.json();
109-
result['title'] = data.name;
116+
result.title = data.name;
110117
const seasons = data.seasons;
118+
result.seasons = [];
111119
for (const season of seasons) {
112120
result[season.season_number] = season.episode_count;
121+
// Exclude Season 0 (Specials) from the list
122+
if (season.season_number !== 0) {
123+
result.seasons.push(season.season_number);
124+
}
113125
}
114126
} else {
115127
throw new Error('Invalid type specified');
@@ -133,16 +145,116 @@ function getNextEp(currentSeason, currentEpisode, tmdbData) {
133145
return [null, null];
134146
}
135147

148+
// Fetch TV show episodes data from TMDB API
149+
async function fetchEpSelectionData(params, tmdbData) {
150+
const result = {};
151+
let url;
152+
const headers = {
153+
'Authorization': `Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIwYTk1NzRmZDcxMjRkNmI5ZTUyNjA4ZWEzNWQ2NzdiNCIsIm5iZiI6MTczNzU5MDQ2NC4zMjUsInN1YiI6IjY3OTE4NmMwZThiNjdmZjgzM2ZhNjM4OCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.kWqK74FSN41PZO7_ENZelydTtX0u2g6dCkAW0vFs4jU`,
154+
'accept': 'application/json'
155+
};
156+
157+
for (const season of tmdbData.seasons) {
158+
url = `https://api.themoviedb.org/3/tv/${params.id}/season/${season}?language=en-US`;
159+
const response = await fetch(url, { method: 'GET', headers: headers });
160+
if (!response.ok) {
161+
throw new Error('Network response was not ok');
162+
}
163+
const seasonData = await response.json();
164+
165+
result[season] = {};
166+
result[season].name = seasonData.name;
167+
result[season].air_date = seasonData.air_date;
168+
result[season].poster_path = seasonData.poster_path;
169+
result[season].episodes = [];
170+
171+
for (const ep of seasonData.episodes) {
172+
const episode = {};
173+
episode.name = ep.name;
174+
episode.episode_number = ep.episode_number;
175+
episode.season_number = ep.season_number;
176+
episode.air_date = ep.air_date;
177+
episode.runtime = ep.runtime;
178+
episode.still_path = ep.still_path;
179+
result[season].episodes.push(episode);
180+
}
181+
}
182+
return result;
183+
}
184+
185+
// Episode Selection Popover show: seasons list
186+
function showSeasons(tvShowTitle) {
187+
seasonsList.style.display = 'block';
188+
episodesList.style.display = 'none';
189+
popoverBackButton.style.display = 'none';
190+
popoverTitle.innerText = tvShowTitle;
191+
popoverListContainer.scrollTop = 0;
192+
}
193+
194+
// Episode Selection Popover show: episodes list
195+
function showEpisodes(seasonName) {
196+
seasonsList.style.display = 'none';
197+
episodesList.style.display = 'block';
198+
popoverBackButton.style.display = 'block';
199+
popoverTitle.innerText = seasonName;
200+
popoverListContainer.scrollTop = 0;
201+
}
202+
203+
// Load Popover container with seasons and episodes
204+
async function loadPopoverSelectEpisode(params, tmdbData) {
205+
// Get episode data
206+
const epSelectionData = await fetchEpSelectionData(params, tmdbData);
207+
208+
// Populate seasons list
209+
seasonsList.innerHTML = tmdbData.seasons.map(season => `
210+
<li data-season="${season}">
211+
<div class="season-name">${epSelectionData[season].name}</div>
212+
<div class="season-details">${epSelectionData[season].air_date ? epSelectionData[season].air_date : ""}</div>
213+
</li>
214+
`).join('');
215+
216+
// Handle season click
217+
seasonsList.addEventListener('click', (e) => {
218+
const li = e.target.closest('li');
219+
if (li) {
220+
const season = li.getAttribute('data-season');
221+
const episodes = epSelectionData[season].episodes;
222+
episodesList.innerHTML = episodes.map(ep => `
223+
<li data-season="${season}" data-episode="${ep.episode_number}">
224+
<div class="episode-name">E${ep.episode_number} - ${ep.name}</div>
225+
<div class="episode-details">${ep.air_date ? ep.air_date : ""}&nbsp;&nbsp;&nbsp;${ep.runtime ? `(${ep.runtime}m)` : ""}</div>
226+
</li>
227+
`).join('');
228+
showEpisodes(epSelectionData[season].name);
229+
}
230+
});
231+
232+
// Handle episode click
233+
episodesList.addEventListener('click', (e) => {
234+
const li = e.target.closest('li');
235+
if (li) {
236+
const season = li.getAttribute('data-season');
237+
const episode = li.getAttribute('data-episode');
238+
const currentUrl = new URL(window.location.href);
239+
currentUrl.searchParams.set('s', season);
240+
currentUrl.searchParams.set('e', episode);
241+
currentUrl.searchParams.set('server', getSelectedServerButtonId());
242+
window.location.href = currentUrl.toString();
243+
}
244+
});
245+
showSeasons(tmdbData.title);
246+
}
247+
248+
// Initialize popover data
136249
window.onload = async () => {
137250
const params = getURLParams();
138251
if (!params) {
139-
redirectTowebsite();
252+
window.location.href = "https://github.com/TomasTNunes/TMDB-Player?tab=readme-ov-file#tmdb-player";
140253
return;
141254
}
142255

143256
try {
144257
const tmdbData = await fetchTMDBData(params);
145-
const title = document.getElementById('title');
146258

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

268+
// Next Episode
156269
const [nextEpS, nextEpE] = getNextEp(params.season, params.episode, tmdbData);
157270
if (nextEpS !== null) {
158-
const nextEpButton = document.getElementById('nextep-button');
159271
nextEpButton.title = `Next Episode: S${nextEpS} E${nextEpE}`;
160-
nextEpButton.style.display = 'flex';
272+
nextEpButton.style.display = 'flex';
161273
nextEpButton.style.cursor = 'pointer';
162-
nextEpButton.style.opacity = 1;
274+
nextEpButton.style.visibility = 'visible';
163275
nextEpButton.disabled = false;
164276
nextEpButton.addEventListener('click', () => {
165277
const currentUrl = new URL(window.location.href);
@@ -168,20 +280,46 @@ window.onload = async () => {
168280
currentUrl.searchParams.set('server', getSelectedServerButtonId());
169281
window.location.href = currentUrl.toString();
170282
});
171-
}
172-
else {
173-
const nextEpButton = document.getElementById('nextep-button');
283+
} else {
174284
nextEpButton.title = `No Next Episode`;
175-
nextEpButton.style.display = 'flex'; // Ensure the button is visible
176-
nextEpButton.disabled = true; // Disable the button
285+
nextEpButton.style.display = 'flex';
286+
nextEpButton.style.visibility = 'visible';
287+
nextEpButton.disabled = true;
177288
}
178-
}
179289

290+
// Episode Selection
291+
epSelectButton.style.display = 'flex';
292+
epSelectButton.style.cursor = 'pointer';
293+
epSelectButton.style.visibility = 'visible';
294+
epSelectButton.disabled = false;
295+
// Open popover in current season when clicking the button
296+
epSelectButton.addEventListener('click', (e) => {
297+
e.stopPropagation();
298+
const currentSeasonLi = seasonsList.querySelector(`li[data-season="${params.season}"]`);
299+
if (currentSeasonLi) {
300+
currentSeasonLi.click();
301+
}
302+
popoverContainer.classList.toggle('active');
303+
});
304+
// Close popover when clicking outside
305+
document.addEventListener('click', (e) => {
306+
if (!popoverContainer.contains(e.target)) {
307+
popoverContainer.classList.remove('active');
308+
showSeasons(tmdbData.title);
309+
}
310+
});
311+
// Show seasons list when click Back Button
312+
popoverBackButton.addEventListener('click', (e) => {
313+
showSeasons(tmdbData.title);
314+
});
315+
loadPopoverSelectEpisode(params, tmdbData); // dont await to not block the page load
316+
317+
}
180318
} catch (error) {
181319
console.error('Error loading data:', error);
182-
// Optionally, display an error message to the user
183-
document.getElementById('title').innerText = 'Title';
320+
title.innerText = 'Title';
184321
}
322+
185323
if (params.server) {
186324
changeServer(parseInt(params.server));
187325
} else {

0 commit comments

Comments
 (0)