@ttt-productions/media-viewer 0.10.14 → 0.10.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react/audio-viewer.d.ts.map +1 -1
- package/dist/react/audio-viewer.js +23 -19
- package/dist/react/audio-viewer.js.map +1 -1
- package/dist/react/image-viewer.d.ts.map +1 -1
- package/dist/react/image-viewer.js +12 -7
- package/dist/react/image-viewer.js.map +1 -1
- package/dist/react/media-viewer.d.ts.map +1 -1
- package/dist/react/media-viewer.js +21 -7
- package/dist/react/media-viewer.js.map +1 -1
- package/dist/react/use-media-recovery.d.ts +7 -1
- package/dist/react/use-media-recovery.d.ts.map +1 -1
- package/dist/react/use-media-recovery.js +111 -78
- package/dist/react/use-media-recovery.js.map +1 -1
- package/dist/react/video-viewer.d.ts.map +1 -1
- package/dist/react/video-viewer.js +25 -20
- package/dist/react/video-viewer.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-viewer.d.ts","sourceRoot":"","sources":["../../src/react/audio-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"audio-viewer.d.ts","sourceRoot":"","sources":["../../src/react/audio-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,qBAwLlD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { useInView } from "react-intersection-observer";
|
|
4
4
|
import { Skeleton } from "@ttt-productions/ui-core/react";
|
|
@@ -6,12 +6,12 @@ import { useMediaPlayback } from "./use-media-playback.js";
|
|
|
6
6
|
import { AudioPlayerChrome } from "./audio-player-chrome.js";
|
|
7
7
|
import { useLoadWatchdog } from "./use-load-watchdog.js";
|
|
8
8
|
export function AudioViewer(props) {
|
|
9
|
-
const { url, className, mediaClassName, autoPlay, loop = false, lazy = true, skeleton: showSkeleton = true, priority = false, preload = "metadata", onLoad, onLoadChange, onError, fallback, loadTimeoutMs, onEnded, onProgressSample, startAtSeconds, endOverlay, playbackControlsRef, visualizerMode, persistKey, extraActions, } = props;
|
|
9
|
+
const { url, className, mediaClassName, autoPlay, loop = false, lazy = true, skeleton: showSkeleton = true, priority = false, preload = "metadata", onLoad, onLoadChange, onError, onVisibilityChange, fallback, loadTimeoutMs, onEnded, onProgressSample, startAtSeconds, endOverlay, playbackControlsRef, visualizerMode, persistKey, extraActions, } = props;
|
|
10
10
|
const [isLoaded, setIsLoaded] = React.useState(false);
|
|
11
11
|
const [hasError, setHasError] = React.useState(false);
|
|
12
|
-
const [hasMetadata, setHasMetadata] = React.useState(false);
|
|
13
12
|
const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);
|
|
14
13
|
const audioRef = React.useRef(null);
|
|
14
|
+
const loadReportedRef = React.useRef(false);
|
|
15
15
|
// Additive playback API — derives from element events (no observer, MEDIA-102).
|
|
16
16
|
const { hasEnded, handlers: playbackHandlers, attachElement } = useMediaPlayback(audioRef, { onEnded, onProgressSample, startAtSeconds, endOverlay }, playbackControlsRef, url);
|
|
17
17
|
// The element attaches through the playback lifecycle so the hook knows the
|
|
@@ -20,11 +20,14 @@ export function AudioViewer(props) {
|
|
|
20
20
|
audioRef.current = el;
|
|
21
21
|
attachElement(el);
|
|
22
22
|
}, [attachElement]);
|
|
23
|
+
// The viewer's one observer (MEDIA-102): lazy-load gating, and — while an owner listens —
|
|
24
|
+
// the visibility it reports, which is why it keeps running after the load starts then.
|
|
23
25
|
const { ref: inViewRef, inView } = useInView({
|
|
24
26
|
triggerOnce: false,
|
|
25
27
|
threshold: 0.01,
|
|
26
|
-
skip: priority || shouldLoad,
|
|
28
|
+
skip: priority || (shouldLoad && !onVisibilityChange),
|
|
27
29
|
rootMargin: "100px",
|
|
30
|
+
onChange: (visible) => onVisibilityChange?.(visible),
|
|
28
31
|
});
|
|
29
32
|
React.useEffect(() => {
|
|
30
33
|
if (inView && !shouldLoad) {
|
|
@@ -47,36 +50,37 @@ export function AudioViewer(props) {
|
|
|
47
50
|
React.useEffect(() => {
|
|
48
51
|
setHasError(false);
|
|
49
52
|
setIsLoaded(false);
|
|
50
|
-
|
|
53
|
+
loadReportedRef.current = false;
|
|
51
54
|
}, [url]);
|
|
52
|
-
|
|
55
|
+
// A preload="metadata" element may not fire loadeddata or canplay until play, so the
|
|
56
|
+
// first of loadedmetadata / loadeddata / canplay makes it usable: the skeleton goes, the
|
|
57
|
+
// player shows, and onLoad fires once for this load.
|
|
58
|
+
const reveal = React.useCallback(() => {
|
|
53
59
|
setIsLoaded(true);
|
|
60
|
+
if (loadReportedRef.current)
|
|
61
|
+
return;
|
|
62
|
+
loadReportedRef.current = true;
|
|
54
63
|
onLoad?.();
|
|
55
64
|
}, [onLoad]);
|
|
56
|
-
const handleCanPlay = React.useCallback(() => {
|
|
57
|
-
setIsLoaded(true);
|
|
58
|
-
}, []);
|
|
59
65
|
const handleError = React.useCallback(() => {
|
|
60
66
|
setHasError(true);
|
|
61
67
|
setIsLoaded(true); // Stop skeleton on error
|
|
62
68
|
onError?.();
|
|
63
69
|
}, [onError]);
|
|
64
|
-
// Watchdog settles on METADATA — preload="metadata" audio may not fire
|
|
65
|
-
// loadeddata/canplay until play, and must not false-positive.
|
|
66
70
|
const handleLoadedMetadata = React.useCallback((e) => {
|
|
67
|
-
|
|
71
|
+
reveal();
|
|
68
72
|
playbackHandlers.onLoadedMetadata(e);
|
|
69
|
-
}, [playbackHandlers]);
|
|
70
|
-
useLoadWatchdog(shouldLoad && !isLoaded && !
|
|
73
|
+
}, [reveal, playbackHandlers]);
|
|
74
|
+
useLoadWatchdog(shouldLoad && !isLoaded && !hasError, loadTimeoutMs, handleError);
|
|
75
|
+
// The error state stays inside the observed wrapper, so the viewer keeps reporting
|
|
76
|
+
// its visibility while its owner recovers it.
|
|
71
77
|
if (hasError) {
|
|
72
|
-
|
|
73
|
-
return _jsx(_Fragment, { children: fallback });
|
|
74
|
-
return (_jsx("div", { className: className, children: _jsx("div", { className: "mv-audio-error", children: _jsx("p", { children: "Failed to load audio" }) }) }));
|
|
78
|
+
return (_jsx("div", { ref: inViewRef, className: className, style: fallback ? { position: "relative", width: "100%", height: "100%" } : undefined, children: fallback ?? (_jsx("div", { className: "mv-audio-error", children: _jsx("p", { children: "Failed to load audio" }) })) }));
|
|
75
79
|
}
|
|
76
|
-
return (_jsx("div", { ref: inViewRef, className: className, style: { position: "relative", width: "100%", outline: "none" }, children: !shouldLoad ? (showSkeleton ? _jsx(Skeleton, { style: { height: "3.5rem", width: "100%" } }) : null) : (_jsxs(_Fragment, { children: [showSkeleton && !isLoaded && (_jsx(Skeleton, { style: { position: "absolute", inset: 0, height: "3.5rem", width: "100%" } })), _jsx("audio", { ref: setAudioRef, src: url, className: mediaClassName, controls: false, autoPlay: autoPlay, loop: loop, preload: preload, onLoadedData:
|
|
80
|
+
return (_jsx("div", { ref: inViewRef, className: className, style: { position: "relative", width: "100%", outline: "none" }, children: !shouldLoad ? (showSkeleton ? _jsx(Skeleton, { style: { height: "3.5rem", width: "100%" } }) : null) : (_jsxs(_Fragment, { children: [showSkeleton && !isLoaded && (_jsx(Skeleton, { style: { position: "absolute", inset: 0, height: "3.5rem", width: "100%" } })), _jsx("audio", { ref: setAudioRef, src: url, className: mediaClassName, controls: false, autoPlay: autoPlay, loop: loop, preload: preload, onLoadedData: reveal, onCanPlay: reveal, onError: handleError, onLoadedMetadata: handleLoadedMetadata, onTimeUpdate: playbackHandlers.onTimeUpdate, onPlay: playbackHandlers.onPlay, onPause: playbackHandlers.onPause, onSeeked: playbackHandlers.onSeeked, onEnded: playbackHandlers.onEnded, style: {
|
|
77
81
|
width: "100%",
|
|
78
82
|
opacity: isLoaded ? 1 : 0,
|
|
79
|
-
transition: "opacity
|
|
83
|
+
transition: "opacity var(--motion-slow) var(--motion-ease)",
|
|
80
84
|
} }), _jsx(AudioPlayerChrome, { audioRef: audioRef, visualizerMode: visualizerMode, persistKey: persistKey, extraActions: extraActions }), endOverlay != null && hasEnded && (_jsx("div", { className: "mv-end-overlay", style: { position: "absolute", inset: 0, zIndex: 2 }, children: endOverlay }))] })) }));
|
|
81
85
|
}
|
|
82
86
|
//# sourceMappingURL=audio-viewer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-viewer.js","sourceRoot":"","sources":["../../src/react/audio-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { useInView } from \"react-intersection-observer\";\nimport { Skeleton } from \"@ttt-productions/ui-core/react\";\nimport type { AudioViewerProps } from \"../types.js\";\nimport { useMediaPlayback } from \"./use-media-playback.js\";\nimport { AudioPlayerChrome } from \"./audio-player-chrome.js\";\nimport { useLoadWatchdog } from \"./use-load-watchdog.js\";\n\nexport function AudioViewer(props: AudioViewerProps) {\n const {\n url,\n className,\n mediaClassName,\n autoPlay,\n loop = false,\n lazy = true,\n skeleton: showSkeleton = true,\n priority = false,\n preload = \"metadata\",\n onLoad,\n onLoadChange,\n onError,\n fallback,\n loadTimeoutMs,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n visualizerMode,\n persistKey,\n extraActions,\n } = props;\n\n const [isLoaded, setIsLoaded] = React.useState(false);\n const [hasError, setHasError] = React.useState(false);\n const [hasMetadata, setHasMetadata] = React.useState(false);\n const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);\n const audioRef = React.useRef<HTMLAudioElement>(null);\n\n // Additive playback API — derives from element events (no observer, MEDIA-102).\n const { hasEnded, handlers: playbackHandlers, attachElement } = useMediaPlayback(\n audioRef,\n { onEnded, onProgressSample, startAtSeconds, endOverlay },\n playbackControlsRef,\n url,\n );\n\n // The element attaches through the playback lifecycle so the hook knows the\n // active generation; detach (unmount) retires it.\n const setAudioRef = React.useCallback(\n (el: HTMLAudioElement | null) => {\n audioRef.current = el;\n attachElement(el);\n },\n [attachElement],\n );\n\n const { ref: inViewRef, inView } = useInView({\n triggerOnce: false,\n threshold: 0.01,\n skip: priority || shouldLoad,\n rootMargin: \"100px\",\n });\n\n React.useEffect(() => {\n if (inView && !shouldLoad) {\n setShouldLoad(true);\n }\n }, [inView, shouldLoad]);\n\n // Accordion compatibility: delayed auto-load on mount\n React.useEffect(() => {\n if (priority || shouldLoad) return;\n const timer = setTimeout(() => {\n if (!shouldLoad) setShouldLoad(true);\n }, 100);\n return () => clearTimeout(timer);\n }, [url, priority, shouldLoad]);\n\n React.useEffect(() => {\n onLoadChange?.(!isLoaded);\n }, [isLoaded, onLoadChange]);\n\n React.useEffect(() => {\n setHasError(false);\n setIsLoaded(false);\n setHasMetadata(false);\n }, [url]);\n\n const handleLoadedData = React.useCallback(() => {\n setIsLoaded(true);\n onLoad?.();\n }, [onLoad]);\n\n const handleCanPlay = React.useCallback(() => {\n setIsLoaded(true);\n }, []);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n setIsLoaded(true); // Stop skeleton on error\n onError?.();\n }, [onError]);\n\n // Watchdog settles on METADATA — preload=\"metadata\" audio may not fire\n // loadeddata/canplay until play, and must not false-positive.\n const handleLoadedMetadata = React.useCallback(\n (e: React.SyntheticEvent<HTMLAudioElement>) => {\n setHasMetadata(true);\n playbackHandlers.onLoadedMetadata(e);\n },\n [playbackHandlers]\n );\n\n useLoadWatchdog(\n shouldLoad && !isLoaded && !hasMetadata && !hasError,\n loadTimeoutMs,\n handleError\n );\n\n if (hasError) {\n if (fallback) return <>{fallback}</>;\n return (\n <div className={className}>\n <div className=\"mv-audio-error\">\n <p>Failed to load audio</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n ref={inViewRef}\n className={className}\n style={{ position: \"relative\", width: \"100%\", outline: \"none\" }}\n >\n {!shouldLoad ? (\n showSkeleton ? <Skeleton style={{ height: \"3.5rem\", width: \"100%\" }} /> : null\n ) : (\n <>\n {showSkeleton && !isLoaded && (\n <Skeleton style={{ position: \"absolute\", inset: 0, height: \"3.5rem\", width: \"100%\" }} />\n )}\n <audio\n ref={setAudioRef}\n src={url}\n className={mediaClassName}\n controls={false}\n autoPlay={autoPlay}\n loop={loop}\n preload={preload}\n onLoadedData={handleLoadedData}\n onCanPlay={handleCanPlay}\n onError={handleError}\n onLoadedMetadata={handleLoadedMetadata}\n onTimeUpdate={playbackHandlers.onTimeUpdate}\n onPlay={playbackHandlers.onPlay}\n onPause={playbackHandlers.onPause}\n onSeeked={playbackHandlers.onSeeked}\n onEnded={playbackHandlers.onEnded}\n style={{\n width: \"100%\",\n opacity: isLoaded ? 1 : 0,\n transition: \"opacity 300ms ease\",\n }}\n />\n <AudioPlayerChrome\n audioRef={audioRef}\n visualizerMode={visualizerMode}\n persistKey={persistKey}\n extraActions={extraActions}\n />\n {endOverlay != null && hasEnded && (\n <div\n className=\"mv-end-overlay\"\n style={{ position: \"absolute\", inset: 0, zIndex: 2 }}\n >\n {endOverlay}\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audio-viewer.js","sourceRoot":"","sources":["../../src/react/audio-viewer.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,MAAM,EACJ,GAAG,EACH,SAAS,EACT,cAAc,EACd,QAAQ,EACR,IAAI,GAAG,KAAK,EACZ,IAAI,GAAG,IAAI,EACX,QAAQ,EAAE,YAAY,GAAG,IAAI,EAC7B,QAAQ,GAAG,KAAK,EAChB,OAAO,GAAG,UAAU,EACpB,MAAM,EACN,YAAY,EACZ,OAAO,EACP,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,cAAc,EACd,UAAU,EACV,YAAY,GACb,GAAG,KAAK,CAAC;IAEV,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAmB,IAAI,CAAC,CAAC;IACtD,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE5C,gFAAgF;IAChF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAE,aAAa,EAAE,GAAG,gBAAgB,CAC9E,QAAQ,EACR,EAAE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,EACzD,mBAAmB,EACnB,GAAG,CACJ,CAAC;IAEF,4EAA4E;IAC5E,kDAAkD;IAClD,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CACnC,CAAC,EAA2B,EAAE,EAAE;QAC9B,QAAQ,CAAC,OAAO,GAAG,EAAE,CAAC;QACtB,aAAa,CAAC,EAAE,CAAC,CAAC;IACpB,CAAC,EACD,CAAC,aAAa,CAAC,CAChB,CAAC;IAEF,0FAA0F;IAC1F,uFAAuF;IACvF,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC3C,WAAW,EAAE,KAAK;QAClB,SAAS,EAAE,IAAI;QACf,IAAI,EAAE,QAAQ,IAAI,CAAC,UAAU,IAAI,CAAC,kBAAkB,CAAC;QACrD,UAAU,EAAE,OAAO;QACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,kBAAkB,EAAE,CAAC,OAAO,CAAC;KACrD,CAAC,CAAC;IAEH,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1B,aAAa,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAEzB,sDAAsD;IACtD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,QAAQ,IAAI,UAAU;YAAE,OAAO;QACnC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,UAAU;gBAAE,aAAa,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC,EAAE,GAAG,CAAC,CAAC;QACR,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;IAEhC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;IAE7B,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,eAAe,CAAC,OAAO,GAAG,KAAK,CAAC;IAClC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEV,qFAAqF;IACrF,yFAAyF;IACzF,qDAAqD;IACrD,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QACpC,WAAW,CAAC,IAAI,CAAC,CAAC;QAClB,IAAI,eAAe,CAAC,OAAO;YAAE,OAAO;QACpC,eAAe,CAAC,OAAO,GAAG,IAAI,CAAC;QAC/B,MAAM,EAAE,EAAE,CAAC;IACb,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QACzC,WAAW,CAAC,IAAI,CAAC,CAAC;QAClB,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,yBAAyB;QAC5C,OAAO,EAAE,EAAE,CAAC;IACd,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,MAAM,oBAAoB,GAAG,KAAK,CAAC,WAAW,CAC5C,CAAC,CAAyC,EAAE,EAAE;QAC5C,MAAM,EAAE,CAAC;QACT,gBAAgB,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,EACD,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAC3B,CAAC;IAEF,eAAe,CAAC,UAAU,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC;IAElF,mFAAmF;IACnF,8CAA8C;IAC9C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,CACL,cACE,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,YAEpF,QAAQ,IAAI,CACX,cAAK,SAAS,EAAC,gBAAgB,YAC7B,+CAA2B,GACvB,CACP,GACG,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,cACE,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAE9D,CAAC,UAAU,CAAC,CAAC,CAAC,CACb,YAAY,CAAC,CAAC,CAAC,KAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAI,CAAC,CAAC,CAAC,IAAI,CAC/E,CAAC,CAAC,CAAC,CACF,8BACG,YAAY,IAAI,CAAC,QAAQ,IAAI,CAC5B,KAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAI,CACzF,EACD,gBACE,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,KAAK,EACf,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,WAAW,EACpB,gBAAgB,EAAE,oBAAoB,EACtC,YAAY,EAAE,gBAAgB,CAAC,YAAY,EAC3C,MAAM,EAAE,gBAAgB,CAAC,MAAM,EAC/B,OAAO,EAAE,gBAAgB,CAAC,OAAO,EACjC,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,EACnC,OAAO,EAAE,gBAAgB,CAAC,OAAO,EACjC,KAAK,EAAE;wBACL,KAAK,EAAE,MAAM;wBACb,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;wBACzB,UAAU,EAAE,+CAA+C;qBAC5D,GACD,EACF,KAAC,iBAAiB,IAChB,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,cAAc,EAC9B,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,YAAY,GAC1B,EACD,UAAU,IAAI,IAAI,IAAI,QAAQ,IAAI,CACjC,cACE,SAAS,EAAC,gBAAgB,EAC1B,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,YAEnD,UAAU,GACP,CACP,IACA,CACJ,GACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["import * as React from \"react\";\nimport { useInView } from \"react-intersection-observer\";\nimport { Skeleton } from \"@ttt-productions/ui-core/react\";\nimport type { AudioViewerProps } from \"../types.js\";\nimport { useMediaPlayback } from \"./use-media-playback.js\";\nimport { AudioPlayerChrome } from \"./audio-player-chrome.js\";\nimport { useLoadWatchdog } from \"./use-load-watchdog.js\";\n\nexport function AudioViewer(props: AudioViewerProps) {\n const {\n url,\n className,\n mediaClassName,\n autoPlay,\n loop = false,\n lazy = true,\n skeleton: showSkeleton = true,\n priority = false,\n preload = \"metadata\",\n onLoad,\n onLoadChange,\n onError,\n onVisibilityChange,\n fallback,\n loadTimeoutMs,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n visualizerMode,\n persistKey,\n extraActions,\n } = props;\n\n const [isLoaded, setIsLoaded] = React.useState(false);\n const [hasError, setHasError] = React.useState(false);\n const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);\n const audioRef = React.useRef<HTMLAudioElement>(null);\n const loadReportedRef = React.useRef(false);\n\n // Additive playback API — derives from element events (no observer, MEDIA-102).\n const { hasEnded, handlers: playbackHandlers, attachElement } = useMediaPlayback(\n audioRef,\n { onEnded, onProgressSample, startAtSeconds, endOverlay },\n playbackControlsRef,\n url,\n );\n\n // The element attaches through the playback lifecycle so the hook knows the\n // active generation; detach (unmount) retires it.\n const setAudioRef = React.useCallback(\n (el: HTMLAudioElement | null) => {\n audioRef.current = el;\n attachElement(el);\n },\n [attachElement],\n );\n\n // The viewer's one observer (MEDIA-102): lazy-load gating, and — while an owner listens —\n // the visibility it reports, which is why it keeps running after the load starts then.\n const { ref: inViewRef, inView } = useInView({\n triggerOnce: false,\n threshold: 0.01,\n skip: priority || (shouldLoad && !onVisibilityChange),\n rootMargin: \"100px\",\n onChange: (visible) => onVisibilityChange?.(visible),\n });\n\n React.useEffect(() => {\n if (inView && !shouldLoad) {\n setShouldLoad(true);\n }\n }, [inView, shouldLoad]);\n\n // Accordion compatibility: delayed auto-load on mount\n React.useEffect(() => {\n if (priority || shouldLoad) return;\n const timer = setTimeout(() => {\n if (!shouldLoad) setShouldLoad(true);\n }, 100);\n return () => clearTimeout(timer);\n }, [url, priority, shouldLoad]);\n\n React.useEffect(() => {\n onLoadChange?.(!isLoaded);\n }, [isLoaded, onLoadChange]);\n\n React.useEffect(() => {\n setHasError(false);\n setIsLoaded(false);\n loadReportedRef.current = false;\n }, [url]);\n\n // A preload=\"metadata\" element may not fire loadeddata or canplay until play, so the\n // first of loadedmetadata / loadeddata / canplay makes it usable: the skeleton goes, the\n // player shows, and onLoad fires once for this load.\n const reveal = React.useCallback(() => {\n setIsLoaded(true);\n if (loadReportedRef.current) return;\n loadReportedRef.current = true;\n onLoad?.();\n }, [onLoad]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n setIsLoaded(true); // Stop skeleton on error\n onError?.();\n }, [onError]);\n\n const handleLoadedMetadata = React.useCallback(\n (e: React.SyntheticEvent<HTMLAudioElement>) => {\n reveal();\n playbackHandlers.onLoadedMetadata(e);\n },\n [reveal, playbackHandlers]\n );\n\n useLoadWatchdog(shouldLoad && !isLoaded && !hasError, loadTimeoutMs, handleError);\n\n // The error state stays inside the observed wrapper, so the viewer keeps reporting\n // its visibility while its owner recovers it.\n if (hasError) {\n return (\n <div\n ref={inViewRef}\n className={className}\n style={fallback ? { position: \"relative\", width: \"100%\", height: \"100%\" } : undefined}\n >\n {fallback ?? (\n <div className=\"mv-audio-error\">\n <p>Failed to load audio</p>\n </div>\n )}\n </div>\n );\n }\n\n return (\n <div\n ref={inViewRef}\n className={className}\n style={{ position: \"relative\", width: \"100%\", outline: \"none\" }}\n >\n {!shouldLoad ? (\n showSkeleton ? <Skeleton style={{ height: \"3.5rem\", width: \"100%\" }} /> : null\n ) : (\n <>\n {showSkeleton && !isLoaded && (\n <Skeleton style={{ position: \"absolute\", inset: 0, height: \"3.5rem\", width: \"100%\" }} />\n )}\n <audio\n ref={setAudioRef}\n src={url}\n className={mediaClassName}\n controls={false}\n autoPlay={autoPlay}\n loop={loop}\n preload={preload}\n onLoadedData={reveal}\n onCanPlay={reveal}\n onError={handleError}\n onLoadedMetadata={handleLoadedMetadata}\n onTimeUpdate={playbackHandlers.onTimeUpdate}\n onPlay={playbackHandlers.onPlay}\n onPause={playbackHandlers.onPause}\n onSeeked={playbackHandlers.onSeeked}\n onEnded={playbackHandlers.onEnded}\n style={{\n width: \"100%\",\n opacity: isLoaded ? 1 : 0,\n transition: \"opacity var(--motion-slow) var(--motion-ease)\",\n }}\n />\n <AudioPlayerChrome\n audioRef={audioRef}\n visualizerMode={visualizerMode}\n persistKey={persistKey}\n extraActions={extraActions}\n />\n {endOverlay != null && hasEnded && (\n <div\n className=\"mv-end-overlay\"\n style={{ position: \"absolute\", inset: 0, zIndex: 2 }}\n >\n {endOverlay}\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-viewer.d.ts","sourceRoot":"","sources":["../../src/react/image-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"image-viewer.d.ts","sourceRoot":"","sources":["../../src/react/image-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,4BA+JlD"}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { useInView } from "react-intersection-observer";
|
|
4
4
|
import { Skeleton } from "@ttt-productions/ui-core/react";
|
|
5
5
|
import { useLoadWatchdog } from "./use-load-watchdog.js";
|
|
6
6
|
export function ImageViewer(props) {
|
|
7
|
-
const { url, alt, className, mediaClassName, enableZoom = true, lazy = true, unloadOnExit = false, priority = false, skeleton: showSkeleton = true, isCircular = false, preventGestures = true, onLoad, onError, fallback, loadTimeoutMs, } = props;
|
|
7
|
+
const { url, alt, className, mediaClassName, enableZoom = true, lazy = true, unloadOnExit = false, priority = false, skeleton: showSkeleton = true, isCircular = false, preventGestures = true, onLoad, onError, onVisibilityChange, fallback, loadTimeoutMs, } = props;
|
|
8
8
|
const [isLoaded, setIsLoaded] = React.useState(false);
|
|
9
9
|
const [hasError, setHasError] = React.useState(false);
|
|
10
10
|
const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);
|
|
11
11
|
const [zoomed, setZoomed] = React.useState(false);
|
|
12
12
|
const imgRef = React.useRef(null);
|
|
13
|
+
// The viewer's one observer (MEDIA-102): lazy-load gating, and — while an owner listens —
|
|
14
|
+
// the visibility it reports, which is why it keeps observing after the first sighting then.
|
|
13
15
|
const { ref: inViewRef, inView } = useInView({
|
|
14
|
-
triggerOnce: !unloadOnExit,
|
|
16
|
+
triggerOnce: !unloadOnExit && !onVisibilityChange,
|
|
15
17
|
threshold: 0.01,
|
|
16
18
|
rootMargin: "50px",
|
|
17
19
|
skip: priority || !lazy,
|
|
20
|
+
onChange: (visible) => onVisibilityChange?.(visible),
|
|
18
21
|
});
|
|
19
22
|
React.useEffect(() => {
|
|
20
23
|
if (inView && !shouldLoad) {
|
|
@@ -68,12 +71,14 @@ export function ImageViewer(props) {
|
|
|
68
71
|
toggleZoom();
|
|
69
72
|
}
|
|
70
73
|
}, [enableZoom, toggleZoom]);
|
|
71
|
-
if (hasError) {
|
|
72
|
-
return fallback ? _jsx(_Fragment, { children: fallback }) : null;
|
|
73
|
-
}
|
|
74
74
|
const wrapperStyle = isCircular
|
|
75
75
|
? { borderRadius: "50%", overflow: "hidden" }
|
|
76
76
|
: {};
|
|
77
|
+
// The error state stays inside the observed wrapper, so the viewer keeps reporting
|
|
78
|
+
// its visibility while its owner recovers it.
|
|
79
|
+
if (hasError) {
|
|
80
|
+
return fallback ? (_jsx("div", { ref: inViewRef, className: className, style: { position: "relative", width: "100%", height: "100%", ...wrapperStyle }, children: fallback })) : null;
|
|
81
|
+
}
|
|
77
82
|
return (_jsxs("div", { ref: inViewRef, className: className, role: enableZoom ? "button" : undefined, tabIndex: enableZoom ? 0 : undefined, "aria-pressed": enableZoom ? zoomed : undefined, "aria-label": enableZoom ? (zoomed ? "Zoom out image" : "Zoom in image") : undefined, onClick: toggleZoom, onKeyDown: onKeyDown, style: { position: "relative", width: "100%", height: "100%", outline: "none", ...wrapperStyle }, children: [showSkeleton && !isLoaded && shouldLoad && (_jsx(Skeleton, { style: { position: "absolute", inset: 0, width: "100%", height: "100%" } })), shouldLoad && (_jsx("img", { ref: imgRef, src: url, alt: alt ?? "", draggable: false, className: mediaClassName, onLoad: handleLoad, onError: handleError, loading: priority ? "eager" : "lazy", decoding: "async", style: {
|
|
78
83
|
maxWidth: "100%",
|
|
79
84
|
maxHeight: "100%",
|
|
@@ -81,7 +86,7 @@ export function ImageViewer(props) {
|
|
|
81
86
|
height: "100%",
|
|
82
87
|
objectFit: "cover",
|
|
83
88
|
opacity: isLoaded ? 1 : 0,
|
|
84
|
-
transition: "opacity
|
|
89
|
+
transition: "opacity var(--motion-slow) var(--motion-ease), transform var(--motion-fast) var(--motion-ease)",
|
|
85
90
|
transform: zoomed ? "scale(1.5)" : "scale(1)",
|
|
86
91
|
cursor: enableZoom ? (zoomed ? "zoom-out" : "zoom-in") : undefined,
|
|
87
92
|
WebkitUserSelect: "none",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-viewer.js","sourceRoot":"","sources":["../../src/react/image-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { useInView } from \"react-intersection-observer\";\nimport { Skeleton } from \"@ttt-productions/ui-core/react\";\nimport type { ImageViewerProps } from \"../types.js\";\nimport { useLoadWatchdog } from \"./use-load-watchdog.js\";\n\nexport function ImageViewer(props: ImageViewerProps) {\n const {\n url,\n alt,\n className,\n mediaClassName,\n enableZoom = true,\n lazy = true,\n unloadOnExit = false,\n priority = false,\n skeleton: showSkeleton = true,\n isCircular = false,\n preventGestures = true,\n onLoad,\n onError,\n fallback,\n loadTimeoutMs,\n } = props;\n\n const [isLoaded, setIsLoaded] = React.useState(false);\n const [hasError, setHasError] = React.useState(false);\n const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);\n const [zoomed, setZoomed] = React.useState(false);\n const imgRef = React.useRef<HTMLImageElement>(null);\n\n const { ref: inViewRef, inView } = useInView({\n triggerOnce: !unloadOnExit,\n threshold: 0.01,\n rootMargin: \"50px\",\n skip: priority || !lazy,\n });\n\n React.useEffect(() => {\n if (inView && !shouldLoad) {\n setShouldLoad(true);\n }\n // Unload when leaving viewport (if enabled)\n if (!inView && unloadOnExit && shouldLoad && !priority) {\n setShouldLoad(false);\n setIsLoaded(false);\n }\n }, [inView, shouldLoad, unloadOnExit, priority]);\n\n React.useEffect(() => {\n setHasError(false);\n setIsLoaded(false);\n }, [url]);\n\n const handleLoad = React.useCallback(() => {\n setIsLoaded(true);\n onLoad?.();\n }, [onLoad]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n onError?.();\n }, [onError]);\n\n // Bounded load: a visible image that fires neither load nor error within the\n // budget is treated as errored so it resolves instead of skeleton-ing forever.\n useLoadWatchdog(shouldLoad && !isLoaded && !hasError, loadTimeoutMs, handleError);\n\n // iOS: prevent multi-touch zoom gestures on the image\n React.useEffect(() => {\n const img = imgRef.current;\n if (!img || !preventGestures) return;\n\n const prevent = (e: TouchEvent) => {\n if (e.touches.length > 1) e.preventDefault();\n };\n\n img.addEventListener(\"touchstart\", prevent, { passive: false });\n return () => {\n img.removeEventListener(\"touchstart\", prevent);\n };\n }, [preventGestures, shouldLoad]);\n\n const toggleZoom = React.useCallback(() => {\n if (!enableZoom) return;\n setZoomed((z) => !z);\n }, [enableZoom]);\n\n const onKeyDown = React.useCallback(\n (e: React.KeyboardEvent) => {\n if (!enableZoom) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n toggleZoom();\n }\n },\n [enableZoom, toggleZoom]\n );\n\n if (hasError) {\n return fallback ? <>{fallback}</> : null;\n }\n\n const wrapperStyle: React.CSSProperties = isCircular\n ? { borderRadius: \"50%\", overflow: \"hidden\" }\n : {};\n\n return (\n <div\n ref={inViewRef}\n className={className}\n role={enableZoom ? \"button\" : undefined}\n tabIndex={enableZoom ? 0 : undefined}\n aria-pressed={enableZoom ? zoomed : undefined}\n aria-label={enableZoom ? (zoomed ? \"Zoom out image\" : \"Zoom in image\") : undefined}\n onClick={toggleZoom}\n onKeyDown={onKeyDown}\n style={{ position: \"relative\", width: \"100%\", height: \"100%\", outline: \"none\", ...wrapperStyle }}\n >\n {showSkeleton && !isLoaded && shouldLoad && (\n <Skeleton style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" }} />\n )}\n {shouldLoad && (\n <img\n ref={imgRef}\n src={url}\n alt={alt ?? \"\"}\n draggable={false}\n className={mediaClassName}\n onLoad={handleLoad}\n onError={handleError}\n loading={priority ? \"eager\" : \"lazy\"}\n decoding=\"async\"\n style={{\n maxWidth: \"100%\",\n maxHeight: \"100%\",\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n opacity: isLoaded ? 1 : 0,\n transition: \"opacity 300ms ease, transform 150ms ease\",\n transform: zoomed ? \"scale(1.5)\" : \"scale(1)\",\n cursor: enableZoom ? (zoomed ? \"zoom-out\" : \"zoom-in\") : undefined,\n WebkitUserSelect: \"none\",\n WebkitTouchCallout: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n } as React.CSSProperties}\n />\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"image-viewer.js","sourceRoot":"","sources":["../../src/react/image-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { useInView } from \"react-intersection-observer\";\nimport { Skeleton } from \"@ttt-productions/ui-core/react\";\nimport type { ImageViewerProps } from \"../types.js\";\nimport { useLoadWatchdog } from \"./use-load-watchdog.js\";\n\nexport function ImageViewer(props: ImageViewerProps) {\n const {\n url,\n alt,\n className,\n mediaClassName,\n enableZoom = true,\n lazy = true,\n unloadOnExit = false,\n priority = false,\n skeleton: showSkeleton = true,\n isCircular = false,\n preventGestures = true,\n onLoad,\n onError,\n onVisibilityChange,\n fallback,\n loadTimeoutMs,\n } = props;\n\n const [isLoaded, setIsLoaded] = React.useState(false);\n const [hasError, setHasError] = React.useState(false);\n const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);\n const [zoomed, setZoomed] = React.useState(false);\n const imgRef = React.useRef<HTMLImageElement>(null);\n\n // The viewer's one observer (MEDIA-102): lazy-load gating, and — while an owner listens —\n // the visibility it reports, which is why it keeps observing after the first sighting then.\n const { ref: inViewRef, inView } = useInView({\n triggerOnce: !unloadOnExit && !onVisibilityChange,\n threshold: 0.01,\n rootMargin: \"50px\",\n skip: priority || !lazy,\n onChange: (visible) => onVisibilityChange?.(visible),\n });\n\n React.useEffect(() => {\n if (inView && !shouldLoad) {\n setShouldLoad(true);\n }\n // Unload when leaving viewport (if enabled)\n if (!inView && unloadOnExit && shouldLoad && !priority) {\n setShouldLoad(false);\n setIsLoaded(false);\n }\n }, [inView, shouldLoad, unloadOnExit, priority]);\n\n React.useEffect(() => {\n setHasError(false);\n setIsLoaded(false);\n }, [url]);\n\n const handleLoad = React.useCallback(() => {\n setIsLoaded(true);\n onLoad?.();\n }, [onLoad]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n onError?.();\n }, [onError]);\n\n // Bounded load: a visible image that fires neither load nor error within the\n // budget is treated as errored so it resolves instead of skeleton-ing forever.\n useLoadWatchdog(shouldLoad && !isLoaded && !hasError, loadTimeoutMs, handleError);\n\n // iOS: prevent multi-touch zoom gestures on the image\n React.useEffect(() => {\n const img = imgRef.current;\n if (!img || !preventGestures) return;\n\n const prevent = (e: TouchEvent) => {\n if (e.touches.length > 1) e.preventDefault();\n };\n\n img.addEventListener(\"touchstart\", prevent, { passive: false });\n return () => {\n img.removeEventListener(\"touchstart\", prevent);\n };\n }, [preventGestures, shouldLoad]);\n\n const toggleZoom = React.useCallback(() => {\n if (!enableZoom) return;\n setZoomed((z) => !z);\n }, [enableZoom]);\n\n const onKeyDown = React.useCallback(\n (e: React.KeyboardEvent) => {\n if (!enableZoom) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n toggleZoom();\n }\n },\n [enableZoom, toggleZoom]\n );\n\n const wrapperStyle: React.CSSProperties = isCircular\n ? { borderRadius: \"50%\", overflow: \"hidden\" }\n : {};\n\n // The error state stays inside the observed wrapper, so the viewer keeps reporting\n // its visibility while its owner recovers it.\n if (hasError) {\n return fallback ? (\n <div\n ref={inViewRef}\n className={className}\n style={{ position: \"relative\", width: \"100%\", height: \"100%\", ...wrapperStyle }}\n >\n {fallback}\n </div>\n ) : null;\n }\n\n return (\n <div\n ref={inViewRef}\n className={className}\n role={enableZoom ? \"button\" : undefined}\n tabIndex={enableZoom ? 0 : undefined}\n aria-pressed={enableZoom ? zoomed : undefined}\n aria-label={enableZoom ? (zoomed ? \"Zoom out image\" : \"Zoom in image\") : undefined}\n onClick={toggleZoom}\n onKeyDown={onKeyDown}\n style={{ position: \"relative\", width: \"100%\", height: \"100%\", outline: \"none\", ...wrapperStyle }}\n >\n {showSkeleton && !isLoaded && shouldLoad && (\n <Skeleton style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" }} />\n )}\n {shouldLoad && (\n <img\n ref={imgRef}\n src={url}\n alt={alt ?? \"\"}\n draggable={false}\n className={mediaClassName}\n onLoad={handleLoad}\n onError={handleError}\n loading={priority ? \"eager\" : \"lazy\"}\n decoding=\"async\"\n style={{\n maxWidth: \"100%\",\n maxHeight: \"100%\",\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n opacity: isLoaded ? 1 : 0,\n transition: \"opacity var(--motion-slow) var(--motion-ease), transform var(--motion-fast) var(--motion-ease)\",\n transform: zoomed ? \"scale(1.5)\" : \"scale(1)\",\n cursor: enableZoom ? (zoomed ? \"zoom-out\" : \"zoom-in\") : undefined,\n WebkitUserSelect: \"none\",\n WebkitTouchCallout: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n } as React.CSSProperties}\n />\n )}\n </div>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-viewer.d.ts","sourceRoot":"","sources":["../../src/react/media-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,iBAAiB,EAAiC,MAAM,aAAa,CAAC;AA6GpF,wBAAgB,WAAW,CAAC,KAAK,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"media-viewer.d.ts","sourceRoot":"","sources":["../../src/react/media-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,iBAAiB,EAAiC,MAAM,aAAa,CAAC;AA6GpF,wBAAgB,WAAW,CAAC,KAAK,EAAE,iBAAiB,4BA2PnD;AAED,wBAAwB;AACxB,eAAO,MAAM,YAAY,oBAAc,CAAC"}
|
|
@@ -69,13 +69,27 @@ export function MediaViewer(props) {
|
|
|
69
69
|
setHasError(false);
|
|
70
70
|
setRemountKey((k) => k + 1);
|
|
71
71
|
}, []);
|
|
72
|
+
// The viewer's visibility as its own observer reports it (MEDIA-102 — this router adds
|
|
73
|
+
// none). Only a recovering viewer listens — one whose element failed, the only time a
|
|
74
|
+
// retry is scheduled — and retries wait while it is off screen. A healthy viewer does
|
|
75
|
+
// not listen, so its observer keeps its own lazy-load lifetime. Each failure starts from
|
|
76
|
+
// "visible", so a report from an earlier failure never outlives it.
|
|
77
|
+
const listensForVisibility = recoveryAdapter !== undefined && hasError;
|
|
78
|
+
const [elementVisible, setElementVisible] = React.useState(true);
|
|
79
|
+
const [listenedLastRender, setListenedLastRender] = React.useState(listensForVisibility);
|
|
80
|
+
if (listenedLastRender !== listensForVisibility) {
|
|
81
|
+
setListenedLastRender(listensForVisibility);
|
|
82
|
+
setElementVisible(true);
|
|
83
|
+
}
|
|
72
84
|
// Recovery hook (no-op when recoveryAdapter is omitted)
|
|
73
85
|
const { recoveryState, onMediaError, onMediaLoad, manualRetry } = useMediaRecovery({
|
|
74
86
|
url: typeof url === "string" ? url : null,
|
|
75
87
|
adapter: recoveryAdapter,
|
|
88
|
+
isElementVisible: elementVisible,
|
|
76
89
|
statusHint: assetStatusHint,
|
|
77
90
|
onRemount: handleRemount,
|
|
78
91
|
});
|
|
92
|
+
const onVisibilityChange = listensForVisibility ? setElementVisible : undefined;
|
|
79
93
|
React.useEffect(() => {
|
|
80
94
|
setHasError(false);
|
|
81
95
|
let objectUrl = null;
|
|
@@ -128,23 +142,23 @@ export function MediaViewer(props) {
|
|
|
128
142
|
if (!sourceUrl) {
|
|
129
143
|
return (_jsx("div", { className: className, style: wrapperStyle, children: urlPending ? _jsx(PendingFallback, { isCircular: isCircular }) : _jsx(EmptyFallback, { isCircular: isCircular }) }));
|
|
130
144
|
}
|
|
131
|
-
// Recovery overlay (for all non-idle/non-loaded recovery states)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
145
|
+
// Recovery overlay (for all non-idle/non-loaded recovery states). It renders as the
|
|
146
|
+
// failed viewer's fallback, inside that viewer's observed wrapper, so the viewer keeps
|
|
147
|
+
// reporting its visibility while it recovers.
|
|
148
|
+
const recoveryOverlay = isInRecovery && hasError ? (_jsx(RecoveryOverlay, { state: recoveryState, onManualRetry: manualRetry, isCircular: isCircular })) : null;
|
|
135
149
|
// Vanilla error (no recovery adapter) -> error fallback
|
|
136
150
|
if (hasError && !recoveryAdapter) {
|
|
137
151
|
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(ErrorFallback, { isCircular: isCircular }) }));
|
|
138
152
|
}
|
|
139
153
|
const t = inferType({ type, mime, name, url: sourceUrl });
|
|
140
154
|
if (t === "image") {
|
|
141
|
-
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(ImageViewer, { url: sourceUrl, alt: alt, lazy: lazy, priority: priority, skeleton: skeleton, unloadOnExit: unloadOnExit, isCircular: isCircular, enableZoom: enableZoom, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onError: handleError, fallback: _jsx(ErrorFallback, { isCircular: isCircular }) }, remountKey) }));
|
|
155
|
+
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(ImageViewer, { url: sourceUrl, alt: alt, lazy: lazy, priority: priority, skeleton: skeleton, unloadOnExit: unloadOnExit, isCircular: isCircular, enableZoom: enableZoom, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onError: handleError, onVisibilityChange: onVisibilityChange, fallback: recoveryOverlay ?? _jsx(ErrorFallback, { isCircular: isCircular }) }, remountKey) }));
|
|
142
156
|
}
|
|
143
157
|
if (t === "video") {
|
|
144
|
-
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(VideoViewer, { url: sourceUrl, lazy: lazy, priority: priority, skeleton: skeleton, unloadOnExit: unloadOnExit, controls: controls, autoPlay: autoPlay, muted: muted, loop: loop, posterUrl: posterUrl, preload: preload, autoPlayOnVisible: autoPlayOnVisible, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onError: handleError, onEnded: onEnded, onProgressSample: onProgressSample, startAtSeconds: startAtSeconds, endOverlay: endOverlay, playbackControlsRef: playbackControlsRef }, remountKey) }));
|
|
158
|
+
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(VideoViewer, { url: sourceUrl, lazy: lazy, priority: priority, skeleton: skeleton, unloadOnExit: unloadOnExit, controls: controls, autoPlay: autoPlay, muted: muted, loop: loop, posterUrl: posterUrl, preload: preload, autoPlayOnVisible: autoPlayOnVisible, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onError: handleError, onVisibilityChange: onVisibilityChange, fallback: recoveryOverlay ?? undefined, onEnded: onEnded, onProgressSample: onProgressSample, startAtSeconds: startAtSeconds, endOverlay: endOverlay, playbackControlsRef: playbackControlsRef }, remountKey) }));
|
|
145
159
|
}
|
|
146
160
|
if (t === "audio") {
|
|
147
|
-
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(AudioViewer, { url: sourceUrl, lazy: lazy, priority: priority, skeleton: skeleton, autoPlay: autoPlay, loop: loop, preload: preload, visualizerMode: audioVisualizerMode, persistKey: audioPersistKey, extraActions: audioExtraActions, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onLoadChange: onLoadChange, onError: handleError, onEnded: onEnded, onProgressSample: onProgressSample, startAtSeconds: startAtSeconds, endOverlay: endOverlay, playbackControlsRef: playbackControlsRef }, remountKey) }));
|
|
161
|
+
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(AudioViewer, { url: sourceUrl, lazy: lazy, priority: priority, skeleton: skeleton, autoPlay: autoPlay, loop: loop, preload: preload, visualizerMode: audioVisualizerMode, persistKey: audioPersistKey, extraActions: audioExtraActions, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onLoadChange: onLoadChange, onError: handleError, onVisibilityChange: onVisibilityChange, fallback: recoveryOverlay ?? undefined, onEnded: onEnded, onProgressSample: onProgressSample, startAtSeconds: startAtSeconds, endOverlay: endOverlay, playbackControlsRef: playbackControlsRef }, remountKey) }));
|
|
148
162
|
}
|
|
149
163
|
// Unknown type -> fallback link
|
|
150
164
|
return shouldShowFallback(fallbackMode) ? (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(MediaFallbackLink, { url: sourceUrl, filename: filename, label: fallbackLabel ?? "Download" }) })) : null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-viewer.js","sourceRoot":"","sources":["../../src/react/media-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { getSimplifiedMediaType } from \"@ttt-productions/media-schemas\";\nimport type { SimplifiedMediaType } from \"@ttt-productions/media-schemas\";\n\nimport type { MediaPreviewProps, MediaViewerType, FallbackMode } from \"../types.js\";\nimport { MediaFallbackLink, shouldShowFallback, EmptyFallback, ErrorFallback, PendingFallback } from \"./fallback.js\";\nimport { ImageViewer } from \"./image-viewer.js\";\nimport { VideoViewer } from \"./video-viewer.js\";\nimport { AudioViewer } from \"./audio-viewer.js\";\nimport { useMediaRecovery } from \"./use-media-recovery.js\";\nimport type { RecoveryState } from \"../recovery.js\";\n\nfunction inferType(props: { type?: MediaPreviewProps[\"type\"]; mime?: string; name?: string; url?: string }): MediaViewerType {\n if (props.type) {\n // Handle MIME strings like \"image/png\" -> \"image\"\n const primary = typeof props.type === \"string\" ? props.type.split(\"/\")[0] : props.type;\n if (primary === \"image\" || primary === \"video\" || primary === \"audio\") return primary;\n if (primary === \"other\") return \"other\";\n }\n\n const hint = props.mime ?? props.name ?? props.url;\n if (!hint) return \"other\";\n const simplified: SimplifiedMediaType = getSimplifiedMediaType(hint);\n return simplified;\n}\n\n// ---------------------------------------------------------------------------\n// Recovery-phase UI helpers\n// ---------------------------------------------------------------------------\n\nfunction RecoveryOverlay({ state, onManualRetry, isCircular }: {\n state: RecoveryState;\n onManualRetry: () => void;\n isCircular?: boolean;\n}) {\n const wrapStyle: React.CSSProperties = {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n gap: \"0.5rem\",\n ...(isCircular ? { borderRadius: \"50%\" } : {}),\n };\n\n if (state.phase === \"processing\") {\n return (\n <div className=\"mv-recovery-processing\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Processing media…</span>\n </div>\n );\n }\n\n if (state.phase === \"finalizing\") {\n return (\n <div className=\"mv-recovery-finalizing\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Finishing media…</span>\n </div>\n );\n }\n\n if (state.phase === \"transient-retry\" || state.phase === \"propagating\") {\n return (\n <div className=\"mv-recovery-retrying\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Loading…</span>\n </div>\n );\n }\n\n if (state.phase === \"auth-retry\") {\n return (\n <div className=\"mv-recovery-auth\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Refreshing access…</span>\n </div>\n );\n }\n\n if (state.phase === \"hard-unavailable\") {\n return (\n <div className=\"mv-recovery-hard\" style={wrapStyle}>\n <ErrorFallback isCircular={isCircular} />\n {state.reason === \"auth\" ? (\n <span className=\"mv-recovery-label\">Access denied.</span>\n ) : (\n <span className=\"mv-recovery-label\">Media unavailable.</span>\n )}\n </div>\n );\n }\n\n if (state.phase === \"max-wait-fallback\") {\n return (\n <div className=\"mv-recovery-max-wait\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">This media is taking longer than expected.</span>\n <button\n type=\"button\"\n className=\"mv-recovery-retry-btn\"\n onClick={onManualRetry}\n >\n Retry\n </button>\n </div>\n );\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport function MediaViewer(props: MediaPreviewProps) {\n const {\n url,\n alt,\n type,\n className,\n unloadOnExit,\n urlPending = false,\n isCircular = false,\n enableZoom,\n priority,\n skeleton,\n lazy,\n controls,\n autoPlay,\n muted,\n loop,\n posterUrl,\n preload,\n autoPlayOnVisible,\n onLoad,\n onError,\n onLoadChange,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n audioVisualizerMode,\n audioPersistKey,\n audioExtraActions,\n fallbackMode = \"link\",\n fallbackLabel,\n filename,\n mime,\n name,\n recoveryAdapter,\n assetStatusHint,\n loadTimeoutMs,\n } = props;\n\n const [hasError, setHasError] = React.useState(false);\n const [sourceUrl, setSourceUrl] = React.useState<string | null>(null);\n\n // Remount key: incrementing this causes child media elements to unmount and\n // re-mount from scratch, re-issuing the HTTP request.\n const [remountKey, setRemountKey] = React.useState(0);\n\n const handleRemount = React.useCallback(() => {\n setHasError(false);\n setRemountKey((k) => k + 1);\n }, []);\n\n // Recovery hook (no-op when recoveryAdapter is omitted)\n const { recoveryState, onMediaError, onMediaLoad, manualRetry } =\n useMediaRecovery({\n url: typeof url === \"string\" ? url : null,\n adapter: recoveryAdapter,\n statusHint: assetStatusHint,\n onRemount: handleRemount,\n });\n\n React.useEffect(() => {\n setHasError(false);\n let objectUrl: string | null = null;\n\n if (typeof url === \"string\") {\n setSourceUrl(url);\n } else if (url instanceof Blob) {\n objectUrl = URL.createObjectURL(url);\n setSourceUrl(objectUrl);\n } else {\n setSourceUrl(null);\n }\n\n return () => {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [url]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n onError?.();\n // If a recovery adapter is provided, delegate to the recovery hook.\n if (recoveryAdapter) {\n onMediaError();\n }\n }, [onError, recoveryAdapter, onMediaError]);\n\n const handleLoad = React.useCallback(() => {\n setHasError(false);\n onLoad?.();\n // Signal successful load to the recovery state machine.\n if (recoveryAdapter) {\n onMediaLoad();\n }\n }, [onLoad, recoveryAdapter, onMediaLoad]);\n\n const circularStyle: React.CSSProperties = isCircular\n ? { borderRadius: \"50%\", overflow: \"hidden\" }\n : {};\n\n const wrapperStyle: React.CSSProperties = {\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n ...circularStyle,\n };\n\n // When recovery is active and in a terminal/blocking state, show the overlay.\n const isInRecovery =\n recoveryAdapter !== undefined &&\n recoveryState.phase !== \"idle\" &&\n recoveryState.phase !== \"loaded\" &&\n recoveryState.phase !== \"loading\";\n\n // No URL -> empty fallback\n if (!sourceUrl) {\n return (\n <div className={className} style={wrapperStyle}>\n {urlPending ? <PendingFallback isCircular={isCircular} /> : <EmptyFallback isCircular={isCircular} />}\n </div>\n );\n }\n\n // Recovery overlay (for all non-idle/non-loaded recovery states)\n if (isInRecovery && hasError) {\n return (\n <div className={className} style={wrapperStyle}>\n <RecoveryOverlay\n state={recoveryState}\n onManualRetry={manualRetry}\n isCircular={isCircular}\n />\n </div>\n );\n }\n\n // Vanilla error (no recovery adapter) -> error fallback\n if (hasError && !recoveryAdapter) {\n return (\n <div className={className} style={wrapperStyle}>\n <ErrorFallback isCircular={isCircular} />\n </div>\n );\n }\n\n const t = inferType({ type, mime, name, url: sourceUrl });\n\n if (t === \"image\") {\n return (\n <div className={className} style={wrapperStyle}>\n <ImageViewer\n key={remountKey}\n url={sourceUrl}\n alt={alt}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n unloadOnExit={unloadOnExit}\n isCircular={isCircular}\n enableZoom={enableZoom}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onError={handleError}\n fallback={<ErrorFallback isCircular={isCircular} />}\n />\n </div>\n );\n }\n\n if (t === \"video\") {\n return (\n <div className={className} style={wrapperStyle}>\n <VideoViewer\n key={remountKey}\n url={sourceUrl}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n unloadOnExit={unloadOnExit}\n controls={controls}\n autoPlay={autoPlay}\n muted={muted}\n loop={loop}\n posterUrl={posterUrl}\n preload={preload}\n autoPlayOnVisible={autoPlayOnVisible}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onError={handleError}\n onEnded={onEnded}\n onProgressSample={onProgressSample}\n startAtSeconds={startAtSeconds}\n endOverlay={endOverlay}\n playbackControlsRef={playbackControlsRef}\n />\n </div>\n );\n }\n\n if (t === \"audio\") {\n return (\n <div className={className} style={wrapperStyle}>\n <AudioViewer\n key={remountKey}\n url={sourceUrl}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n autoPlay={autoPlay}\n loop={loop}\n preload={preload}\n visualizerMode={audioVisualizerMode}\n persistKey={audioPersistKey}\n extraActions={audioExtraActions}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onLoadChange={onLoadChange}\n onError={handleError}\n onEnded={onEnded}\n onProgressSample={onProgressSample}\n startAtSeconds={startAtSeconds}\n endOverlay={endOverlay}\n playbackControlsRef={playbackControlsRef}\n />\n </div>\n );\n }\n\n // Unknown type -> fallback link\n return shouldShowFallback(fallbackMode as FallbackMode) ? (\n <div className={className} style={wrapperStyle}>\n <MediaFallbackLink url={sourceUrl} filename={filename} label={fallbackLabel ?? \"Download\"} />\n </div>\n ) : null;\n}\n\n/** Back-compat alias */\nexport const MediaPreview = MediaViewer;\n"]}
|
|
1
|
+
{"version":3,"file":"media-viewer.js","sourceRoot":"","sources":["../../src/react/media-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { getSimplifiedMediaType } from \"@ttt-productions/media-schemas\";\nimport type { SimplifiedMediaType } from \"@ttt-productions/media-schemas\";\n\nimport type { MediaPreviewProps, MediaViewerType, FallbackMode } from \"../types.js\";\nimport { MediaFallbackLink, shouldShowFallback, EmptyFallback, ErrorFallback, PendingFallback } from \"./fallback.js\";\nimport { ImageViewer } from \"./image-viewer.js\";\nimport { VideoViewer } from \"./video-viewer.js\";\nimport { AudioViewer } from \"./audio-viewer.js\";\nimport { useMediaRecovery } from \"./use-media-recovery.js\";\nimport type { RecoveryState } from \"../recovery.js\";\n\nfunction inferType(props: { type?: MediaPreviewProps[\"type\"]; mime?: string; name?: string; url?: string }): MediaViewerType {\n if (props.type) {\n // Handle MIME strings like \"image/png\" -> \"image\"\n const primary = typeof props.type === \"string\" ? props.type.split(\"/\")[0] : props.type;\n if (primary === \"image\" || primary === \"video\" || primary === \"audio\") return primary;\n if (primary === \"other\") return \"other\";\n }\n\n const hint = props.mime ?? props.name ?? props.url;\n if (!hint) return \"other\";\n const simplified: SimplifiedMediaType = getSimplifiedMediaType(hint);\n return simplified;\n}\n\n// ---------------------------------------------------------------------------\n// Recovery-phase UI helpers\n// ---------------------------------------------------------------------------\n\nfunction RecoveryOverlay({ state, onManualRetry, isCircular }: {\n state: RecoveryState;\n onManualRetry: () => void;\n isCircular?: boolean;\n}) {\n const wrapStyle: React.CSSProperties = {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n gap: \"0.5rem\",\n ...(isCircular ? { borderRadius: \"50%\" } : {}),\n };\n\n if (state.phase === \"processing\") {\n return (\n <div className=\"mv-recovery-processing\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Processing media…</span>\n </div>\n );\n }\n\n if (state.phase === \"finalizing\") {\n return (\n <div className=\"mv-recovery-finalizing\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Finishing media…</span>\n </div>\n );\n }\n\n if (state.phase === \"transient-retry\" || state.phase === \"propagating\") {\n return (\n <div className=\"mv-recovery-retrying\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Loading…</span>\n </div>\n );\n }\n\n if (state.phase === \"auth-retry\") {\n return (\n <div className=\"mv-recovery-auth\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Refreshing access…</span>\n </div>\n );\n }\n\n if (state.phase === \"hard-unavailable\") {\n return (\n <div className=\"mv-recovery-hard\" style={wrapStyle}>\n <ErrorFallback isCircular={isCircular} />\n {state.reason === \"auth\" ? (\n <span className=\"mv-recovery-label\">Access denied.</span>\n ) : (\n <span className=\"mv-recovery-label\">Media unavailable.</span>\n )}\n </div>\n );\n }\n\n if (state.phase === \"max-wait-fallback\") {\n return (\n <div className=\"mv-recovery-max-wait\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">This media is taking longer than expected.</span>\n <button\n type=\"button\"\n className=\"mv-recovery-retry-btn\"\n onClick={onManualRetry}\n >\n Retry\n </button>\n </div>\n );\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport function MediaViewer(props: MediaPreviewProps) {\n const {\n url,\n alt,\n type,\n className,\n unloadOnExit,\n urlPending = false,\n isCircular = false,\n enableZoom,\n priority,\n skeleton,\n lazy,\n controls,\n autoPlay,\n muted,\n loop,\n posterUrl,\n preload,\n autoPlayOnVisible,\n onLoad,\n onError,\n onLoadChange,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n audioVisualizerMode,\n audioPersistKey,\n audioExtraActions,\n fallbackMode = \"link\",\n fallbackLabel,\n filename,\n mime,\n name,\n recoveryAdapter,\n assetStatusHint,\n loadTimeoutMs,\n } = props;\n\n const [hasError, setHasError] = React.useState(false);\n const [sourceUrl, setSourceUrl] = React.useState<string | null>(null);\n\n // Remount key: incrementing this causes child media elements to unmount and\n // re-mount from scratch, re-issuing the HTTP request.\n const [remountKey, setRemountKey] = React.useState(0);\n\n const handleRemount = React.useCallback(() => {\n setHasError(false);\n setRemountKey((k) => k + 1);\n }, []);\n\n // The viewer's visibility as its own observer reports it (MEDIA-102 — this router adds\n // none). Only a recovering viewer listens — one whose element failed, the only time a\n // retry is scheduled — and retries wait while it is off screen. A healthy viewer does\n // not listen, so its observer keeps its own lazy-load lifetime. Each failure starts from\n // \"visible\", so a report from an earlier failure never outlives it.\n const listensForVisibility = recoveryAdapter !== undefined && hasError;\n const [elementVisible, setElementVisible] = React.useState(true);\n const [listenedLastRender, setListenedLastRender] = React.useState(listensForVisibility);\n if (listenedLastRender !== listensForVisibility) {\n setListenedLastRender(listensForVisibility);\n setElementVisible(true);\n }\n\n // Recovery hook (no-op when recoveryAdapter is omitted)\n const { recoveryState, onMediaError, onMediaLoad, manualRetry } =\n useMediaRecovery({\n url: typeof url === \"string\" ? url : null,\n adapter: recoveryAdapter,\n isElementVisible: elementVisible,\n statusHint: assetStatusHint,\n onRemount: handleRemount,\n });\n const onVisibilityChange = listensForVisibility ? setElementVisible : undefined;\n\n React.useEffect(() => {\n setHasError(false);\n let objectUrl: string | null = null;\n\n if (typeof url === \"string\") {\n setSourceUrl(url);\n } else if (url instanceof Blob) {\n objectUrl = URL.createObjectURL(url);\n setSourceUrl(objectUrl);\n } else {\n setSourceUrl(null);\n }\n\n return () => {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [url]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n onError?.();\n // If a recovery adapter is provided, delegate to the recovery hook.\n if (recoveryAdapter) {\n onMediaError();\n }\n }, [onError, recoveryAdapter, onMediaError]);\n\n const handleLoad = React.useCallback(() => {\n setHasError(false);\n onLoad?.();\n // Signal successful load to the recovery state machine.\n if (recoveryAdapter) {\n onMediaLoad();\n }\n }, [onLoad, recoveryAdapter, onMediaLoad]);\n\n const circularStyle: React.CSSProperties = isCircular\n ? { borderRadius: \"50%\", overflow: \"hidden\" }\n : {};\n\n const wrapperStyle: React.CSSProperties = {\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n ...circularStyle,\n };\n\n // When recovery is active and in a terminal/blocking state, show the overlay.\n const isInRecovery =\n recoveryAdapter !== undefined &&\n recoveryState.phase !== \"idle\" &&\n recoveryState.phase !== \"loaded\" &&\n recoveryState.phase !== \"loading\";\n\n // No URL -> empty fallback\n if (!sourceUrl) {\n return (\n <div className={className} style={wrapperStyle}>\n {urlPending ? <PendingFallback isCircular={isCircular} /> : <EmptyFallback isCircular={isCircular} />}\n </div>\n );\n }\n\n // Recovery overlay (for all non-idle/non-loaded recovery states). It renders as the\n // failed viewer's fallback, inside that viewer's observed wrapper, so the viewer keeps\n // reporting its visibility while it recovers.\n const recoveryOverlay =\n isInRecovery && hasError ? (\n <RecoveryOverlay state={recoveryState} onManualRetry={manualRetry} isCircular={isCircular} />\n ) : null;\n\n // Vanilla error (no recovery adapter) -> error fallback\n if (hasError && !recoveryAdapter) {\n return (\n <div className={className} style={wrapperStyle}>\n <ErrorFallback isCircular={isCircular} />\n </div>\n );\n }\n\n const t = inferType({ type, mime, name, url: sourceUrl });\n\n if (t === \"image\") {\n return (\n <div className={className} style={wrapperStyle}>\n <ImageViewer\n key={remountKey}\n url={sourceUrl}\n alt={alt}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n unloadOnExit={unloadOnExit}\n isCircular={isCircular}\n enableZoom={enableZoom}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onError={handleError}\n onVisibilityChange={onVisibilityChange}\n fallback={recoveryOverlay ?? <ErrorFallback isCircular={isCircular} />}\n />\n </div>\n );\n }\n\n if (t === \"video\") {\n return (\n <div className={className} style={wrapperStyle}>\n <VideoViewer\n key={remountKey}\n url={sourceUrl}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n unloadOnExit={unloadOnExit}\n controls={controls}\n autoPlay={autoPlay}\n muted={muted}\n loop={loop}\n posterUrl={posterUrl}\n preload={preload}\n autoPlayOnVisible={autoPlayOnVisible}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onError={handleError}\n onVisibilityChange={onVisibilityChange}\n fallback={recoveryOverlay ?? undefined}\n onEnded={onEnded}\n onProgressSample={onProgressSample}\n startAtSeconds={startAtSeconds}\n endOverlay={endOverlay}\n playbackControlsRef={playbackControlsRef}\n />\n </div>\n );\n }\n\n if (t === \"audio\") {\n return (\n <div className={className} style={wrapperStyle}>\n <AudioViewer\n key={remountKey}\n url={sourceUrl}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n autoPlay={autoPlay}\n loop={loop}\n preload={preload}\n visualizerMode={audioVisualizerMode}\n persistKey={audioPersistKey}\n extraActions={audioExtraActions}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onLoadChange={onLoadChange}\n onError={handleError}\n onVisibilityChange={onVisibilityChange}\n fallback={recoveryOverlay ?? undefined}\n onEnded={onEnded}\n onProgressSample={onProgressSample}\n startAtSeconds={startAtSeconds}\n endOverlay={endOverlay}\n playbackControlsRef={playbackControlsRef}\n />\n </div>\n );\n }\n\n // Unknown type -> fallback link\n return shouldShowFallback(fallbackMode as FallbackMode) ? (\n <div className={className} style={wrapperStyle}>\n <MediaFallbackLink url={sourceUrl} filename={filename} label={fallbackLabel ?? \"Download\"} />\n </div>\n ) : null;\n}\n\n/** Back-compat alias */\nexport const MediaPreview = MediaViewer;\n"]}
|
|
@@ -4,6 +4,12 @@ export interface UseMediaRecoveryOptions {
|
|
|
4
4
|
url: string | null | undefined;
|
|
5
5
|
/** Injected diagnostic adapter from the app. */
|
|
6
6
|
adapter?: MediaDiagnosticAdapter;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the viewer's own element is on screen, from the viewer's one
|
|
9
|
+
* IntersectionObserver (MEDIA-102). A scheduled retry waits while it is false.
|
|
10
|
+
* Omitted = visible (a viewer that does not observe itself).
|
|
11
|
+
*/
|
|
12
|
+
isElementVisible?: boolean;
|
|
7
13
|
/**
|
|
8
14
|
* Optional app hint about the server-side lifecycle of the asset.
|
|
9
15
|
* When "failed" or "rejected" the element error is treated as hard.
|
|
@@ -38,5 +44,5 @@ export interface UseMediaRecoveryResult {
|
|
|
38
44
|
*/
|
|
39
45
|
manualRetry: () => void;
|
|
40
46
|
}
|
|
41
|
-
export declare function useMediaRecovery({ url, adapter, statusHint, onRemount, }: UseMediaRecoveryOptions): UseMediaRecoveryResult;
|
|
47
|
+
export declare function useMediaRecovery({ url, adapter, isElementVisible, statusHint, onRemount, }: UseMediaRecoveryOptions): UseMediaRecoveryResult;
|
|
42
48
|
//# sourceMappingURL=use-media-recovery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-media-recovery.d.ts","sourceRoot":"","sources":["../../src/react/use-media-recovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,sBAAsB,EACtB,aAAa,EACb,eAAe,EAChB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-media-recovery.d.ts","sourceRoot":"","sources":["../../src/react/use-media-recovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,sBAAsB,EACtB,aAAa,EACb,eAAe,EAChB,MAAM,gBAAgB,CAAC;AAoFxB,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAE/B,gDAAgD;IAChD,OAAO,CAAC,EAAE,sBAAsB,CAAC;IAEjC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAE7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,8BAA8B;IAC9B,aAAa,EAAE,aAAa,CAAC;IAE7B;;;;OAIG;IACH,YAAY,EAAE,MAAM,IAAI,CAAC;IAEzB;;;OAGG;IACH,WAAW,EAAE,MAAM,IAAI,CAAC;IAExB;;;OAGG;IACH,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB;AAOD,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,OAAO,EACP,gBAAuB,EACvB,UAAU,EACV,SAAS,GACV,EAAE,uBAAuB,GAAG,sBAAsB,CA6VlD"}
|