mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
autoclose 10s after video over
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-ByArKJJA.js"></script>
|
<script type="module" crossorigin src="/assets/index-C85Tgudp.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BcnYeuah.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BcnYeuah.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
// Replay Ready Popup
|
// Replay Ready Popup
|
||||||
// Purpose: Presents the latest Discord-hosted replay video to web users as soon as upload completes.
|
// Purpose: Presents the latest Discord-hosted replay video to web users as soon as upload completes.
|
||||||
// Scope: Owns the ephemeral modal shell, immediate video loading, and click-outside close behavior.
|
// Scope: Owns the ephemeral modal shell, immediate video loading, and click-outside close behavior.
|
||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import CardFrame from '../CardFrame/index.jsx';
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
|
const CLOSE_AFTER_VIDEO_END_MS = 10000;
|
||||||
|
const CLOSE_IF_VIDEO_NEVER_PLAYS_MS = 20000;
|
||||||
|
|
||||||
function normalizeUrl(value) {
|
function normalizeUrl(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
return text || null;
|
return text || null;
|
||||||
@@ -16,7 +20,66 @@ function formatBytes(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' }) {
|
export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' }) {
|
||||||
|
const fallbackCloseTimerRef = useRef(null);
|
||||||
|
const endedCloseTimerRef = useRef(null);
|
||||||
|
const onCloseRef = useRef(onClose);
|
||||||
const videoUrl = normalizeUrl(replay?.url);
|
const videoUrl = normalizeUrl(replay?.url);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onCloseRef.current = onClose;
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const clearCloseTimers = useCallback(() => {
|
||||||
|
if (fallbackCloseTimerRef.current) {
|
||||||
|
clearTimeout(fallbackCloseTimerRef.current);
|
||||||
|
fallbackCloseTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (endedCloseTimerRef.current) {
|
||||||
|
clearTimeout(endedCloseTimerRef.current);
|
||||||
|
endedCloseTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closePopup = useCallback(() => {
|
||||||
|
// Timers are always cleared before closing so a stale timeout from a previous
|
||||||
|
// replay cannot close the next replay popup after React reuses this component.
|
||||||
|
clearCloseTimers();
|
||||||
|
onCloseRef.current?.();
|
||||||
|
}, [clearCloseTimers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearCloseTimers();
|
||||||
|
if (!videoUrl) return undefined;
|
||||||
|
|
||||||
|
// A Discord media URL can fail because the attachment URL expired, the browser
|
||||||
|
// cannot load the remote media, or autoplay never reaches actual playback. This
|
||||||
|
// fallback keeps both popup variants from getting stuck forever in those cases.
|
||||||
|
fallbackCloseTimerRef.current = setTimeout(closePopup, CLOSE_IF_VIDEO_NEVER_PLAYS_MS);
|
||||||
|
return clearCloseTimers;
|
||||||
|
}, [clearCloseTimers, closePopup, videoUrl]);
|
||||||
|
|
||||||
|
const handleVideoPlaying = useCallback(() => {
|
||||||
|
if (!fallbackCloseTimerRef.current) return;
|
||||||
|
// Once playback really starts, the video has proven useful. From this point on,
|
||||||
|
// the popup should stay open until the viewer finishes the replay or closes it.
|
||||||
|
clearTimeout(fallbackCloseTimerRef.current);
|
||||||
|
fallbackCloseTimerRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleVideoEnded = useCallback(() => {
|
||||||
|
if (endedCloseTimerRef.current) {
|
||||||
|
clearTimeout(endedCloseTimerRef.current);
|
||||||
|
}
|
||||||
|
// Leaving the finished replay visible briefly gives people time to notice the
|
||||||
|
// final frame and use the Discord/open-video links before the popup cleans itself up.
|
||||||
|
endedCloseTimerRef.current = setTimeout(closePopup, CLOSE_AFTER_VIDEO_END_MS);
|
||||||
|
}, [closePopup]);
|
||||||
|
|
||||||
|
const videoLifecycleProps = {
|
||||||
|
onPlaying: handleVideoPlaying,
|
||||||
|
onEnded: handleVideoEnded,
|
||||||
|
};
|
||||||
|
|
||||||
if (!videoUrl) return null;
|
if (!videoUrl) return null;
|
||||||
|
|
||||||
const isPanel = variant === 'panel';
|
const isPanel = variant === 'panel';
|
||||||
@@ -57,6 +120,7 @@ export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' })
|
|||||||
autoPlay
|
autoPlay
|
||||||
preload="auto"
|
preload="auto"
|
||||||
playsInline
|
playsInline
|
||||||
|
{...videoLifecycleProps}
|
||||||
// The floating panel is intentionally compact because it appears for users who
|
// The floating panel is intentionally compact because it appears for users who
|
||||||
// did not ask for the replay. It still loads the video immediately, but its
|
// did not ask for the replay. It still loads the video immediately, but its
|
||||||
// bounded height prevents the floating card from covering too much of the UI.
|
// bounded height prevents the floating card from covering too much of the UI.
|
||||||
@@ -84,6 +148,7 @@ export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' })
|
|||||||
autoPlay
|
autoPlay
|
||||||
preload="auto"
|
preload="auto"
|
||||||
playsInline
|
playsInline
|
||||||
|
{...videoLifecycleProps}
|
||||||
className={`${isPanel ? 'h-full min-h-[10rem]' : 'aspect-video max-h-[72vh]'} w-full bg-black`}
|
className={`${isPanel ? 'h-full min-h-[10rem]' : 'aspect-video max-h-[72vh]'} w-full bg-black`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ function normalizeSources(list = []) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PANEL_REPLAY_AUTO_CLOSE_MS = 35000;
|
|
||||||
|
|
||||||
export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHeight = false }) {
|
export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHeight = false }) {
|
||||||
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
|
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
|
||||||
const mode = useSessionSelector((state) => state.session?.mode || null);
|
const mode = useSessionSelector((state) => state.session?.mode || null);
|
||||||
@@ -193,16 +191,6 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
|
|||||||
|
|
||||||
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
|
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showPanelReplay || !latestReplayJobId) return undefined;
|
|
||||||
// Non-requester replay previews are intentionally lightweight. They appear automatically
|
|
||||||
// so web-only users can catch the replay, then clear themselves before blocking the source controls.
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
setDismissedPanelReplayId(latestReplayJobId);
|
|
||||||
}, PANEL_REPLAY_AUTO_CLOSE_MS);
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}, [showPanelReplay, latestReplayJobId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${fillHeight ? 'flex h-full min-h-0 flex-col' : ''}`}>
|
<div className={`relative ${fillHeight ? 'flex h-full min-h-0 flex-col' : ''}`}>
|
||||||
{showPanelReplay ? (
|
{showPanelReplay ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user