push
This commit is contained in:
+268
-11
@@ -24,8 +24,21 @@
|
||||
const sourceLabel = document.getElementById('sourceLabel');
|
||||
const resumeNote = document.getElementById('resumeNote');
|
||||
const sourcesPanel = document.getElementById('sourcesPanel');
|
||||
const changeSourceBtn = document.getElementById('changeSourceBtn');
|
||||
const episodeSection = document.getElementById('episodes');
|
||||
|
||||
// custom player controls
|
||||
const playerWrap = document.getElementById('playerWrap');
|
||||
const bigPlay = document.getElementById('bigPlay');
|
||||
const pcSeek = document.getElementById('pcSeek');
|
||||
const pcPlay = document.getElementById('pcPlay');
|
||||
const pcMute = document.getElementById('pcMute');
|
||||
const pcVol = document.getElementById('pcVol');
|
||||
const pcTime = document.getElementById('pcTime');
|
||||
const pcCc = document.getElementById('pcCc');
|
||||
const pcQuality = document.getElementById('pcQuality');
|
||||
const pcGear = document.getElementById('pcGear');
|
||||
const pcPip = document.getElementById('pcPip');
|
||||
const pcFull = document.getElementById('pcFull');
|
||||
const seasonSelect = document.getElementById('seasonSelect');
|
||||
const episodeGrid = document.getElementById('episodeGrid');
|
||||
const prevBtn = document.getElementById('prevEpisode');
|
||||
@@ -39,12 +52,14 @@
|
||||
sourceIndex: -1,
|
||||
season: null,
|
||||
episode: null,
|
||||
loadedKey: null,
|
||||
};
|
||||
|
||||
/* ---- status overlay ------------------------------------------------------ */
|
||||
|
||||
function showStatus(msg, isError) {
|
||||
statusEl.hidden = false;
|
||||
playerWrap.classList.remove('player-wrap--controls', 'player-wrap--paused');
|
||||
statusEl.innerHTML = isError
|
||||
? `<h3>Playback problem</h3><p>${SR.util.escapeHtml(msg)}</p>`
|
||||
: `<div class="spinner"></div><p>${SR.util.escapeHtml(msg)}</p>`;
|
||||
@@ -52,6 +67,7 @@
|
||||
|
||||
function hideStatus() {
|
||||
statusEl.hidden = true;
|
||||
syncPaused();
|
||||
}
|
||||
|
||||
/* ---- player --------------------------------------------------------------- */
|
||||
@@ -68,6 +84,229 @@
|
||||
},
|
||||
});
|
||||
|
||||
/* ---- custom player controls -------------------------------------------------- */
|
||||
|
||||
const ICON_PLAY =
|
||||
'<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"></path></svg>';
|
||||
const ICON_PAUSE =
|
||||
'<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 4h4v16H6zm8 0h4v16h-4z"></path></svg>';
|
||||
const ICON_VOL =
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path><path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path></svg>';
|
||||
const ICON_VOL_X =
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line></svg>';
|
||||
|
||||
let hideTimer = null;
|
||||
let scrubbing = false;
|
||||
|
||||
function paintProgress(el, ratio) {
|
||||
const pct = Math.max(0, Math.min(1, ratio)) * 100;
|
||||
el.style.setProperty('--pc-progress', pct + '%');
|
||||
}
|
||||
|
||||
function timeLabel() {
|
||||
const d = video.duration;
|
||||
const ds = d && isFinite(d) ? SR.util.formatSeconds(d) : '0:00';
|
||||
return `${SR.util.formatSeconds(video.currentTime)} / ${ds}`;
|
||||
}
|
||||
|
||||
function syncPlayIcon() {
|
||||
const playing = !video.paused && !video.ended;
|
||||
pcPlay.innerHTML = playing ? ICON_PAUSE : ICON_PLAY;
|
||||
pcPlay.setAttribute('aria-label', playing ? 'Pause' : 'Play');
|
||||
}
|
||||
|
||||
function syncVolIcon() {
|
||||
const muted = video.muted || video.volume === 0;
|
||||
pcMute.innerHTML = muted ? ICON_VOL_X : ICON_VOL;
|
||||
pcMute.setAttribute('aria-label', muted ? 'Unmute' : 'Mute');
|
||||
pcMute.classList.toggle('is-on', muted);
|
||||
pcVol.value = String(video.muted ? 0 : video.volume);
|
||||
paintProgress(pcVol, video.muted ? 0 : video.volume);
|
||||
}
|
||||
|
||||
function syncTimeUI() {
|
||||
if (scrubbing) return;
|
||||
const d = video.duration;
|
||||
if (d && isFinite(d) && d > 0) {
|
||||
pcSeek.value = String(Math.round((video.currentTime / d) * 1000));
|
||||
paintProgress(pcSeek, video.currentTime / d);
|
||||
}
|
||||
pcTime.textContent = timeLabel();
|
||||
}
|
||||
|
||||
function syncPaused() {
|
||||
playerWrap.classList.toggle('player-wrap--paused', video.paused);
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
if (!video.src && !video.currentSrc) return;
|
||||
if (video.paused || video.ended) {
|
||||
video.play().catch(() => {});
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
|
||||
function showControls() {
|
||||
playerWrap.classList.add('player-wrap--controls');
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(() => {
|
||||
if (!video.paused && !video.ended && !scrubbing) {
|
||||
playerWrap.classList.remove('player-wrap--controls');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
const el = playerWrap;
|
||||
const doc = document;
|
||||
if (doc.fullscreenElement || doc.webkitFullscreenElement) {
|
||||
(doc.exitFullscreen || doc.webkitExitFullscreen).call(doc);
|
||||
} else {
|
||||
(el.requestFullscreen || el.webkitRequestFullscreen).call(el);
|
||||
}
|
||||
}
|
||||
|
||||
function togglePip() {
|
||||
if (!document.pictureInPictureEnabled) {
|
||||
SR.app.toast('Picture-in-picture is not supported in this browser', true);
|
||||
return;
|
||||
}
|
||||
if (document.pictureInPictureElement) {
|
||||
document.exitPictureInPicture().catch(() => {});
|
||||
} else {
|
||||
video.requestPictureInPicture().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCc() {
|
||||
const tracks = video.textTracks;
|
||||
let target = null;
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (tracks[i].kind === 'subtitles') {
|
||||
target = tracks[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
SR.app.toast('This source has no subtitles', true);
|
||||
return;
|
||||
}
|
||||
target.mode = target.mode === 'showing' ? 'hidden' : 'showing';
|
||||
pcCc.classList.toggle('is-on', target.mode === 'showing');
|
||||
pcCc.setAttribute('aria-pressed', String(target.mode === 'showing'));
|
||||
}
|
||||
|
||||
// video events
|
||||
video.addEventListener('click', togglePlay);
|
||||
video.addEventListener('dblclick', toggleFullscreen);
|
||||
video.addEventListener('play', () => {
|
||||
syncPlayIcon();
|
||||
syncPaused();
|
||||
showControls();
|
||||
});
|
||||
video.addEventListener('pause', () => {
|
||||
syncPlayIcon();
|
||||
syncPaused();
|
||||
showControls();
|
||||
});
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
syncPlayIcon();
|
||||
syncPaused();
|
||||
syncTimeUI();
|
||||
});
|
||||
video.addEventListener('timeupdate', syncTimeUI);
|
||||
video.addEventListener('volumechange', syncVolIcon);
|
||||
|
||||
// buttons
|
||||
bigPlay.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
togglePlay();
|
||||
});
|
||||
pcPlay.addEventListener('click', togglePlay);
|
||||
pcMute.addEventListener('click', () => {
|
||||
video.muted = !video.muted;
|
||||
});
|
||||
pcCc.addEventListener('click', toggleCc);
|
||||
pcGear.addEventListener('click', () => {
|
||||
sourcesPanel.hidden = !sourcesPanel.hidden;
|
||||
pcGear.classList.toggle('is-on', !sourcesPanel.hidden);
|
||||
});
|
||||
pcPip.addEventListener('click', togglePip);
|
||||
pcFull.addEventListener('click', toggleFullscreen);
|
||||
|
||||
// seek + volume scrubbing
|
||||
pcSeek.addEventListener('pointerdown', () => {
|
||||
scrubbing = true;
|
||||
showControls();
|
||||
});
|
||||
pcSeek.addEventListener('input', () => {
|
||||
const d = video.duration;
|
||||
if (d && isFinite(d) && d > 0) {
|
||||
paintProgress(pcSeek, pcSeek.value / 1000);
|
||||
pcTime.textContent = `${SR.util.formatSeconds((pcSeek.value / 1000) * d)} / ${SR.util.formatSeconds(d)}`;
|
||||
}
|
||||
});
|
||||
pcSeek.addEventListener('change', () => {
|
||||
const d = video.duration;
|
||||
if (d && isFinite(d) && d > 0) {
|
||||
video.currentTime = (pcSeek.value / 1000) * d;
|
||||
}
|
||||
scrubbing = false;
|
||||
syncTimeUI();
|
||||
});
|
||||
pcVol.addEventListener('input', () => {
|
||||
video.volume = Number(pcVol.value);
|
||||
video.muted = video.volume === 0;
|
||||
});
|
||||
|
||||
// reveal controls on hover/interaction
|
||||
playerWrap.addEventListener('pointermove', showControls);
|
||||
playerWrap.addEventListener('pointerdown', showControls);
|
||||
|
||||
// keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const tag = (e.target.tagName || '').toLowerCase();
|
||||
if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
|
||||
const d = video.duration;
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'k':
|
||||
e.preventDefault();
|
||||
togglePlay();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (d && isFinite(d)) video.currentTime = Math.min(d, video.currentTime + 10);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (d && isFinite(d)) video.currentTime = Math.max(0, video.currentTime - 10);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
video.volume = Math.min(1, video.volume + 0.05);
|
||||
video.muted = false;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
video.volume = Math.max(0, video.volume - 0.05);
|
||||
break;
|
||||
case 'm':
|
||||
video.muted = !video.muted;
|
||||
break;
|
||||
case 'f':
|
||||
toggleFullscreen();
|
||||
break;
|
||||
case 'c':
|
||||
toggleCc();
|
||||
break;
|
||||
}
|
||||
if (video.src || video.currentSrc) showControls();
|
||||
});
|
||||
|
||||
// initial icon state
|
||||
syncPlayIcon();
|
||||
syncVolIcon();
|
||||
|
||||
/* ---- hero ------------------------------------------------------------------ */
|
||||
|
||||
function renderHero(d) {
|
||||
@@ -119,15 +358,29 @@
|
||||
return SR.storage.keyFor({ type, tmdbId: Number(id), season: state.season, episode: state.episode });
|
||||
}
|
||||
|
||||
let providerNames = null; // cached per page load; null → ask server again
|
||||
|
||||
async function getProviderNames() {
|
||||
if (providerNames) return providerNames;
|
||||
try {
|
||||
const data = await SR.api.providers();
|
||||
providerNames = SR.config.enabledProviders(data.all || [], data.enabled || []);
|
||||
} catch (e) {
|
||||
providerNames = null; // unreachable → let /streams use server defaults
|
||||
}
|
||||
return providerNames;
|
||||
}
|
||||
|
||||
async function loadStreams() {
|
||||
const query = { tmdb: id, type };
|
||||
const query = { tmdb: id, type, providers: await getProviderNames() };
|
||||
if (type === 'tv') {
|
||||
if (state.season == null || state.episode == null) return;
|
||||
query.season = state.season;
|
||||
query.episode = state.episode;
|
||||
}
|
||||
sourcesPanel.hidden = true;
|
||||
changeSourceBtn.textContent = 'Change source';
|
||||
pcGear.classList.remove('is-on');
|
||||
pcQuality.textContent = '';
|
||||
showStatus('Finding streams…');
|
||||
sourceLabel.textContent = '';
|
||||
resumeNote.textContent = '';
|
||||
@@ -135,7 +388,7 @@
|
||||
const data = await SR.api.streams(query);
|
||||
state.streams = data.streams || [];
|
||||
renderSources();
|
||||
changeSourceBtn.hidden = state.streams.length === 0;
|
||||
pcGear.hidden = state.streams.length === 0;
|
||||
if (state.streams.length) playSource(0);
|
||||
else showStatus('No streams found for this title.', true);
|
||||
} catch (e) {
|
||||
@@ -166,18 +419,22 @@
|
||||
function playSource(i) {
|
||||
const s = state.streams[i];
|
||||
if (!s) return;
|
||||
const key = keyFor();
|
||||
// Same episode as the one currently loaded → source switch: carry the
|
||||
// playhead over (resumeAt: null). A different episode → fresh resume:
|
||||
// pick up its own saved record, or start from the top.
|
||||
const resumeAt = state.loadedKey === key ? null : (() => {
|
||||
const rec = SR.storage.get(key);
|
||||
return rec && rec.t > 10 ? rec.t : 0;
|
||||
})();
|
||||
state.sourceIndex = i;
|
||||
state.loadedKey = key;
|
||||
renderSources();
|
||||
sourceLabel.textContent = `${s.name} — ${[s.quality, s.provider].filter(Boolean).join(', ')}`;
|
||||
const rec = SR.storage.get(keyFor());
|
||||
player.load(s, { resumeAt: rec && rec.t > 10 ? rec.t : null });
|
||||
pcQuality.textContent = s.quality || '';
|
||||
player.load(s, { resumeAt });
|
||||
}
|
||||
|
||||
changeSourceBtn.addEventListener('click', () => {
|
||||
sourcesPanel.hidden = !sourcesPanel;
|
||||
changeSourceBtn.textContent = sourcesPanel.hidden ? 'Change source' : 'Hide sources';
|
||||
});
|
||||
|
||||
/* ---- resume persistence ---------------------------------------------------------- */
|
||||
|
||||
let lastPersist = 0;
|
||||
|
||||
Reference in New Issue
Block a user