@oxyhq/bloom 1.0.6 → 1.2.0
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/docs/level-picker.mdx +105 -0
- package/docs/media-flight.mdx +272 -0
- package/docs/migrating-the-media-gallery.mdx +103 -0
- package/docs/migrating-to-1.0.mdx +1 -1
- package/docs/slider.mdx +3 -1
- package/docs/zoomable-media-gallery.mdx +116 -0
- package/lib/commonjs/floating/index.js.map +1 -1
- package/lib/commonjs/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
- package/lib/commonjs/index.js +8 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +33 -25
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/level-picker/LevelPicker.js +349 -0
- package/lib/commonjs/level-picker/LevelPicker.js.map +1 -0
- package/lib/commonjs/level-picker/LevelPicker.web.js +346 -0
- package/lib/commonjs/level-picker/LevelPicker.web.js.map +1 -0
- package/lib/commonjs/level-picker/constants.js +137 -0
- package/lib/commonjs/level-picker/constants.js.map +1 -0
- package/lib/commonjs/level-picker/index.js +13 -0
- package/lib/commonjs/level-picker/index.js.map +1 -0
- package/lib/commonjs/level-picker/index.web.js +13 -0
- package/lib/commonjs/level-picker/index.web.js.map +1 -0
- package/lib/commonjs/level-picker/types.js +6 -0
- package/lib/commonjs/level-picker/types.js.map +1 -0
- package/lib/commonjs/media-flight/MediaFlightLayer.js +151 -0
- package/lib/commonjs/media-flight/MediaFlightLayer.js.map +1 -0
- package/lib/commonjs/media-flight/MediaSurface.js +188 -0
- package/lib/commonjs/media-flight/MediaSurface.js.map +1 -0
- package/lib/commonjs/media-flight/constants.js +39 -0
- package/lib/commonjs/media-flight/constants.js.map +1 -0
- package/lib/commonjs/media-flight/expo-video-module.js +148 -0
- package/lib/commonjs/media-flight/expo-video-module.js.map +1 -0
- package/lib/commonjs/media-flight/index.js +102 -0
- package/lib/commonjs/media-flight/index.js.map +1 -0
- package/lib/commonjs/media-flight/store.js +457 -0
- package/lib/commonjs/media-flight/store.js.map +1 -0
- package/lib/commonjs/media-flight/types.js +6 -0
- package/lib/commonjs/media-flight/types.js.map +1 -0
- package/lib/commonjs/media-flight/use-media-flight.js +41 -0
- package/lib/commonjs/media-flight/use-media-flight.js.map +1 -0
- package/lib/commonjs/tab-bar/TabBarBase.js +1 -1
- package/lib/commonjs/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +185 -131
- package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js +5 -5
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js.map +1 -1
- package/lib/commonjs/zoomable-media-gallery/types.js +6 -0
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
- package/lib/module/floating/index.js +5 -0
- package/lib/module/floating/index.js.map +1 -1
- package/lib/module/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +3 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/level-picker/LevelPicker.js +343 -0
- package/lib/module/level-picker/LevelPicker.js.map +1 -0
- package/lib/module/level-picker/LevelPicker.web.js +340 -0
- package/lib/module/level-picker/LevelPicker.web.js.map +1 -0
- package/lib/module/level-picker/constants.js +131 -0
- package/lib/module/level-picker/constants.js.map +1 -0
- package/lib/module/level-picker/index.js +4 -0
- package/lib/module/level-picker/index.js.map +1 -0
- package/lib/module/level-picker/index.web.js +4 -0
- package/lib/module/level-picker/index.web.js.map +1 -0
- package/lib/module/level-picker/types.js +4 -0
- package/lib/module/level-picker/types.js.map +1 -0
- package/lib/module/media-flight/MediaFlightLayer.js +146 -0
- package/lib/module/media-flight/MediaFlightLayer.js.map +1 -0
- package/lib/module/media-flight/MediaSurface.js +180 -0
- package/lib/module/media-flight/MediaSurface.js.map +1 -0
- package/lib/module/media-flight/constants.js +35 -0
- package/lib/module/media-flight/constants.js.map +1 -0
- package/lib/module/media-flight/expo-video-module.js +142 -0
- package/lib/module/media-flight/expo-video-module.js.map +1 -0
- package/lib/module/media-flight/index.js +9 -0
- package/lib/module/media-flight/index.js.map +1 -0
- package/lib/module/media-flight/store.js +439 -0
- package/lib/module/media-flight/store.js.map +1 -0
- package/lib/module/media-flight/types.js +4 -0
- package/lib/module/media-flight/types.js.map +1 -0
- package/lib/module/media-flight/use-media-flight.js +36 -0
- package/lib/module/media-flight/use-media-flight.js.map +1 -0
- package/lib/module/tab-bar/TabBarBase.js +1 -1
- package/lib/module/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +184 -130
- package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
- package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
- package/lib/module/zoomable-media-gallery/index.js +5 -0
- package/lib/module/zoomable-media-gallery/index.js.map +1 -0
- package/lib/module/zoomable-media-gallery/types.js +4 -0
- package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
- package/lib/typescript/commonjs/floating/index.d.ts +5 -0
- package/lib/typescript/commonjs/floating/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/level-picker/LevelPicker.d.ts +6 -0
- package/lib/typescript/commonjs/level-picker/LevelPicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/LevelPicker.web.d.ts +6 -0
- package/lib/typescript/commonjs/level-picker/LevelPicker.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/constants.d.ts +93 -0
- package/lib/typescript/commonjs/level-picker/constants.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/index.d.ts +3 -0
- package/lib/typescript/commonjs/level-picker/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/index.web.d.ts +3 -0
- package/lib/typescript/commonjs/level-picker/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/types.d.ts +63 -0
- package/lib/typescript/commonjs/level-picker/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts +11 -0
- package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts +106 -0
- package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/constants.d.ts +18 -0
- package/lib/typescript/commonjs/media-flight/constants.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts +103 -0
- package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/index.d.ts +9 -0
- package/lib/typescript/commonjs/media-flight/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/store.d.ts +132 -0
- package/lib/typescript/commonjs/media-flight/store.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/types.d.ts +195 -0
- package/lib/typescript/commonjs/media-flight/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts +4 -0
- package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
- package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-media-gallery/index.d.ts +4 -0
- package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts +90 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts.map +1 -0
- package/lib/typescript/module/floating/index.d.ts +5 -0
- package/lib/typescript/module/floating/index.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/level-picker/LevelPicker.d.ts +6 -0
- package/lib/typescript/module/level-picker/LevelPicker.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/LevelPicker.web.d.ts +6 -0
- package/lib/typescript/module/level-picker/LevelPicker.web.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/constants.d.ts +93 -0
- package/lib/typescript/module/level-picker/constants.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/index.d.ts +3 -0
- package/lib/typescript/module/level-picker/index.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/index.web.d.ts +3 -0
- package/lib/typescript/module/level-picker/index.web.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/types.d.ts +63 -0
- package/lib/typescript/module/level-picker/types.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts +11 -0
- package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/MediaSurface.d.ts +106 -0
- package/lib/typescript/module/media-flight/MediaSurface.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/constants.d.ts +18 -0
- package/lib/typescript/module/media-flight/constants.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/expo-video-module.d.ts +103 -0
- package/lib/typescript/module/media-flight/expo-video-module.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/index.d.ts +9 -0
- package/lib/typescript/module/media-flight/index.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/store.d.ts +132 -0
- package/lib/typescript/module/media-flight/store.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/types.d.ts +195 -0
- package/lib/typescript/module/media-flight/types.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/use-media-flight.d.ts +4 -0
- package/lib/typescript/module/media-flight/use-media-flight.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
- package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
- package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-media-gallery/index.d.ts +4 -0
- package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-media-gallery/types.d.ts +90 -0
- package/lib/typescript/module/zoomable-media-gallery/types.d.ts.map +1 -0
- package/package.json +61 -8
- package/src/floating/index.ts +5 -0
- package/src/image-aspect-ratio-cache/aspect-ratio-cache.ts +1 -1
- package/src/index.ts +4 -1
- package/src/index.web.ts +4 -1
- package/src/level-picker/LevelPicker.tsx +365 -0
- package/src/level-picker/LevelPicker.web.tsx +374 -0
- package/src/level-picker/constants.ts +131 -0
- package/src/level-picker/index.ts +2 -0
- package/src/level-picker/index.web.ts +2 -0
- package/src/level-picker/types.ts +63 -0
- package/src/media-flight/MediaFlightLayer.tsx +144 -0
- package/src/media-flight/MediaSurface.tsx +233 -0
- package/src/media-flight/constants.ts +32 -0
- package/src/media-flight/expo-video-module.ts +181 -0
- package/src/media-flight/index.ts +33 -0
- package/src/media-flight/store.ts +497 -0
- package/src/media-flight/types.ts +210 -0
- package/src/media-flight/use-media-flight.ts +42 -0
- package/src/tab-bar/TabBarBase.tsx +1 -1
- package/src/{zoomable-image-gallery/ZoomableImageGallery.tsx → zoomable-media-gallery/ZoomableMediaGallery.tsx} +192 -117
- package/src/zoomable-media-gallery/index.ts +11 -0
- package/src/zoomable-media-gallery/types.ts +100 -0
- package/docs/zoomable-image-gallery.mdx +0 -66
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
- package/lib/commonjs/zoomable-image-gallery/types.js +0 -2
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
- package/lib/module/zoomable-image-gallery/index.js +0 -5
- package/lib/module/zoomable-image-gallery/index.js.map +0 -1
- package/lib/module/zoomable-image-gallery/types.js +0 -2
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts +0 -4
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts +0 -56
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts.map +0 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
- package/lib/typescript/module/zoomable-image-gallery/index.d.ts +0 -4
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts +0 -56
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts.map +0 -1
- package/src/zoomable-image-gallery/index.ts +0 -9
- package/src/zoomable-image-gallery/types.ts +0 -59
- /package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
- /package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
- /package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
- /package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
- /package/src/{zoomable-image-gallery → zoomable-media-gallery}/constants.ts +0 -0
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
NativeScrollEvent,
|
|
13
13
|
type ViewStyle,
|
|
14
14
|
} from 'react-native';
|
|
15
|
-
import { Image } from 'expo-image';
|
|
16
15
|
import Animated, {
|
|
17
16
|
useSharedValue,
|
|
18
17
|
useAnimatedStyle,
|
|
@@ -36,6 +35,7 @@ import {
|
|
|
36
35
|
ArrowOutOfBox_Stroke2_Corner0_Rounded,
|
|
37
36
|
} from '../icons';
|
|
38
37
|
import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
|
|
38
|
+
import { MediaPoster, MediaSurface } from '../media-flight/MediaSurface';
|
|
39
39
|
import {
|
|
40
40
|
getAspectRatio,
|
|
41
41
|
getIntrinsicSize,
|
|
@@ -65,13 +65,44 @@ import {
|
|
|
65
65
|
THUMBNAIL_STRIP_TILE_SIZE,
|
|
66
66
|
} from './constants';
|
|
67
67
|
import type {
|
|
68
|
-
|
|
68
|
+
GalleryMedia,
|
|
69
69
|
MeasuredRect,
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
ZoomableMediaGalleryHandle,
|
|
71
|
+
ZoomableMediaGalleryProps,
|
|
72
72
|
} from './types';
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The still-frame URI of an item: the image itself, or a video's poster.
|
|
76
|
+
* `undefined` for a video with no poster — there is nothing to probe, size
|
|
77
|
+
* against or show in the strip, and every caller below handles that.
|
|
78
|
+
*/
|
|
79
|
+
function posterUri(item: GalleryMedia): string | undefined {
|
|
80
|
+
return item.kind === 'video' ? item.poster : item.uri;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The aspect ratio already known for an item, from the shared probe cache. */
|
|
84
|
+
function cachedRatio(item: GalleryMedia): number | undefined {
|
|
85
|
+
const uri = posterUri(item);
|
|
86
|
+
return uri === undefined ? undefined : getAspectRatio(uri);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A stable React key. An image is identified by its URI; a video is not (its
|
|
91
|
+
* player is an object with no URL), which is why `GalleryVideo` carries an
|
|
92
|
+
* explicit `id`. The index is folded in so a post repeating one image still
|
|
93
|
+
* pages correctly.
|
|
94
|
+
*/
|
|
95
|
+
function mediaKey(item: GalleryMedia, index: number): string {
|
|
96
|
+
return item.kind === 'video' ? `video-${item.id}-${index}` : `image-${item.uri}-${index}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What the share sheet is handed. A poster is not what "share this video"
|
|
101
|
+
* means, so a video shares its own `shareUrl` or nothing at all.
|
|
102
|
+
*/
|
|
103
|
+
function shareUri(item: GalleryMedia): string | undefined {
|
|
104
|
+
return item.kind === 'video' ? item.shareUrl : item.uri;
|
|
105
|
+
}
|
|
75
106
|
|
|
76
107
|
// Web-only interaction hints (no-ops on native): the zoom surfaces are
|
|
77
108
|
// tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
|
|
@@ -108,7 +139,7 @@ function NavArrow({
|
|
|
108
139
|
<Pressable
|
|
109
140
|
onPress={onPress}
|
|
110
141
|
accessibilityRole="button"
|
|
111
|
-
accessibilityLabel={isLeft ? 'Previous
|
|
142
|
+
accessibilityLabel={isLeft ? 'Previous item' : 'Next item'}
|
|
112
143
|
hitSlop={8}
|
|
113
144
|
onHoverIn={onHoverIn}
|
|
114
145
|
onHoverOut={onHoverOut}
|
|
@@ -134,7 +165,7 @@ interface FittedSize {
|
|
|
134
165
|
}
|
|
135
166
|
|
|
136
167
|
/**
|
|
137
|
-
* Resolve `cornerRadius` against the size an
|
|
168
|
+
* Resolve `cornerRadius` against the size an item is actually rendered at, so
|
|
138
169
|
* `'circle'` stays a circle at every fitted size (and through the open/close
|
|
139
170
|
* animation, which scales this same box).
|
|
140
171
|
*/
|
|
@@ -143,23 +174,38 @@ function resolveCornerRadius(cornerRadius: number | 'circle', fit: FittedSize):
|
|
|
143
174
|
}
|
|
144
175
|
|
|
145
176
|
/**
|
|
146
|
-
* Fullscreen, swipeable
|
|
147
|
-
* measured-origin zoom transition
|
|
177
|
+
* Fullscreen, swipeable MEDIA viewer — images and consumer-owned videos in one
|
|
178
|
+
* pager — replicating the profile avatar's measured-origin zoom transition
|
|
179
|
+
* (`ZoomableAvatar`) for rectangular post media:
|
|
148
180
|
*
|
|
149
181
|
* - Open/close feel is identical to the avatar (same spring configs, web
|
|
150
182
|
* timing/easing, blur backdrop, and the measure-origin technique). The viewer
|
|
151
183
|
* renders through the Bloom `Portal` on BOTH platforms (RN's `Modal` is not
|
|
152
184
|
* used on native — on the New Architecture / Fabric Android its host views
|
|
153
185
|
* mount full-screen but never composite, leaving the viewer invisible).
|
|
154
|
-
* - The OPENING (tapped)
|
|
186
|
+
* - The OPENING (tapped) item animates from its measured rect to a centered,
|
|
155
187
|
* aspect-ratio-preserving fit within {@link FIT_FRACTION} of the screen. Once
|
|
156
188
|
* the open animation settles, a horizontal paging `ScrollView` mounts seeded at
|
|
157
|
-
* the tapped index so the user can swipe between every
|
|
189
|
+
* the tapped index so the user can swipe between every item in the post.
|
|
158
190
|
* - Gesture disambiguation: the pager owns horizontal swipes; a vertical-only
|
|
159
191
|
* `Gesture.Pan` (`activeOffsetY` + `failOffsetX`) owns drag-to-dismiss, so the
|
|
160
192
|
* two never fight.
|
|
193
|
+
*
|
|
194
|
+
* ## Video, and why it does not restart
|
|
195
|
+
*
|
|
196
|
+
* A video page is fed by a `VideoPlayer` the CONSUMER created and owns. Bloom
|
|
197
|
+
* never creates or destroys one: expo-video keeps the player OBJECT separate
|
|
198
|
+
* from the `VideoView` that shows it, and one player may feed several views, so
|
|
199
|
+
* handing the same player to this gallery moves a playing video into fullscreen
|
|
200
|
+
* without re-opening the stream. Exactly ONE view is mounted per opening — the
|
|
201
|
+
* active page's — while every other page and every strip tile renders the still
|
|
202
|
+
* (`MediaPoster`). The single unavoidable swap is the pre-pager open surface
|
|
203
|
+
* handing over to the pager's active page once the open animation settles.
|
|
204
|
+
*
|
|
205
|
+
* `expo-video` is an OPTIONAL peer loaded through `media-flight/expo-video-module`.
|
|
206
|
+
* Without it a video page degrades to its poster, once, with a dev warning.
|
|
161
207
|
*/
|
|
162
|
-
const
|
|
208
|
+
const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, ZoomableMediaGalleryProps>(({ measureThumb, cornerRadius = DEFAULT_CORNER_RADIUS, indicatorVariant = 'dots', videoControls = false }, ref) => {
|
|
163
209
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = useWindowDimensions();
|
|
164
210
|
|
|
165
211
|
const radiusFor = useCallback(
|
|
@@ -168,17 +214,17 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
168
214
|
);
|
|
169
215
|
|
|
170
216
|
const [isOpen, setIsOpen] = useState(false);
|
|
171
|
-
// Once true, the swipeable pager is mounted and the single open-
|
|
217
|
+
// Once true, the swipeable pager is mounted and the single open-surface hidden.
|
|
172
218
|
const [pagerReady, setPagerReady] = useState(false);
|
|
173
|
-
const [
|
|
219
|
+
const [items, setItems] = useState<GalleryMedia[]>([]);
|
|
174
220
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
175
|
-
// Whether the active
|
|
221
|
+
// Whether the active media is currently pinched/double-tapped past its base
|
|
176
222
|
// size. Drives the pan-while-zoomed gesture, disables the pager's horizontal
|
|
177
|
-
// paging + the dismiss drag so a pan moves the zoomed
|
|
223
|
+
// paging + the dismiss drag so a pan moves the zoomed media instead.
|
|
178
224
|
const [isZoomed, setIsZoomed] = useState(false);
|
|
179
|
-
// Aspect ratio of the OPENING
|
|
225
|
+
// Aspect ratio of the OPENING item (drives the open-animation fit box).
|
|
180
226
|
const [openRatio, setOpenRatio] = useState<number>(DEFAULT_ASPECT_RATIO);
|
|
181
|
-
// Per-
|
|
227
|
+
// Per-item aspect ratios for the pager pages (index-aligned with `items`).
|
|
182
228
|
const [pageRatios, setPageRatios] = useState<Record<number, number>>({});
|
|
183
229
|
|
|
184
230
|
// Native `Share` is always available; web exposes a share button only when the
|
|
@@ -193,7 +239,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
193
239
|
const translateY = useSharedValue(0);
|
|
194
240
|
const opacity = useSharedValue(0);
|
|
195
241
|
|
|
196
|
-
// Zoom transform of the active
|
|
242
|
+
// Zoom transform of the active media, kept SEPARATE from the open/close +
|
|
197
243
|
// dismiss-drag transform above (reusing those would collide two meanings).
|
|
198
244
|
// `zoom*` are the live/persisted values; `baseZoomScale` is captured at pinch
|
|
199
245
|
// start and `savedZoomTranslate*` at pan start for incremental bookkeeping.
|
|
@@ -204,7 +250,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
204
250
|
const savedZoomTranslateX = useSharedValue(0);
|
|
205
251
|
const savedZoomTranslateY = useSharedValue(0);
|
|
206
252
|
|
|
207
|
-
// Origin (offset from screen center) of the tapped
|
|
253
|
+
// Origin (offset from screen center) of the tapped item.
|
|
208
254
|
const originX = useSharedValue(0);
|
|
209
255
|
const originY = useSharedValue(0);
|
|
210
256
|
|
|
@@ -212,7 +258,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
212
258
|
const startY = useSharedValue(0);
|
|
213
259
|
// Per-drag baseline captured at pan start (transient).
|
|
214
260
|
const startScale = useSharedValue(1);
|
|
215
|
-
// Persistent scale of the open-
|
|
261
|
+
// Persistent scale of the open-surface at its thumbnail origin (thumbWidth /
|
|
216
262
|
// fittedWidth). The open animation grows from this to 1; the close animation
|
|
217
263
|
// shrinks back to it. Never overwritten by the drag gesture.
|
|
218
264
|
const originScale = useSharedValue(1);
|
|
@@ -271,9 +317,9 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
271
317
|
setActiveIndex((prev) => (prev === next ? prev : next));
|
|
272
318
|
}, []);
|
|
273
319
|
|
|
274
|
-
// Snap the active
|
|
275
|
-
// all zoom bookkeeping. Called on every path that changes the active
|
|
276
|
-
// freshly-viewed
|
|
320
|
+
// Snap the active media back to its un-zoomed baseline (no animation) and
|
|
321
|
+
// clear all zoom bookkeeping. Called on every path that changes the active
|
|
322
|
+
// page so a freshly-viewed item always starts un-zoomed.
|
|
277
323
|
const resetZoom = useCallback(() => {
|
|
278
324
|
zoomScale.value = 1;
|
|
279
325
|
zoomTranslateX.value = 0;
|
|
@@ -291,7 +337,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
291
337
|
);
|
|
292
338
|
|
|
293
339
|
// Largest width/height for `ratio` that fits inside `fitBox` without cropping
|
|
294
|
-
// (contain), never exceeding the
|
|
340
|
+
// (contain), never exceeding the media's own resolution: filling the viewport
|
|
295
341
|
// with a 512px avatar or a small thumbnail just renders it soft. `uri` is what
|
|
296
342
|
// lets us look the intrinsic size up; without it (not measured yet) the fit
|
|
297
343
|
// box wins and the clamp applies on the next frame.
|
|
@@ -311,22 +357,27 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
311
357
|
return { width, height };
|
|
312
358
|
}, [fitBox]);
|
|
313
359
|
|
|
314
|
-
// Fitted size of the single open-
|
|
315
|
-
// the opened
|
|
316
|
-
// after swiping it tracks the viewed
|
|
317
|
-
// and flies back the
|
|
360
|
+
// Fitted size of the single open-surface for the CURRENT page. On open this is
|
|
361
|
+
// the opened item's fit (active index == opened index, ratio == `openRatio`);
|
|
362
|
+
// after swiping it tracks the viewed item so the collapse-on-dismiss renders
|
|
363
|
+
// and flies back the media actually on screen.
|
|
364
|
+
const activeItem = items[activeIndex];
|
|
318
365
|
const activeRatio = pageRatios[activeIndex] ?? openRatio;
|
|
319
|
-
const activeUri =
|
|
366
|
+
const activeUri = activeItem === undefined ? undefined : posterUri(activeItem);
|
|
320
367
|
const activeFit = useMemo(
|
|
321
368
|
() => fitForRatio(activeRatio, activeUri),
|
|
322
369
|
[fitForRatio, activeRatio, activeUri],
|
|
323
370
|
);
|
|
324
371
|
|
|
325
|
-
// Alt text (accessibility description) of the
|
|
372
|
+
// Alt text (accessibility description) of the item currently on screen, shown
|
|
326
373
|
// as a caption at the bottom of the viewer (Bluesky-style lightbox footer).
|
|
327
|
-
const activeAlt =
|
|
374
|
+
const activeAlt = activeItem?.alt?.trim() || undefined;
|
|
328
375
|
|
|
329
|
-
|
|
376
|
+
// Probe and cache the ratio of the page at `index`. A video with no poster has
|
|
377
|
+
// nothing to probe — it keeps whatever ratio the consumer declared, or the
|
|
378
|
+
// gallery default — so this is a no-op rather than a guess.
|
|
379
|
+
const ensureRatio = useCallback((index: number, uri: string | undefined) => {
|
|
380
|
+
if (uri === undefined) return;
|
|
330
381
|
const cached = getAspectRatio(uri);
|
|
331
382
|
if (cached !== undefined) {
|
|
332
383
|
setPageRatios((prev) => (prev[index] === cached ? prev : { ...prev, [index]: cached }));
|
|
@@ -348,7 +399,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
348
399
|
opacity.value = 0;
|
|
349
400
|
}, [opacity, scale, translateX, translateY]);
|
|
350
401
|
|
|
351
|
-
// Fly the (possibly dragged)
|
|
402
|
+
// Fly the (possibly dragged) media back toward `target` — the rect of the
|
|
352
403
|
// thumbnail currently being viewed — shrinking to its footprint. Uses the
|
|
353
404
|
// EXACT same close spring (native) / timing (web) as the avatar transition.
|
|
354
405
|
const flyBackTo = useCallback(
|
|
@@ -396,14 +447,14 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
396
447
|
// fly-back from an already-moving image.
|
|
397
448
|
if (dismissingRef.current) return;
|
|
398
449
|
dismissingRef.current = true;
|
|
399
|
-
// Collapse the pager back to the single open-
|
|
400
|
-
// one
|
|
450
|
+
// Collapse the pager back to the single open-surface so the fly-back
|
|
451
|
+
// animates one item (the current one) rather than the whole scrolled strip.
|
|
401
452
|
setPagerReady(false);
|
|
402
453
|
|
|
403
454
|
const index = activeIndexRef.current;
|
|
404
|
-
const current =
|
|
405
|
-
// Recompute the fly-back target from the CURRENT
|
|
406
|
-
// thumbnail + the same fitted box (`activeFit`) the open-
|
|
455
|
+
const current = items[index];
|
|
456
|
+
// Recompute the fly-back target from the CURRENT item: the live rect of its
|
|
457
|
+
// thumbnail + the same fitted box (`activeFit`) the open-surface is rendered
|
|
407
458
|
// at, using the same screen-center math as `open`. Falls back to a center
|
|
408
459
|
// fade-out when the thumbnail can't be measured.
|
|
409
460
|
if (measureThumb && current) {
|
|
@@ -428,7 +479,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
428
479
|
activeFit,
|
|
429
480
|
fadeOutCenter,
|
|
430
481
|
flyBackTo,
|
|
431
|
-
|
|
482
|
+
items,
|
|
432
483
|
measureThumb,
|
|
433
484
|
SCREEN_HEIGHT,
|
|
434
485
|
SCREEN_WIDTH,
|
|
@@ -442,41 +493,43 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
442
493
|
}, []);
|
|
443
494
|
|
|
444
495
|
const open = useCallback(
|
|
445
|
-
(
|
|
446
|
-
if (isOpen ||
|
|
496
|
+
(nextItems: GalleryMedia[], index: number, rect?: MeasuredRect) => {
|
|
497
|
+
if (isOpen || nextItems.length === 0) return;
|
|
447
498
|
dismissingRef.current = false;
|
|
448
|
-
const safeIndex = Math.min(Math.max(index, 0),
|
|
449
|
-
const target =
|
|
499
|
+
const safeIndex = Math.min(Math.max(index, 0), nextItems.length - 1);
|
|
500
|
+
const target = nextItems[safeIndex];
|
|
450
501
|
if (!target) return;
|
|
451
|
-
const
|
|
502
|
+
const targetUri = posterUri(target);
|
|
503
|
+
const knownRatio = target.aspectRatio ?? cachedRatio(target);
|
|
452
504
|
const ratio = knownRatio ?? DEFAULT_ASPECT_RATIO;
|
|
453
505
|
|
|
454
|
-
|
|
506
|
+
setItems(nextItems);
|
|
455
507
|
setActiveIndexBoth(safeIndex);
|
|
456
508
|
setOpenRatio(ratio);
|
|
457
509
|
// Seed every page whose ratio is already known (consumer-provided metadata
|
|
458
510
|
// or a previously-cached probe) so swiping never hits the same snap — only
|
|
459
|
-
// a genuinely-unknown
|
|
511
|
+
// a genuinely-unknown item falls through to `ensureRatio`'s async probe.
|
|
460
512
|
const initialRatios: Record<number, number> = {};
|
|
461
|
-
|
|
462
|
-
const r =
|
|
513
|
+
nextItems.forEach((item, i) => {
|
|
514
|
+
const r = item.aspectRatio ?? cachedRatio(item);
|
|
463
515
|
if (r !== undefined) initialRatios[i] = r;
|
|
464
516
|
});
|
|
465
517
|
setPageRatios(initialRatios);
|
|
466
518
|
|
|
467
|
-
// Resolve the opening ratio if it was not yet known, then re-fit.
|
|
468
|
-
|
|
469
|
-
|
|
519
|
+
// Resolve the opening ratio if it was not yet known, then re-fit. A video
|
|
520
|
+
// with no poster has nothing to probe and keeps the default.
|
|
521
|
+
if (knownRatio === undefined && targetUri !== undefined) {
|
|
522
|
+
void fetchAspectRatio(targetUri).then((resolved) => {
|
|
470
523
|
setOpenRatio(resolved);
|
|
471
524
|
setPageRatios((prev) => ({ ...prev, [safeIndex]: resolved }));
|
|
472
525
|
});
|
|
473
526
|
}
|
|
474
527
|
|
|
475
|
-
// The open-
|
|
528
|
+
// The open-surface box is rendered at its FINAL fitted size; the open
|
|
476
529
|
// animation grows it from the thumbnail's footprint (scale < 1) up to 1,
|
|
477
530
|
// mirroring the avatar's small→big scale. The initial scale is the ratio
|
|
478
531
|
// of the thumbnail width to the fitted width.
|
|
479
|
-
const fitted = fitForRatio(ratio,
|
|
532
|
+
const fitted = fitForRatio(ratio, targetUri);
|
|
480
533
|
const centerX = SCREEN_WIDTH / 2;
|
|
481
534
|
const centerY = SCREEN_HEIGHT / 2;
|
|
482
535
|
if (rect && rect.width > 0) {
|
|
@@ -591,7 +644,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
591
644
|
[opacity],
|
|
592
645
|
);
|
|
593
646
|
|
|
594
|
-
// The single open-
|
|
647
|
+
// The single open-surface animates from origin → fitted center.
|
|
595
648
|
const openImageStyle = useAnimatedStyle(
|
|
596
649
|
() => ({
|
|
597
650
|
transform: [
|
|
@@ -635,7 +688,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
635
688
|
// (web, where paging may not fire a reliable momentum-end).
|
|
636
689
|
const updateIndexFromOffset = useCallback(
|
|
637
690
|
(offsetX: number) => {
|
|
638
|
-
const lastIndex =
|
|
691
|
+
const lastIndex = items.length - 1;
|
|
639
692
|
if (lastIndex < 0) return;
|
|
640
693
|
const next = Math.min(Math.max(Math.round(offsetX / SCREEN_WIDTH), 0), lastIndex);
|
|
641
694
|
if (next === activeIndexRef.current) return;
|
|
@@ -643,10 +696,10 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
643
696
|
// the page scrolling away).
|
|
644
697
|
resetZoom();
|
|
645
698
|
setActiveIndexBoth(next);
|
|
646
|
-
const
|
|
647
|
-
if (
|
|
699
|
+
const item = items[next];
|
|
700
|
+
if (item) ensureRatio(next, posterUri(item));
|
|
648
701
|
},
|
|
649
|
-
[ensureRatio,
|
|
702
|
+
[ensureRatio, items, resetZoom, setActiveIndexBoth, SCREEN_WIDTH]
|
|
650
703
|
);
|
|
651
704
|
|
|
652
705
|
// Programmatically page to `index` (arrow buttons, keyboard, thumbnail taps).
|
|
@@ -655,13 +708,13 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
655
708
|
const pageTo = useCallback(
|
|
656
709
|
(index: number) => {
|
|
657
710
|
if (!pagerReady) return;
|
|
658
|
-
const lastIndex =
|
|
711
|
+
const lastIndex = items.length - 1;
|
|
659
712
|
if (lastIndex < 0) return;
|
|
660
713
|
const clamped = Math.min(Math.max(index, 0), lastIndex);
|
|
661
714
|
resetZoom();
|
|
662
715
|
pagerRef.current?.scrollTo({ x: clamped * SCREEN_WIDTH, y: 0, animated: true });
|
|
663
716
|
},
|
|
664
|
-
[
|
|
717
|
+
[items.length, pagerReady, resetZoom, SCREEN_WIDTH]
|
|
665
718
|
);
|
|
666
719
|
|
|
667
720
|
const onPagerScroll = useCallback(
|
|
@@ -830,10 +883,12 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
830
883
|
[tapGesture, pinchGesture, panWhileZoomedGesture]
|
|
831
884
|
);
|
|
832
885
|
|
|
833
|
-
// Share the active
|
|
834
|
-
// on supporting browsers (the button is only rendered where it exists).
|
|
886
|
+
// Share the active item's URI: the OS share sheet on native; the Web Share API
|
|
887
|
+
// on supporting browsers (the button is only rendered where it exists). A
|
|
888
|
+
// video shares its own `shareUrl`; its poster is not what "share this" means.
|
|
835
889
|
const handleShare = useCallback(async () => {
|
|
836
|
-
const
|
|
890
|
+
const item = items[activeIndexRef.current];
|
|
891
|
+
const uri = item === undefined ? undefined : shareUri(item);
|
|
837
892
|
if (!uri) return;
|
|
838
893
|
try {
|
|
839
894
|
if (Platform.OS === 'web') {
|
|
@@ -847,9 +902,9 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
847
902
|
// A user-cancelled share sheet rejects (AbortError on web) — that's
|
|
848
903
|
// expected, not a failure. Anything else is a real error worth surfacing.
|
|
849
904
|
if (err instanceof Error && err.name === 'AbortError') return;
|
|
850
|
-
if (typeof console !== 'undefined' && console.error) console.error('
|
|
905
|
+
if (typeof console !== 'undefined' && console.error) console.error('Media share failed:', err);
|
|
851
906
|
}
|
|
852
|
-
}, [
|
|
907
|
+
}, [items]);
|
|
853
908
|
|
|
854
909
|
// Keyboard navigation (web only), scoped to the open lifetime like Dialog.web's
|
|
855
910
|
// Escape handler. Reads the live index from the ref so it need not re-subscribe
|
|
@@ -888,7 +943,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
888
943
|
in isolation. */}
|
|
889
944
|
<Backdrop
|
|
890
945
|
onPress={handleDismiss}
|
|
891
|
-
accessibilityLabel="Close
|
|
946
|
+
accessibilityLabel="Close media viewer"
|
|
892
947
|
progress={opacity}
|
|
893
948
|
/>
|
|
894
949
|
|
|
@@ -902,18 +957,26 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
902
957
|
]}
|
|
903
958
|
pointerEvents="box-none"
|
|
904
959
|
>
|
|
905
|
-
{!pagerReady && (
|
|
960
|
+
{!pagerReady && activeItem !== undefined && (
|
|
906
961
|
<Pressable onPress={handleDismiss} style={webPointerStyle}>
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
962
|
+
{/* The animated transform rides the BOX, not the media, so the
|
|
963
|
+
image and video arms move identically — and so the box's
|
|
964
|
+
radius clips a video the same way it rounds an image. */}
|
|
965
|
+
<Animated.View
|
|
910
966
|
style={[
|
|
967
|
+
styles.mediaBox,
|
|
911
968
|
{ width: activeFit.width, height: activeFit.height, borderRadius: radiusFor(activeFit) },
|
|
912
969
|
openImageStyle,
|
|
913
970
|
]}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
971
|
+
>
|
|
972
|
+
<MediaSurface
|
|
973
|
+
content={activeItem}
|
|
974
|
+
contentFit="contain"
|
|
975
|
+
nativeControls={videoControls}
|
|
976
|
+
accessibilityLabel={activeItem.alt}
|
|
977
|
+
style={StyleSheet.absoluteFill}
|
|
978
|
+
/>
|
|
979
|
+
</Animated.View>
|
|
917
980
|
</Pressable>
|
|
918
981
|
)}
|
|
919
982
|
|
|
@@ -933,55 +996,64 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
933
996
|
scrollEventThrottle={16}
|
|
934
997
|
style={StyleSheet.absoluteFill}
|
|
935
998
|
>
|
|
936
|
-
{
|
|
937
|
-
const
|
|
938
|
-
const
|
|
999
|
+
{items.map((item, idx) => {
|
|
1000
|
+
const uri = posterUri(item);
|
|
1001
|
+
const ratio =
|
|
1002
|
+
pageRatios[idx] ?? cachedRatio(item) ?? item.aspectRatio ?? DEFAULT_ASPECT_RATIO;
|
|
1003
|
+
const fit = fitForRatio(ratio, uri);
|
|
939
1004
|
// Only the active page is zoomable + gesture-wrapped (double-tap
|
|
940
|
-
// / single-tap dismiss / pinch / pan).
|
|
941
|
-
//
|
|
942
|
-
//
|
|
1005
|
+
// / single-tap dismiss / pinch / pan). It is also the ONLY page
|
|
1006
|
+
// that mounts a real media surface: an off-screen page renders
|
|
1007
|
+
// the still, because a second `VideoView` on the same player is
|
|
1008
|
+
// exactly the duplicate surface this package exists to avoid.
|
|
1009
|
+
// The active media handles its own tap-to-dismiss through the
|
|
1010
|
+
// gesture, so it isn't wrapped in a Pressable.
|
|
943
1011
|
if (idx === activeIndex) {
|
|
944
1012
|
return (
|
|
945
1013
|
<Pressable
|
|
946
|
-
key={
|
|
1014
|
+
key={mediaKey(item, idx)}
|
|
947
1015
|
// The page fills the screen ON TOP of the backdrop (the
|
|
948
1016
|
// pager below it needs pointer events to swipe), so a
|
|
949
|
-
// press on the empty area around the
|
|
1017
|
+
// press on the empty area around the media can never
|
|
950
1018
|
// reach the backdrop — the page dismisses it itself.
|
|
951
|
-
// Same outcome as tapping the
|
|
1019
|
+
// Same outcome as tapping the media, which already
|
|
952
1020
|
// dismisses through `singleTapDismissGesture`.
|
|
953
1021
|
onPress={handleDismiss}
|
|
954
|
-
accessibilityLabel="Close
|
|
1022
|
+
accessibilityLabel="Close media viewer"
|
|
955
1023
|
style={[styles.page, { width: SCREEN_WIDTH, height: SCREEN_HEIGHT }, webPointerStyle]}
|
|
956
1024
|
>
|
|
957
1025
|
<GestureDetector gesture={activePageGesture}>
|
|
958
|
-
<
|
|
959
|
-
source={{ uri: img.uri }}
|
|
960
|
-
contentFit="contain"
|
|
1026
|
+
<Animated.View
|
|
961
1027
|
style={[
|
|
1028
|
+
styles.mediaBox,
|
|
962
1029
|
{ width: fit.width, height: fit.height, borderRadius: radiusFor(fit) },
|
|
963
1030
|
zoomImageStyle,
|
|
964
1031
|
]}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1032
|
+
>
|
|
1033
|
+
<MediaSurface
|
|
1034
|
+
content={item}
|
|
1035
|
+
contentFit="contain"
|
|
1036
|
+
nativeControls={videoControls}
|
|
1037
|
+
accessibilityLabel={item.alt}
|
|
1038
|
+
style={StyleSheet.absoluteFill}
|
|
1039
|
+
/>
|
|
1040
|
+
</Animated.View>
|
|
968
1041
|
</GestureDetector>
|
|
969
1042
|
</Pressable>
|
|
970
1043
|
);
|
|
971
1044
|
}
|
|
972
1045
|
return (
|
|
973
1046
|
<Pressable
|
|
974
|
-
key={
|
|
1047
|
+
key={mediaKey(item, idx)}
|
|
975
1048
|
onPress={handleDismiss}
|
|
976
|
-
accessibilityLabel="Close
|
|
1049
|
+
accessibilityLabel="Close media viewer"
|
|
977
1050
|
style={[styles.page, { width: SCREEN_WIDTH, height: SCREEN_HEIGHT }]}
|
|
978
1051
|
>
|
|
979
|
-
<
|
|
980
|
-
|
|
1052
|
+
<MediaPoster
|
|
1053
|
+
content={item}
|
|
981
1054
|
contentFit="contain"
|
|
1055
|
+
accessibilityLabel={item.alt}
|
|
982
1056
|
style={{ width: fit.width, height: fit.height, borderRadius: radiusFor(fit) }}
|
|
983
|
-
transition={0}
|
|
984
|
-
{...(Platform.OS === 'web' ? { draggable: false } : {})}
|
|
985
1057
|
/>
|
|
986
1058
|
</Pressable>
|
|
987
1059
|
);
|
|
@@ -990,10 +1062,10 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
990
1062
|
</Animated.View>
|
|
991
1063
|
)}
|
|
992
1064
|
|
|
993
|
-
{pagerReady &&
|
|
1065
|
+
{pagerReady && items.length > 1 && (
|
|
994
1066
|
<Animated.View style={[styles.indicatorWrap, backdropStyle]} pointerEvents="box-none">
|
|
995
1067
|
<View style={styles.counterPill} pointerEvents="none">
|
|
996
|
-
<Text style={styles.counterText}>{`${activeIndex + 1} / ${
|
|
1068
|
+
<Text style={styles.counterText}>{`${activeIndex + 1} / ${items.length}`}</Text>
|
|
997
1069
|
</View>
|
|
998
1070
|
{indicatorVariant === 'thumbnails' ? (
|
|
999
1071
|
<ScrollView
|
|
@@ -1002,33 +1074,30 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
1002
1074
|
style={styles.thumbStripScroll}
|
|
1003
1075
|
contentContainerStyle={styles.thumbStripContent}
|
|
1004
1076
|
>
|
|
1005
|
-
{
|
|
1077
|
+
{items.map((item, idx) => (
|
|
1006
1078
|
<Pressable
|
|
1007
|
-
key={`thumb-${
|
|
1079
|
+
key={`thumb-${mediaKey(item, idx)}`}
|
|
1008
1080
|
onPress={() => pageTo(idx)}
|
|
1009
1081
|
accessibilityRole="button"
|
|
1010
|
-
accessibilityLabel={`Go to
|
|
1082
|
+
accessibilityLabel={`Go to item ${idx + 1} of ${items.length}`}
|
|
1011
1083
|
style={[
|
|
1012
1084
|
styles.thumbTile,
|
|
1013
1085
|
idx === activeIndex ? styles.thumbTileActive : styles.thumbTileInactive,
|
|
1014
1086
|
webPointerStyle,
|
|
1015
1087
|
]}
|
|
1016
1088
|
>
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
transition={0}
|
|
1022
|
-
{...(Platform.OS === 'web' ? { draggable: false } : {})}
|
|
1023
|
-
/>
|
|
1089
|
+
{/* A STILL, never a surface: one decoder per strip tile is
|
|
1090
|
+
what mounting the real media here would cost, and for a
|
|
1091
|
+
video it would be a second view on the live player. */}
|
|
1092
|
+
<MediaPoster content={item} contentFit="cover" style={styles.thumbTileImage} />
|
|
1024
1093
|
</Pressable>
|
|
1025
1094
|
))}
|
|
1026
1095
|
</ScrollView>
|
|
1027
1096
|
) : (
|
|
1028
1097
|
<View style={styles.dotsRow} pointerEvents="none">
|
|
1029
|
-
{
|
|
1098
|
+
{items.map((item, idx) => (
|
|
1030
1099
|
<View
|
|
1031
|
-
key={`dot-${
|
|
1100
|
+
key={`dot-${mediaKey(item, idx)}`}
|
|
1032
1101
|
style={[styles.dot, idx === activeIndex ? styles.dotActive : styles.dotInactive]}
|
|
1033
1102
|
/>
|
|
1034
1103
|
))}
|
|
@@ -1037,11 +1106,11 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
1037
1106
|
</Animated.View>
|
|
1038
1107
|
)}
|
|
1039
1108
|
|
|
1040
|
-
{Platform.OS === 'web' && pagerReady &&
|
|
1109
|
+
{Platform.OS === 'web' && pagerReady && items.length > 1 && activeIndex > 0 && (
|
|
1041
1110
|
<NavArrow direction="left" onPress={() => pageTo(activeIndex - 1)} />
|
|
1042
1111
|
)}
|
|
1043
1112
|
|
|
1044
|
-
{Platform.OS === 'web' && pagerReady &&
|
|
1113
|
+
{Platform.OS === 'web' && pagerReady && items.length > 1 && activeIndex < items.length - 1 && (
|
|
1045
1114
|
<NavArrow direction="right" onPress={() => pageTo(activeIndex + 1)} />
|
|
1046
1115
|
)}
|
|
1047
1116
|
|
|
@@ -1049,7 +1118,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
1049
1118
|
<Pressable
|
|
1050
1119
|
onPress={handleShare}
|
|
1051
1120
|
accessibilityRole="button"
|
|
1052
|
-
accessibilityLabel="Share
|
|
1121
|
+
accessibilityLabel="Share media"
|
|
1053
1122
|
hitSlop={8}
|
|
1054
1123
|
style={[styles.shareButton, webPointerStyle]}
|
|
1055
1124
|
>
|
|
@@ -1061,7 +1130,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
1061
1130
|
<Animated.View
|
|
1062
1131
|
style={[
|
|
1063
1132
|
styles.altCaptionWrap,
|
|
1064
|
-
|
|
1133
|
+
items.length > 1 && styles.altCaptionWrapWithIndicator,
|
|
1065
1134
|
backdropStyle,
|
|
1066
1135
|
]}
|
|
1067
1136
|
pointerEvents="none"
|
|
@@ -1088,9 +1157,9 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
1088
1157
|
return <Portal>{renderContent()}</Portal>;
|
|
1089
1158
|
});
|
|
1090
1159
|
|
|
1091
|
-
|
|
1160
|
+
ZoomableMediaGalleryInner.displayName = 'ZoomableMediaGallery';
|
|
1092
1161
|
|
|
1093
|
-
export const
|
|
1162
|
+
export const ZoomableMediaGallery = ZoomableMediaGalleryInner;
|
|
1094
1163
|
|
|
1095
1164
|
const styles = StyleSheet.create({
|
|
1096
1165
|
modalContainer: {
|
|
@@ -1134,6 +1203,12 @@ const styles = StyleSheet.create({
|
|
|
1134
1203
|
justifyContent: 'center',
|
|
1135
1204
|
alignItems: 'center',
|
|
1136
1205
|
},
|
|
1206
|
+
// The box the media is painted into. `overflow: hidden` is what makes the
|
|
1207
|
+
// box's corner radius clip a VIDEO — an image would round on its own, a
|
|
1208
|
+
// platform video surface will not.
|
|
1209
|
+
mediaBox: {
|
|
1210
|
+
overflow: 'hidden',
|
|
1211
|
+
},
|
|
1137
1212
|
indicatorWrap: {
|
|
1138
1213
|
position: 'absolute',
|
|
1139
1214
|
bottom: 48,
|
|
@@ -1260,4 +1335,4 @@ const styles = StyleSheet.create({
|
|
|
1260
1335
|
},
|
|
1261
1336
|
});
|
|
1262
1337
|
|
|
1263
|
-
export default
|
|
1338
|
+
export default ZoomableMediaGallery;
|