feat: pause overlay for desktop player

This commit is contained in:
tapframe 2026-06-09 21:16:31 +05:30
parent e31c39b1a6
commit d3e0777cb5
6 changed files with 269 additions and 2 deletions

View file

@ -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",

View file

@ -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),

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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);