mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 00:29:22 +00:00
feat: pause overlay for desktop player
This commit is contained in:
parent
e31c39b1a6
commit
d3e0777cb5
6 changed files with 269 additions and 2 deletions
|
|
@ -52,6 +52,11 @@ data class PlayerControlsState(
|
|||
val episodeText: String = "",
|
||||
val streamTitle: String = "",
|
||||
val providerName: String = "",
|
||||
val pauseOverlayWatchingLabel: String = "You're watching",
|
||||
val pauseOverlayLogo: String? = null,
|
||||
val pauseOverlayEpisodeInfo: String = "",
|
||||
val pauseOverlayEpisodeTitle: String = "",
|
||||
val pauseOverlayDescription: String = "",
|
||||
val resizeModeLabel: String = "Fit",
|
||||
val playbackSpeedLabel: String = "1x",
|
||||
val subtitlesLabel: String = "Subs",
|
||||
|
|
|
|||
|
|
@ -163,6 +163,15 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
|
|||
episodeText = episodeText,
|
||||
streamTitle = activeStreamTitle,
|
||||
providerName = activeProviderName,
|
||||
pauseOverlayWatchingLabel = stringResource(Res.string.compose_player_youre_watching),
|
||||
pauseOverlayLogo = logo,
|
||||
pauseOverlayEpisodeInfo = if (seasonNumber != null && episodeNumber != null) {
|
||||
stringResource(Res.string.compose_player_episode_code_full, seasonNumber, episodeNumber)
|
||||
} else {
|
||||
activeProviderName
|
||||
},
|
||||
pauseOverlayEpisodeTitle = activeEpisodeTitle.orEmpty(),
|
||||
pauseOverlayDescription = (pauseDescription ?: activeStreamSubtitle).orEmpty(),
|
||||
resizeModeLabel = stringResource(resizeMode.labelRes),
|
||||
playbackSpeedLabel = formatPlaybackSpeedLabel(playbackSnapshot.playbackSpeed),
|
||||
subtitlesLabel = stringResource(Res.string.compose_player_subs),
|
||||
|
|
|
|||
|
|
@ -467,6 +467,16 @@ private fun PlayerControlsState.toControlsJson(): String =
|
|||
append(',')
|
||||
appendJsonField("providerName", providerName)
|
||||
append(',')
|
||||
appendJsonField("pauseOverlayWatchingLabel", pauseOverlayWatchingLabel)
|
||||
append(',')
|
||||
appendJsonField("pauseOverlayLogo", pauseOverlayLogo.orEmpty())
|
||||
append(',')
|
||||
appendJsonField("pauseOverlayEpisodeInfo", pauseOverlayEpisodeInfo)
|
||||
append(',')
|
||||
appendJsonField("pauseOverlayEpisodeTitle", pauseOverlayEpisodeTitle)
|
||||
append(',')
|
||||
appendJsonField("pauseOverlayDescription", pauseOverlayDescription)
|
||||
append(',')
|
||||
appendJsonField("resizeModeLabel", resizeModeLabel)
|
||||
append(',')
|
||||
appendJsonField("playbackSpeedLabel", playbackSpeedLabel)
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ button:disabled {
|
|||
.player.opening-active .top-gradient,
|
||||
.player.opening-active .bottom-gradient,
|
||||
.player.opening-active .parental-guide,
|
||||
.player.opening-active .pause-metadata-overlay,
|
||||
.player.opening-active .skip-prompt,
|
||||
.player.opening-active .next-episode-card,
|
||||
.player.opening-active .content {
|
||||
|
|
@ -764,6 +765,163 @@ button:disabled {
|
|||
animation: center-spin 760ms linear infinite;
|
||||
}
|
||||
|
||||
.pause-metadata-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 6;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, .85) 0%,
|
||||
rgba(0, 0, 0, .45) 48%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
.pause-metadata-overlay.visible {
|
||||
opacity: 1;
|
||||
transition-duration: 220ms;
|
||||
}
|
||||
|
||||
.pause-metadata-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding:
|
||||
40px
|
||||
calc(var(--hp) + env(safe-area-inset-right))
|
||||
120px
|
||||
calc(var(--hp) + env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
.pause-kicker {
|
||||
color: #b8b8b8;
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pause-logo {
|
||||
width: min(360px, 62vw);
|
||||
height: 96px;
|
||||
margin-top: 12px;
|
||||
object-fit: contain;
|
||||
object-position: left bottom;
|
||||
}
|
||||
|
||||
.pause-logo[hidden],
|
||||
.pause-title[hidden],
|
||||
.pause-episode-info[hidden],
|
||||
.pause-episode-title[hidden],
|
||||
.pause-description[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pause-title {
|
||||
max-width: 62%;
|
||||
margin-top: 12px;
|
||||
color: #fff;
|
||||
font-size: max(calc(var(--title) * 1.8), 32px);
|
||||
line-height: 1.08;
|
||||
font-weight: 800;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.pause-episode-info {
|
||||
margin-top: 8px;
|
||||
color: #ccc;
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pause-episode-title {
|
||||
max-width: 62%;
|
||||
margin-top: 12px;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
line-height: 1.25;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.pause-description {
|
||||
width: 62%;
|
||||
margin-top: 16px;
|
||||
color: #d6d6d6;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
@media (max-height: 419px) {
|
||||
.pause-metadata-content {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.pause-kicker,
|
||||
.pause-episode-info {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pause-logo {
|
||||
height: 64px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pause-title {
|
||||
max-width: 82%;
|
||||
margin-top: 8px;
|
||||
font-size: max(calc(var(--title) * 1.35), 32px);
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.pause-episode-info {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.pause-episode-title {
|
||||
max-width: 82%;
|
||||
margin-top: 8px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.pause-description {
|
||||
width: 82%;
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 339px) {
|
||||
.pause-metadata-content {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.pause-logo {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.opening-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
|
@ -1844,6 +2002,7 @@ button:focus-visible,
|
|||
.header-actions,
|
||||
.center-controls,
|
||||
.center-status,
|
||||
.pause-metadata-overlay,
|
||||
.skip-prompt,
|
||||
.next-episode-card,
|
||||
.skip-progress-fill,
|
||||
|
|
@ -1852,6 +2011,7 @@ button:focus-visible,
|
|||
.opening-progress-bar,
|
||||
.opening-artwork,
|
||||
.opening-logo,
|
||||
.pause-logo,
|
||||
.next-episode-thumb img,
|
||||
.episode-thumb img,
|
||||
.image-loading::before,
|
||||
|
|
@ -1880,4 +2040,4 @@ svg {
|
|||
display: block;
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,17 @@
|
|||
<span class="center-spinner" aria-hidden="true"></span>
|
||||
</section>
|
||||
|
||||
<section class="pause-metadata-overlay" id="pauseMetadataOverlay" aria-hidden="true">
|
||||
<div class="pause-metadata-content">
|
||||
<div class="pause-kicker" id="pauseWatchingLabel">You're watching</div>
|
||||
<img class="pause-logo" id="pauseLogo" alt="">
|
||||
<div class="pause-title" id="pauseTitle"></div>
|
||||
<div class="pause-episode-info" id="pauseEpisodeInfo"></div>
|
||||
<div class="pause-episode-title" id="pauseEpisodeTitle"></div>
|
||||
<div class="pause-description" id="pauseDescription"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button class="skip-prompt" id="skipPrompt" type="button" aria-label="Skip" aria-hidden="true">
|
||||
<span class="skip-prompt-row">
|
||||
<svg><use href="#icon-skip-next"></use></svg>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@ const seek = document.getElementById("seek");
|
|||
const positionLabel = document.getElementById("position");
|
||||
const durationLabel = document.getElementById("duration");
|
||||
const bufferingStatus = document.getElementById("bufferingStatus");
|
||||
const pauseMetadataOverlay = document.getElementById("pauseMetadataOverlay");
|
||||
const pauseWatchingLabel = document.getElementById("pauseWatchingLabel");
|
||||
const pauseLogo = document.getElementById("pauseLogo");
|
||||
const pauseTitle = document.getElementById("pauseTitle");
|
||||
const pauseEpisodeInfo = document.getElementById("pauseEpisodeInfo");
|
||||
const pauseEpisodeTitle = document.getElementById("pauseEpisodeTitle");
|
||||
const pauseDescription = document.getElementById("pauseDescription");
|
||||
const toggle = document.getElementById("toggle");
|
||||
const toggleIcon = document.getElementById("toggleIcon");
|
||||
const lockIcon = document.getElementById("lockIcon");
|
||||
|
|
@ -138,6 +145,11 @@ let state = {
|
|||
episodeText: "",
|
||||
streamTitle: "",
|
||||
providerName: "",
|
||||
pauseOverlayWatchingLabel: "You're watching",
|
||||
pauseOverlayLogo: "",
|
||||
pauseOverlayEpisodeInfo: "",
|
||||
pauseOverlayEpisodeTitle: "",
|
||||
pauseOverlayDescription: "",
|
||||
resizeModeLabel: "Fit",
|
||||
playbackSpeedLabel: "1x",
|
||||
subtitlesLabel: "Subs",
|
||||
|
|
@ -314,6 +326,9 @@ let skipPromptWasDismissed = false;
|
|||
let skipPromptAutoHidden = false;
|
||||
let skipPromptAutoHideTimer = 0;
|
||||
let skipPromptAutoHideActive = false;
|
||||
let pauseMetadataReady = false;
|
||||
let pauseMetadataTimer = 0;
|
||||
let pauseMetadataEligibilityKey = "";
|
||||
const prefersReducedMotion = window.matchMedia &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const modalTransitionMs = prefersReducedMotion ? 1 : 240;
|
||||
|
|
@ -547,6 +562,62 @@ const setImageSource = (element, source) => {
|
|||
return url;
|
||||
};
|
||||
|
||||
const resetPauseMetadataTimer = () => {
|
||||
window.clearTimeout(pauseMetadataTimer);
|
||||
pauseMetadataTimer = 0;
|
||||
pauseMetadataReady = false;
|
||||
pauseMetadataEligibilityKey = "";
|
||||
};
|
||||
|
||||
const syncPauseMetadataTimer = showOpening => {
|
||||
const durationMs = Math.max(0, Number(state.durationMs) || 0);
|
||||
const eligible = Boolean(!state.isPlaying && !state.isLoading && durationMs > 0 && !showOpening);
|
||||
const key = eligible ? `${Math.round(durationMs)}:${state.title || ""}:${state.pauseOverlayEpisodeInfo || ""}` : "";
|
||||
if (!eligible) {
|
||||
resetPauseMetadataTimer();
|
||||
return;
|
||||
}
|
||||
if (pauseMetadataEligibilityKey === key) return;
|
||||
window.clearTimeout(pauseMetadataTimer);
|
||||
pauseMetadataReady = false;
|
||||
pauseMetadataEligibilityKey = key;
|
||||
pauseMetadataTimer = window.setTimeout(() => {
|
||||
pauseMetadataTimer = 0;
|
||||
pauseMetadataReady = true;
|
||||
renderChrome();
|
||||
}, prefersReducedMotion ? 1 : 5000);
|
||||
};
|
||||
|
||||
const renderPauseMetadataOverlay = showOpening => {
|
||||
syncPauseMetadataTimer(showOpening);
|
||||
|
||||
const logoUrl = setImageSource(pauseLogo, state.pauseOverlayLogo);
|
||||
const titleText = String(state.title || "").trim();
|
||||
const episodeInfo = String(state.pauseOverlayEpisodeInfo || state.providerName || "").trim();
|
||||
const episodeTitleText = String(state.pauseOverlayEpisodeTitle || "").trim();
|
||||
const descriptionText = String(state.pauseOverlayDescription || "").trim();
|
||||
const showOverlay = Boolean(
|
||||
pauseMetadataReady &&
|
||||
!state.controlsVisible &&
|
||||
!state.isLocked &&
|
||||
!activeModal &&
|
||||
!showOpening,
|
||||
);
|
||||
|
||||
pauseWatchingLabel.textContent = state.pauseOverlayWatchingLabel || "You're watching";
|
||||
pauseLogo.hidden = !logoUrl;
|
||||
pauseTitle.textContent = titleText;
|
||||
pauseTitle.hidden = Boolean(logoUrl || !titleText);
|
||||
pauseEpisodeInfo.textContent = episodeInfo;
|
||||
pauseEpisodeInfo.hidden = !episodeInfo;
|
||||
pauseEpisodeTitle.textContent = episodeTitleText;
|
||||
pauseEpisodeTitle.hidden = !episodeTitleText;
|
||||
pauseDescription.textContent = descriptionText;
|
||||
pauseDescription.hidden = !descriptionText;
|
||||
pauseMetadataOverlay.classList.toggle("visible", showOverlay);
|
||||
pauseMetadataOverlay.setAttribute("aria-hidden", showOverlay ? "false" : "true");
|
||||
};
|
||||
|
||||
const normalizedOpeningProgress = () => {
|
||||
const progress = Number(state.openingProgress);
|
||||
return Number.isFinite(progress) ? Math.max(0, Math.min(1, progress)) : null;
|
||||
|
|
@ -1434,6 +1505,7 @@ const renderChrome = () => {
|
|||
root.classList.toggle("chrome-hidden", Boolean(!state.controlsVisible && !(state.isLocked && state.lockedOverlayVisible)));
|
||||
root.classList.toggle("source-visible", Boolean(!isPlaying && !state.isLoading && (state.streamTitle || state.providerName)));
|
||||
const showOpening = renderOpeningOverlay();
|
||||
renderPauseMetadataOverlay(showOpening);
|
||||
syncParentalGuide(showOpening);
|
||||
|
||||
title.textContent = state.title || "";
|
||||
|
|
@ -1902,4 +1974,4 @@ document.addEventListener("keydown", event => {
|
|||
setProgress(0, 0);
|
||||
focusShortcutRoot();
|
||||
render();
|
||||
send("controlsReady", 0);
|
||||
send("controlsReady", 0);
|
||||
|
|
|
|||
Loading…
Reference in a new issue