expo-simple-gallery 0.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/.eslintrc.js +2 -0
- package/README.md +295 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/simplegallery/ExpoSimpleGalleryModule.kt +50 -0
- package/android/src/main/java/expo/modules/simplegallery/ExpoSimpleGalleryView.kt +30 -0
- package/biome.json +200 -0
- package/build/ExpoSimpleGallery.types.d.ts +89 -0
- package/build/ExpoSimpleGallery.types.d.ts.map +1 -0
- package/build/ExpoSimpleGallery.types.js +7 -0
- package/build/ExpoSimpleGallery.types.js.map +1 -0
- package/build/ExpoSimpleGalleryModal.d.ts +3 -0
- package/build/ExpoSimpleGalleryModal.d.ts.map +1 -0
- package/build/ExpoSimpleGalleryModal.js +46 -0
- package/build/ExpoSimpleGalleryModal.js.map +1 -0
- package/build/ExpoSimpleGalleryModal.types.d.ts +23 -0
- package/build/ExpoSimpleGalleryModal.types.d.ts.map +1 -0
- package/build/ExpoSimpleGalleryModal.types.js +2 -0
- package/build/ExpoSimpleGalleryModal.types.js.map +1 -0
- package/build/ExpoSimpleGalleryModule.d.ts +7 -0
- package/build/ExpoSimpleGalleryModule.d.ts.map +1 -0
- package/build/ExpoSimpleGalleryModule.js +4 -0
- package/build/ExpoSimpleGalleryModule.js.map +1 -0
- package/build/ExpoSimpleGalleryView.d.ts +23 -0
- package/build/ExpoSimpleGalleryView.d.ts.map +1 -0
- package/build/ExpoSimpleGalleryView.js +172 -0
- package/build/ExpoSimpleGalleryView.js.map +1 -0
- package/build/SfSymbol.types.d.ts +213 -0
- package/build/SfSymbol.types.d.ts.map +1 -0
- package/build/SfSymbol.types.js +2 -0
- package/build/SfSymbol.types.js.map +1 -0
- package/build/components/MemoizedSectionHeader.d.ts +10 -0
- package/build/components/MemoizedSectionHeader.d.ts.map +1 -0
- package/build/components/MemoizedSectionHeader.js +12 -0
- package/build/components/MemoizedSectionHeader.js.map +1 -0
- package/build/components/MemoizedThumbnailOverlayComponent.d.ts +12 -0
- package/build/components/MemoizedThumbnailOverlayComponent.d.ts.map +1 -0
- package/build/components/MemoizedThumbnailOverlayComponent.js +12 -0
- package/build/components/MemoizedThumbnailOverlayComponent.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/ios/ExpoSimpleGallery.podspec +24 -0
- package/ios/ExpoSimpleGalleryModule.swift +89 -0
- package/ios/ExpoSimpleGalleryView.swift +185 -0
- package/ios/Gallery/GalleryCell.swift +191 -0
- package/ios/Gallery/GalleryGridView+Configuration.swift +253 -0
- package/ios/Gallery/GalleryGridView+Data.swift +122 -0
- package/ios/Gallery/GalleryGridView+Gestures.swift +361 -0
- package/ios/Gallery/GalleryGridView.swift +85 -0
- package/ios/Gallery/GallerySectionHeaderView.swift +48 -0
- package/ios/Gallery/PreviewViewController.swift +90 -0
- package/ios/GalleryImageViewerModule.swift +24 -0
- package/ios/GalleryImageViewerView.swift +637 -0
- package/ios/Protocols/Cancellable.swift +3 -0
- package/ios/Protocols/ContextMenuActionsDelegate.swift +5 -0
- package/ios/Protocols/GestureEventDelegate.swift +17 -0
- package/ios/Protocols/ImageLoaderProtocol.swift +4 -0
- package/ios/Protocols/OverlayMountingDelegate.swift +26 -0
- package/ios/Protocols/OverlayPreloadingDelegate.swift +20 -0
- package/ios/Protocols/ScrollableToIndexDelegate.swift +3 -0
- package/ios/Services/ImageLoadTask.swift +7 -0
- package/ios/Services/ImageLoader.swift +213 -0
- package/ios/Services/ImageResizer.swift +17 -0
- package/ios/Types/GalleryTypes.swift +26 -0
- package/ios/Utils/LayoutCalculator.swift +15 -0
- package/ios/Viewer/ImageLoaderService.swift +439 -0
- package/ios/Viewer/ImagePageViewController+UIGestureRecognizerDelegate.swift +50 -0
- package/ios/Viewer/ImagePageViewController+UIScrollViewDelegate.swift +32 -0
- package/ios/Viewer/ImagePageViewController.swift +346 -0
- package/ios/Viewer/ImagePageViewControllerDelegate.swift +3 -0
- package/ios/Viewer/LivePhotoViewController.swift +192 -0
- package/ios/Viewer/MediaViewControllerDelegate.swift +6 -0
- package/ios/Viewer/MediaViewControllerFactory.swift +75 -0
- package/ios/Viewer/UIImage+Placeholder.swift +54 -0
- package/ios/Viewer/VideoViewController.swift +305 -0
- package/ios/exposimplegallery.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/exposimplegallery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/package.json +43 -0
- package/src/ExpoSimpleGallery.types.ts +137 -0
- package/src/ExpoSimpleGalleryModal.tsx +103 -0
- package/src/ExpoSimpleGalleryModal.types.ts +27 -0
- package/src/ExpoSimpleGalleryModule.ts +9 -0
- package/src/ExpoSimpleGalleryView.tsx +359 -0
- package/src/SfSymbol.types.ts +8426 -0
- package/src/components/MemoizedSectionHeader.tsx +33 -0
- package/src/components/MemoizedThumbnailOverlayComponent.tsx +38 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +9 -0
package/.eslintrc.js
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
ExpoSimpleGallery is a high-performance image gallery module for iOS Expo applications. It provides a native implementation of image grid views with various customization options and interactive features.
|
|
4
|
+
|
|
5
|
+
## Motivation
|
|
6
|
+
|
|
7
|
+
ExpoSimpleGallery addresses common challenges in displaying image collections in React Native iOS applications. Standard React Native image components can struggle with performance when rendering multiple high-resolution images, especially in grid layouts with scrolling and selection capabilities. This module bridges this gap by providing a native iOS implementation that leverages UICollectionView and the Photos framework, offering better memory management and more responsive user interactions. By handling image loading, caching, and rendering on the native side rather than through the JavaScript bridge, ExpoSimpleGallery helps reduce common performance bottlenecks in image-heavy applications. The module supports practical features like image selection, context menus, and customizable layouts while maintaining excellent scroll performance.
|
|
8
|
+
|
|
9
|
+
## Implementation
|
|
10
|
+
React components are dynamically mounted to native cell containers only when they become visible. The library passes information about visible ranges to JavaScript, allowing React to prepare components only for items in or near the viewport. For grouped layouts, additional section header mounting logic ensures that headers appear and disappear appropriately during scrolling. This implementation minimizes the number of simultaneously mounted React components while maintaining a responsive user experience, even with large image collections.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- ⚠️ iOS 15.1+ only
|
|
15
|
+
- ⚠️ New architecture (Fabric) only
|
|
16
|
+
- ⚠️ Expo SDK 52+
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install expo-simple-gallery
|
|
22
|
+
npx pod-install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## URI Formats
|
|
26
|
+
|
|
27
|
+
The gallery supports multiple URI formats:
|
|
28
|
+
|
|
29
|
+
- **Local Files**: `file:///path/to/image.jpg`
|
|
30
|
+
- **Photo Library Assets**: `ph://asset-id` (iOS Photos framework)
|
|
31
|
+
|
|
32
|
+
## Image Loading and Caching
|
|
33
|
+
|
|
34
|
+
ExpoSimpleGallery implements efficient image loading and caching mechanisms:
|
|
35
|
+
|
|
36
|
+
1. **Automatic Thumbnail Generation**: For photos library assets, the module automatically generates thumbnails for faster loading.
|
|
37
|
+
2. **Progressive Loading**: Images first load as lower-quality thumbnails then upgrade to high-quality versions when available.
|
|
38
|
+
3. **Memory Management**: Images are loaded and unloaded as the user scrolls to maintain smooth performance.
|
|
39
|
+
4. **Error Handling**: Built-in error handling for missing or unloadable images.
|
|
40
|
+
|
|
41
|
+
## Fullscreen Image Viewer
|
|
42
|
+
|
|
43
|
+
The gallery includes a built-in fullscreen image viewer with these features:
|
|
44
|
+
|
|
45
|
+
- **Pinch to zoom**: Double tap or pinch to zoom in/out on images
|
|
46
|
+
- **Swipe to navigate**: Horizontal swipe to navigate between images
|
|
47
|
+
- **Pull to dismiss**: Pull down to dismiss the viewer
|
|
48
|
+
- **High-quality loading**: Seamless transition to high-quality images
|
|
49
|
+
|
|
50
|
+
The viewer can be programmatically controlled using `ref.openImageViewer(index)` and `ref.closeImageViewer()`.
|
|
51
|
+
|
|
52
|
+
## Basic Usage
|
|
53
|
+
|
|
54
|
+
```jsx
|
|
55
|
+
import { ExpoSimpleGalleryView } from 'expo-simple-gallery';
|
|
56
|
+
|
|
57
|
+
export default function App() {
|
|
58
|
+
return (
|
|
59
|
+
<ExpoSimpleGalleryView
|
|
60
|
+
assets={['file:///path/to/image1.jpg', 'ph://asset-id-2']}
|
|
61
|
+
columnsCount={3}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Component
|
|
68
|
+
|
|
69
|
+
### ExpoSimpleGalleryView
|
|
70
|
+
|
|
71
|
+
The main component for rendering an image gallery grid.
|
|
72
|
+
|
|
73
|
+
#### Props
|
|
74
|
+
|
|
75
|
+
| Prop | Type | Description |
|
|
76
|
+
|------|------|-------------|
|
|
77
|
+
| `assets` | `string[]` or `string[][]` | Array of image URIs to display. Can be nested arrays for grouped sections. |
|
|
78
|
+
| `columnsCount` | `number` | Number of columns to display in the grid. |
|
|
79
|
+
| `thumbnailPressAction` | `'select'` \| `'open'` \| `'none'` | Action to perform when a thumbnail is pressed. |
|
|
80
|
+
| `thumbnailLongPressAction` | `'select'` \| `'open'` \| `'preview'` \| `'none'` | Action to perform when a thumbnail is long pressed. |
|
|
81
|
+
| `thumbnailPanAction` | `'select'` \| `'none'` | Action to perform when panning across thumbnails. |
|
|
82
|
+
| `contextMenuOptions` | `array` (`UIAction[]`) | Options for the context menu that appears on long press. |
|
|
83
|
+
| `initiallySelected` | `string[]` | URIs of items to be marked as selected on initial render. |
|
|
84
|
+
| `style` | `object` (`ViewStyle`) | Style object for the gallery container. |
|
|
85
|
+
| `thumbnailStyle` | `object` | Style object for thumbnail items. Available properties: `aspectRatio`, `borderRadius`, `borderWidth`, `borderColor' |
|
|
86
|
+
| `contentContainerStyle` | `object` | Style object for the content container. Available properties: `padding`, `paddingHorizontal`, `paddingVertical`, `paddingTop`, `paddingBottom`, `paddingLeft`, `paddingRight`, `gap` |
|
|
87
|
+
| `sectionHeaderStyle` | `object` | Style object for section headers when using grouped assets. Available properties: `height` |
|
|
88
|
+
| `fullscreenViewOverlayStyle` | `object` (`ViewStyle`) | Style object for the fullscreen view overlay. |
|
|
89
|
+
| `thumbnailOverlayComponent` | `({uri: string, index: number, selected: boolean }) => Component` | React component to render as overlay on thumbnails. |
|
|
90
|
+
| `fullscreenViewOverlayComponent` | `({uri: string, index: number, selected: boolean, toggleSelection: (selected?: boolean) => void }) => Component` | React component to render as overlay in fullscreen view. `toggleSelection` toggles the selection state of the item if param is undefined, otherwise sets the selection state to the provided value. |
|
|
91
|
+
| `sectionHeaderComponent` | `({index: number}) => Component` | React component to render as section header. |
|
|
92
|
+
| `showMediaTypeIcon` | `boolean` | Whether to show the media type icon on thumbnails (for videos and live photos). Default: `true` |
|
|
93
|
+
|
|
94
|
+
#### Gesture Actions Reference
|
|
95
|
+
|
|
96
|
+
- **select**: Adds/removes the item to/from the selection and fires the `onSelectionChange` event.
|
|
97
|
+
- **open**: Opens the fullscreen viewer with the selected image.
|
|
98
|
+
- **preview**: Opens a preview with context menu with additional options (if provided).
|
|
99
|
+
- **none**: No action is performed, still fires the corresponding event (`onThumbnailPress`, `onThumbnailLongPress`).
|
|
100
|
+
|
|
101
|
+
#### Events
|
|
102
|
+
|
|
103
|
+
```jsx
|
|
104
|
+
<ExpoSimpleGalleryView
|
|
105
|
+
assets={['file:///path/to/image1.jpg', 'ph://asset-id-2']}
|
|
106
|
+
onThumbnailPress={({nativeEvent}) => console.log(nativeEvent.uri)}
|
|
107
|
+
/>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
| Event | Type | Description |
|
|
111
|
+
|-------|------|-------------|
|
|
112
|
+
| `onThumbnailPress` | `(event: NativeSyntheticEvent<{ uri: string, index: number }>) => void` | Fired when a thumbnail is pressed. Returns the URI and index of the pressed thumbnail. |
|
|
113
|
+
| `onThumbnailLongPress` | `(event: NativeSyntheticEvent<{ uri: string, index: number }>) => void` | Fired when a thumbnail is long pressed. Returns the URI and index of the long pressed thumbnail. |
|
|
114
|
+
| `onSelectionChange` | `(event: NativeSyntheticEvent<{ selected: string[] }>) => void` | Fired when selected items change. Returns the array of selected URIs. |
|
|
115
|
+
| `onOverlayPreloadRequested` | `(event: NativeSyntheticEvent<{ range: [number, number] }>) => void` | Fired when overlays need to be preloaded for optimization. Returns the range of indices to preload. |
|
|
116
|
+
| `onSectionHeadersVisible` | `(event: NativeSyntheticEvent<{ sections: number[] }>) => void` | Fired when section headers become visible. Returns the array of visible section indices. |
|
|
117
|
+
| `onPreviewMenuOptionSelected` | `(event: NativeSyntheticEvent<{ uri: string, index: number, optionIndex: number }>) => void` | Fired when an option is selected from the context menu. Returns the URI, index of the item, and index of the selected option. |
|
|
118
|
+
|
|
119
|
+
#### Methods (available via `ref`)
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { type ExpoSimpleGalleryMethods } from 'expo-simple-gallery';
|
|
123
|
+
...
|
|
124
|
+
const galleryRef = useRef<ExpoSimpleGalleryMethods>(null)
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
galleryRef.current?.centerOnIndex(0);
|
|
127
|
+
galleryRef.current?.setSelected(['file:///path/to/image1.jpg', 'ph://asset-id-2']);
|
|
128
|
+
galleryRef.current?.setThumbnailPressAction('open');
|
|
129
|
+
}, []);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Method | Description |
|
|
133
|
+
|--------|-------------|
|
|
134
|
+
| `centerOnIndex(index: number)` | Scrolls to center the item at the specified index. |
|
|
135
|
+
| `setSelected(uris: string[])` | Sets the selected items by their URIs. Useful is you want to manage selection state externally. Triggers `onSelectionChanged` event. |
|
|
136
|
+
| `setThumbnailPressAction(action: string)` | Sets the action performed when a thumbnail is pressed. Preferred over prop if dynamic changes are needed and you don't want to re-render the component. |
|
|
137
|
+
| `setThumbnailLongPressAction(action: string)` | Sets the action performed when a thumbnail is long pressed. Preferred over prop if dynamic changes are needed and you don't want to re-render the component. |
|
|
138
|
+
| `setThumbnailPanAction(action: string)` | Sets the action performed when panning across thumbnails. Preferred over prop if dynamic changes are needed and you don't want to re-render the component. |
|
|
139
|
+
| `setContextMenuOptions(options: array)` | Sets the context menu options. Preferred over prop if dynamic changes are needed and you don't want to re-render the component. |
|
|
140
|
+
| `openImageViewer(index: number)` | Opens the fullscreen image viewer with inmage at the specified index. |
|
|
141
|
+
| `closeImageViewer()` | Closes the fullscreen image viewer. |
|
|
142
|
+
|
|
143
|
+
## Advanced Usage
|
|
144
|
+
|
|
145
|
+
### Grouped Gallery with Sections
|
|
146
|
+
|
|
147
|
+
If you have multiple groups of images, you can pass nested arrays as `assets` to create sections. Each nested array will be displayed as a separate section with a header. Headers can be set using the `sectionHeaderComponent` prop.
|
|
148
|
+
|
|
149
|
+
```jsx
|
|
150
|
+
import { ExpoSimpleGalleryView } from 'expo-simple-gallery';
|
|
151
|
+
|
|
152
|
+
export default function GroupedGallery() {
|
|
153
|
+
// Images grouped by date or category
|
|
154
|
+
const groups = [
|
|
155
|
+
['file:///path/to/group1/image1.jpg', 'file:///path/to/group1/image2.jpg'],
|
|
156
|
+
['file:///path/to/group2/image1.jpg', 'file:///path/to/group2/image2.jpg'],
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<ExpoSimpleGalleryView
|
|
161
|
+
assets={groups}
|
|
162
|
+
columnsCount={3}
|
|
163
|
+
sectionHeaderStyle={{ height: 40 }}
|
|
164
|
+
sectionHeaderComponent={({ index }) => (
|
|
165
|
+
<View style={{ height: 40, justifyContent: 'center', paddingLeft: 10 }}>
|
|
166
|
+
<Text style={{ fontWeight: 'bold' }}>Section {index + 1}</Text>
|
|
167
|
+
</View>
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Customizing Selection Behavior
|
|
175
|
+
|
|
176
|
+
```jsx
|
|
177
|
+
import { useRef } from 'react';
|
|
178
|
+
import { ExpoSimpleGalleryView, ExpoSimpleGalleryMethods } from 'expo-simple-gallery';
|
|
179
|
+
|
|
180
|
+
export default function SelectionExample() {
|
|
181
|
+
const galleryRef = useRef<ExpoSimpleGalleryMethods>(null);
|
|
182
|
+
const assets = ['file:///path/to/image1.jpg', 'ph://asset-id-2'];
|
|
183
|
+
const [selectedUris, setSelectedUris] = useState<string[]>(['file:///path/to/image1.jpg']);
|
|
184
|
+
|
|
185
|
+
const selectAll = useCallback(() => {
|
|
186
|
+
galleryRef.current?.setSelected(assets)
|
|
187
|
+
}, [assets]);
|
|
188
|
+
|
|
189
|
+
const clearSelection = useCallback(() => {
|
|
190
|
+
galleryRef.current?.setSelected([]);
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<>
|
|
195
|
+
<ExpoSimpleGalleryView
|
|
196
|
+
ref={galleryRef}
|
|
197
|
+
assets={assets}
|
|
198
|
+
thumbnailPressAction="select"
|
|
199
|
+
thumbnailLongPressAction="preview"
|
|
200
|
+
thumbnailPanAction="select"
|
|
201
|
+
initiallySelected={selectedUris}
|
|
202
|
+
onSelectionChange={(e) => {
|
|
203
|
+
setSelectedUris(e.nativeEvent.selected);
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
<Button
|
|
209
|
+
title="Select All"
|
|
210
|
+
onPress={selectAll}
|
|
211
|
+
/>
|
|
212
|
+
<Button
|
|
213
|
+
title="Clear Selection"
|
|
214
|
+
onPress={clearSelection}
|
|
215
|
+
/>
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Implementing Custom Context Menu
|
|
222
|
+
|
|
223
|
+
Context menus appear when using `thumbnailLongPressAction="preview"` and provide additional options for interacting with the image.
|
|
224
|
+
|
|
225
|
+
#### Type definitions
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
type UIMenuElementAttribute = 'disabled' | 'destructive' | 'hidden';
|
|
229
|
+
type UIMenuElementState = 'on' | 'off' | 'mixed'; // For toggle actions: checked | unchecked (default) | partially checked
|
|
230
|
+
|
|
231
|
+
export type UIAction = {
|
|
232
|
+
title: string;
|
|
233
|
+
sfSymbol?: string; /** @see https://developer.apple.com/sf-symbols/ */
|
|
234
|
+
action?: ({uri: string, index: number}) => void;
|
|
235
|
+
attributes?: UIMenuElementAttribute[];
|
|
236
|
+
state?: UIMenuElementState;
|
|
237
|
+
};
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```jsx
|
|
241
|
+
<ExpoSimpleGalleryView
|
|
242
|
+
assets={assets}
|
|
243
|
+
contextMenuOptions={[
|
|
244
|
+
{
|
|
245
|
+
title: 'Share',
|
|
246
|
+
sfSymbol: 'square.and.arrow.up', // iOS SF Symbol name
|
|
247
|
+
action: (item) => {
|
|
248
|
+
// Handle share action
|
|
249
|
+
console.log('Share item:', item.uri);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
title: 'Delete',
|
|
254
|
+
sfSymbol: 'trash',
|
|
255
|
+
attributes: ['destructive'], // Special styling
|
|
256
|
+
action: (item) => {
|
|
257
|
+
// Handle delete action
|
|
258
|
+
console.log('Delete item:', item.uri);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
]}
|
|
262
|
+
/>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Custom Overlays
|
|
266
|
+
|
|
267
|
+
You can provide custom overlay components for thumbnails, section headers and the fullscreen viewer:
|
|
268
|
+
|
|
269
|
+
```jsx
|
|
270
|
+
<ExpoSimpleGalleryView
|
|
271
|
+
assets={assets}
|
|
272
|
+
thumbnailOverlayComponent={({ uri, index, selected }) => (
|
|
273
|
+
<View style={{ position: 'absolute', top: 5, right: 5, opacity: selected ? 1 : 0.5 }}>
|
|
274
|
+
<CheckboxIcon checked={selected} />
|
|
275
|
+
</View>
|
|
276
|
+
)}
|
|
277
|
+
fullscreenViewOverlayComponent={({ uri, index }) => (
|
|
278
|
+
/* Pay attention to not blocking touch events on the image view */
|
|
279
|
+
<View style={{ position: 'absolute', top: 20, left: 0, right: 0 }}>
|
|
280
|
+
<Text style={{ color: 'white', textAlign: 'center' }}>
|
|
281
|
+
URI: {uri} | Index: {index}
|
|
282
|
+
</Text>
|
|
283
|
+
</View>
|
|
284
|
+
)}
|
|
285
|
+
sectionHeaderComponent={({ index }) => (
|
|
286
|
+
<View style={{ height: 40, justifyContent: 'center', paddingLeft: 10 }}>
|
|
287
|
+
<Text style={{ fontWeight: 'bold' }}>Section {index + 1}</Text>
|
|
288
|
+
</View>
|
|
289
|
+
)}
|
|
290
|
+
/>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Contributing
|
|
294
|
+
|
|
295
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.simplegallery'
|
|
4
|
+
version = '0.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
+
def useManagedAndroidSdkVersions = false
|
|
16
|
+
if (useManagedAndroidSdkVersions) {
|
|
17
|
+
useDefaultAndroidSdkVersions()
|
|
18
|
+
} else {
|
|
19
|
+
buildscript {
|
|
20
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
+
ext.safeExtGet = { prop, fallback ->
|
|
22
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
project.android {
|
|
26
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
29
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "expo.modules.simplegallery"
|
|
36
|
+
defaultConfig {
|
|
37
|
+
versionCode 1
|
|
38
|
+
versionName "0.1.0"
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package expo.modules.simplegallery
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
import java.net.URL
|
|
6
|
+
|
|
7
|
+
class ExpoSimpleGalleryModule : Module() {
|
|
8
|
+
// Each module class must implement the definition function. The definition consists of components
|
|
9
|
+
// that describes the module's functionality and behavior.
|
|
10
|
+
// See https://docs.expo.dev/modules/module-api for more details about available components.
|
|
11
|
+
override fun definition() = ModuleDefinition {
|
|
12
|
+
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
|
|
13
|
+
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
|
|
14
|
+
// The module will be accessible from `requireNativeModule('ExpoSimpleGallery')` in JavaScript.
|
|
15
|
+
Name("ExpoSimpleGallery")
|
|
16
|
+
|
|
17
|
+
// Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
|
|
18
|
+
Constants(
|
|
19
|
+
"PI" to Math.PI
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// Defines event names that the module can send to JavaScript.
|
|
23
|
+
Events("onChange")
|
|
24
|
+
|
|
25
|
+
// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
|
|
26
|
+
Function("hello") {
|
|
27
|
+
"Hello world! 👋"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Defines a JavaScript function that always returns a Promise and whose native code
|
|
31
|
+
// is by default dispatched on the different thread than the JavaScript runtime runs on.
|
|
32
|
+
AsyncFunction("setValueAsync") { value: String ->
|
|
33
|
+
// Send an event to JavaScript.
|
|
34
|
+
sendEvent("onChange", mapOf(
|
|
35
|
+
"value" to value
|
|
36
|
+
))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Enables the module to be used as a native view. Definition components that are accepted as part of
|
|
40
|
+
// the view definition: Prop, Events.
|
|
41
|
+
View(ExpoSimpleGalleryView::class) {
|
|
42
|
+
// Defines a setter for the `url` prop.
|
|
43
|
+
Prop("url") { view: ExpoSimpleGalleryView, url: URL ->
|
|
44
|
+
view.webView.loadUrl(url.toString())
|
|
45
|
+
}
|
|
46
|
+
// Defines an event that the view can send to JavaScript.
|
|
47
|
+
Events("onLoad")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package expo.modules.simplegallery
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.webkit.WebView
|
|
5
|
+
import android.webkit.WebViewClient
|
|
6
|
+
import expo.modules.kotlin.AppContext
|
|
7
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
8
|
+
import expo.modules.kotlin.views.ExpoView
|
|
9
|
+
|
|
10
|
+
class ExpoSimpleGalleryView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
11
|
+
// Creates and initializes an event dispatcher for the `onLoad` event.
|
|
12
|
+
// The name of the event is inferred from the value and needs to match the event name defined in the module.
|
|
13
|
+
private val onLoad by EventDispatcher()
|
|
14
|
+
|
|
15
|
+
// Defines a WebView that will be used as the root subview.
|
|
16
|
+
internal val webView = WebView(context).apply {
|
|
17
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
18
|
+
webViewClient = object : WebViewClient() {
|
|
19
|
+
override fun onPageFinished(view: WebView, url: String) {
|
|
20
|
+
// Sends an event to JavaScript. Triggers a callback defined on the view component in JavaScript.
|
|
21
|
+
onLoad(mapOf("url" to url))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
init {
|
|
27
|
+
// Adds the WebView to the view hierarchy.
|
|
28
|
+
addView(webView)
|
|
29
|
+
}
|
|
30
|
+
}
|
package/biome.json
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"ignore": []
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"useEditorconfig": true,
|
|
15
|
+
"formatWithErrors": true,
|
|
16
|
+
"indentStyle": "space",
|
|
17
|
+
"indentWidth": 2,
|
|
18
|
+
"lineEnding": "lf",
|
|
19
|
+
"lineWidth": 80,
|
|
20
|
+
"attributePosition": "auto",
|
|
21
|
+
"bracketSpacing": true
|
|
22
|
+
},
|
|
23
|
+
"organizeImports": {
|
|
24
|
+
"enabled": true
|
|
25
|
+
},
|
|
26
|
+
"linter": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"rules": {
|
|
29
|
+
"recommended": true,
|
|
30
|
+
"complexity": {
|
|
31
|
+
"noWith": "warn"
|
|
32
|
+
},
|
|
33
|
+
"correctness": {
|
|
34
|
+
"noEmptyCharacterClassInRegex": "warn",
|
|
35
|
+
"noEmptyPattern": "warn",
|
|
36
|
+
"noUndeclaredVariables": "error",
|
|
37
|
+
"noUnreachable": "warn",
|
|
38
|
+
"noUnusedImports": "error",
|
|
39
|
+
"noUnusedLabels": "warn",
|
|
40
|
+
"noUnusedVariables": "warn",
|
|
41
|
+
"useExhaustiveDependencies": "warn",
|
|
42
|
+
"useHookAtTopLevel": "error",
|
|
43
|
+
"useIsNan": "error"
|
|
44
|
+
},
|
|
45
|
+
"security": {
|
|
46
|
+
"noDangerouslySetInnerHtmlWithChildren": "warn"
|
|
47
|
+
},
|
|
48
|
+
"style": {
|
|
49
|
+
"useBlockStatements": "off",
|
|
50
|
+
"useConst": "error",
|
|
51
|
+
"useFilenamingConvention": {
|
|
52
|
+
"level": "error",
|
|
53
|
+
"options": {
|
|
54
|
+
"requireAscii": true,
|
|
55
|
+
"filenameCases": [
|
|
56
|
+
"kebab-case"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"suspicious": {
|
|
62
|
+
"noDoubleEquals": "warn",
|
|
63
|
+
"noDuplicateCase": "error",
|
|
64
|
+
"noDuplicateClassMembers": "error",
|
|
65
|
+
"noDuplicateJsxProps": "error",
|
|
66
|
+
"noDuplicateObjectKeys": "error",
|
|
67
|
+
"noDuplicateParameters": "error",
|
|
68
|
+
"noRedeclare": "warn",
|
|
69
|
+
"noUnsafeNegation": "warn",
|
|
70
|
+
"useValidTypeof": "error"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"ignore": [
|
|
74
|
+
"android/app/build",
|
|
75
|
+
"**/credential-provider-plugin/"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"javascript": {
|
|
79
|
+
"formatter": {
|
|
80
|
+
"jsxQuoteStyle": "double",
|
|
81
|
+
"quoteProperties": "asNeeded",
|
|
82
|
+
"trailingCommas": "es5",
|
|
83
|
+
"semicolons": "always",
|
|
84
|
+
"arrowParentheses": "always",
|
|
85
|
+
"bracketSameLine": false,
|
|
86
|
+
"quoteStyle": "single",
|
|
87
|
+
"attributePosition": "auto",
|
|
88
|
+
"bracketSpacing": true
|
|
89
|
+
},
|
|
90
|
+
"globals": [
|
|
91
|
+
"console",
|
|
92
|
+
"__DEV__",
|
|
93
|
+
"shared-node-browser"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"overrides": [
|
|
97
|
+
{
|
|
98
|
+
"include": [
|
|
99
|
+
"*.web.*"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"include": [
|
|
104
|
+
"*.d.ts"
|
|
105
|
+
],
|
|
106
|
+
"linter": {
|
|
107
|
+
"rules": {}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"include": [
|
|
112
|
+
"*.js",
|
|
113
|
+
"*.jsx"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"include": [
|
|
118
|
+
"*.ts",
|
|
119
|
+
"*.tsx",
|
|
120
|
+
"*.d.ts"
|
|
121
|
+
],
|
|
122
|
+
"linter": {
|
|
123
|
+
"rules": {
|
|
124
|
+
"complexity": {
|
|
125
|
+
"noBannedTypes": "error",
|
|
126
|
+
"noUselessConstructor": "warn"
|
|
127
|
+
},
|
|
128
|
+
"correctness": {
|
|
129
|
+
"noUndeclaredVariables": "off",
|
|
130
|
+
"noUnusedVariables": "warn"
|
|
131
|
+
},
|
|
132
|
+
"style": {
|
|
133
|
+
"useConsistentArrayType": {
|
|
134
|
+
"level": "warn",
|
|
135
|
+
"options": {
|
|
136
|
+
"syntax": "shorthand"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"suspicious": {
|
|
141
|
+
"noDuplicateClassMembers": "error",
|
|
142
|
+
"noExtraNonNullAssertion": "warn",
|
|
143
|
+
"noRedeclare": "warn"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"include": [
|
|
150
|
+
"*.web.*"
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"include": [
|
|
155
|
+
"*.d.ts"
|
|
156
|
+
],
|
|
157
|
+
"linter": {
|
|
158
|
+
"rules": {}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"include": [
|
|
163
|
+
"*.js",
|
|
164
|
+
"*.jsx"
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"include": [
|
|
169
|
+
"*.ts",
|
|
170
|
+
"*.tsx",
|
|
171
|
+
"*.d.ts"
|
|
172
|
+
],
|
|
173
|
+
"linter": {
|
|
174
|
+
"rules": {
|
|
175
|
+
"complexity": {
|
|
176
|
+
"noBannedTypes": "error",
|
|
177
|
+
"noUselessConstructor": "warn"
|
|
178
|
+
},
|
|
179
|
+
"correctness": {
|
|
180
|
+
"noUndeclaredVariables": "off",
|
|
181
|
+
"noUnusedVariables": "warn"
|
|
182
|
+
},
|
|
183
|
+
"style": {
|
|
184
|
+
"useConsistentArrayType": {
|
|
185
|
+
"level": "warn",
|
|
186
|
+
"options": {
|
|
187
|
+
"syntax": "shorthand"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"suspicious": {
|
|
192
|
+
"noDuplicateClassMembers": "error",
|
|
193
|
+
"noExtraNonNullAssertion": "warn",
|
|
194
|
+
"noRedeclare": "warn"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|