@kesbyte/capacitor-exif-gallery 1.0.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/CapacitorExifGallery.podspec +17 -0
- package/LICENSE +8 -0
- package/Package.swift +28 -0
- package/README.md +813 -0
- package/dist/esm/ExifGalleryImpl.d.ts +176 -0
- package/dist/esm/ExifGalleryImpl.js +295 -0
- package/dist/esm/ExifGalleryImpl.js.map +1 -0
- package/dist/esm/FilterValidator.d.ts +126 -0
- package/dist/esm/FilterValidator.js +274 -0
- package/dist/esm/FilterValidator.js.map +1 -0
- package/dist/esm/PluginState.d.ts +128 -0
- package/dist/esm/PluginState.js +166 -0
- package/dist/esm/PluginState.js.map +1 -0
- package/dist/esm/PolylineDecoder.d.ts +23 -0
- package/dist/esm/PolylineDecoder.js +34 -0
- package/dist/esm/PolylineDecoder.js.map +1 -0
- package/dist/esm/TranslationLoader.d.ts +140 -0
- package/dist/esm/TranslationLoader.js +218 -0
- package/dist/esm/TranslationLoader.js.map +1 -0
- package/dist/esm/definitions.d.ts +539 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/errors.d.ts +252 -0
- package/dist/esm/errors.js +276 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +40 -0
- package/dist/esm/index.js +42 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/translations/de.json +20 -0
- package/dist/esm/translations/en.json +20 -0
- package/dist/esm/translations/es.json +20 -0
- package/dist/esm/translations/fr.json +20 -0
- package/dist/esm/web.d.ts +6 -0
- package/dist/esm/web.js +14 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +1820 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +1823 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +121 -0
package/README.md
ADDED
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
# Exif Gallery for Capacitor
|
|
2
|
+
|
|
3
|
+
**The intelligent photo picker for location-aware and time-based image selection.**
|
|
4
|
+
|
|
5
|
+
Turn massive photo libraries into precisely filtered galleries. Exif Gallery for Capacitor enables your iOS and Android apps to filter images by GPS location, travel routes, and time ranges—all using EXIF metadata. Perfect for travel apps, photo journals, event documentation, and any application that needs smart image selection based on where and when photos were taken.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@kesbyte/capacitor-exif-gallery)
|
|
8
|
+
[](https://www.npmjs.com/package/@kesbyte/capacitor-exif-gallery)
|
|
9
|
+
[](https://exif-gallery.kesbyte-digital.com)
|
|
10
|
+
|
|
11
|
+
## Sample App
|
|
12
|
+
|
|
13
|
+
A complete working example is available in the source repository at `/sample-app`. This Ionic Angular application demonstrates:
|
|
14
|
+
|
|
15
|
+
- Interactive location and time filtering
|
|
16
|
+
- Route-based image filtering using polylines
|
|
17
|
+
- EXIF metadata display
|
|
18
|
+
- Multi-language UI
|
|
19
|
+
- Real-world integration patterns
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="docs/images/01_onboarding.png" width="250" alt="Onboarding and welcome screen" />
|
|
23
|
+
<img src="docs/images/02_filters.png" width="250" alt="Location and time filters" />
|
|
24
|
+
<img src="docs/images/03_code.png" width="250" alt="Code examples and integration" />
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
### Running the Sample App
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/KesByte-Digital/capacitor-exif-gallery.git
|
|
31
|
+
cd capacitor-exif-gallery/sample-app
|
|
32
|
+
npm install
|
|
33
|
+
npx cap sync
|
|
34
|
+
|
|
35
|
+
# Run on iOS
|
|
36
|
+
ionic cap run ios
|
|
37
|
+
|
|
38
|
+
# Run on Android
|
|
39
|
+
ionic cap run android
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Note:** The sample app is in the source repository and demonstrates plugin functionality. It is not included in the npm package.
|
|
43
|
+
|
|
44
|
+
## Key Features
|
|
45
|
+
|
|
46
|
+
- **Location-based filtering** - Filter images within a geographic radius using GPS EXIF data
|
|
47
|
+
- **Encoded polyline support** - Use Google Maps polyline format for route-based filtering
|
|
48
|
+
- **Time range filtering** - Select images taken within a specific date/time range
|
|
49
|
+
- **Combined filters** - Apply location AND time filters simultaneously
|
|
50
|
+
- **EXIF metadata extraction** - Access GPS coordinates and timestamps from selected images
|
|
51
|
+
- **Intelligent fallback** - Automatically switches to time-based filtering if location results are too few
|
|
52
|
+
- **Multi-language UI** - Built-in support for English, German, French, and Spanish
|
|
53
|
+
- **Automatic permissions** - Handles photo library and location permission requests seamlessly
|
|
54
|
+
- **Custom UI text** - Override default labels and messages in any language
|
|
55
|
+
|
|
56
|
+
## Platform Support
|
|
57
|
+
|
|
58
|
+
- **iOS:** 15.0+ (Swift)
|
|
59
|
+
- **Android:** 7.0+ (API 24+, Kotlin)
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
### 1. Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @kesbyte/capacitor-exif-gallery
|
|
67
|
+
npx cap sync
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Configure License Key (Production Only)
|
|
71
|
+
|
|
72
|
+
**⚠️ Required for production builds only** - Debug builds work without a license for testing.
|
|
73
|
+
|
|
74
|
+
Purchase your license key at **[exif-gallery.kesbyte-digital.com](https://exif-gallery.kesbyte-digital.com)**, then add it to your app configuration:
|
|
75
|
+
|
|
76
|
+
#### iOS Configuration (Info.plist)
|
|
77
|
+
|
|
78
|
+
Open your `ios/App/App/Info.plist` and add:
|
|
79
|
+
|
|
80
|
+
```xml
|
|
81
|
+
<key>KBExifGalleryLicense</key>
|
|
82
|
+
<string>YOUR_LICENSE_KEY_HERE</string>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Android Configuration (AndroidManifest.xml)
|
|
86
|
+
|
|
87
|
+
Open your `android/app/src/main/AndroidManifest.xml` and add inside the `<application>` tag:
|
|
88
|
+
|
|
89
|
+
```xml
|
|
90
|
+
<application>
|
|
91
|
+
<!-- Other configuration... -->
|
|
92
|
+
|
|
93
|
+
<meta-data
|
|
94
|
+
android:name="com.kesbytedigital.exifgallery.LICENSE_KEY"
|
|
95
|
+
android:value="YOUR_LICENSE_KEY_HERE" />
|
|
96
|
+
|
|
97
|
+
</application>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Validation Behavior
|
|
101
|
+
|
|
102
|
+
- **Debug Builds:** License validation is skipped - full functionality available for testing
|
|
103
|
+
- **Production Builds:** License is validated when `pick()` is called
|
|
104
|
+
- ✅ Valid license: Gallery opens normally
|
|
105
|
+
- ❌ Invalid/missing license: `pick()` throws an error immediately
|
|
106
|
+
- Error codes: `LICENSE_MISSING`, `LICENSE_INVALID`, `LICENSE_BUNDLE_MISMATCH`
|
|
107
|
+
|
|
108
|
+
**Note:** The license is validated at the moment you call `pick()`, not during `initialize()`. This ensures fast app startup while still enforcing licensing before the plugin is actually used.
|
|
109
|
+
|
|
110
|
+
#### Troubleshooting
|
|
111
|
+
|
|
112
|
+
**"License key not found" error:**
|
|
113
|
+
- Verify the key name matches exactly: `KBExifGalleryLicense` (iOS) or `com.kesbytedigital.exifgallery.LICENSE_KEY` (Android)
|
|
114
|
+
- Ensure you ran `npx cap sync` after adding the license
|
|
115
|
+
- Check that the license key has no extra whitespace or line breaks
|
|
116
|
+
|
|
117
|
+
**"Bundle ID mismatch" error:**
|
|
118
|
+
- Your license is tied to a specific bundle ID (e.g., `com.example.myapp`)
|
|
119
|
+
- Verify your app's bundle ID matches the license
|
|
120
|
+
- iOS: Check `CFBundleIdentifier` in Info.plist
|
|
121
|
+
- Android: Check `applicationId` in `build.gradle`
|
|
122
|
+
|
|
123
|
+
### 3. Configure Permissions
|
|
124
|
+
|
|
125
|
+
#### iOS Permissions (Info.plist)
|
|
126
|
+
|
|
127
|
+
Add the following to your `ios/App/App/Info.plist`:
|
|
128
|
+
|
|
129
|
+
```xml
|
|
130
|
+
<key>NSPhotoLibraryUsageDescription</key>
|
|
131
|
+
<string>This app needs access to your photo library to filter and select images</string>
|
|
132
|
+
|
|
133
|
+
<key>NSLocationWhenInUseUsageDescription</key>
|
|
134
|
+
<string>This app uses your location to enhance image filtering capabilities</string>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Permission Details:**
|
|
138
|
+
- `NSPhotoLibraryUsageDescription`: Required for reading photos from the library
|
|
139
|
+
- `NSLocationWhenInUseUsageDescription`: Required for location-based filtering (only when app is in use)
|
|
140
|
+
|
|
141
|
+
#### Android Permissions (AndroidManifest.xml)
|
|
142
|
+
|
|
143
|
+
Add the following to your `android/app/src/main/AndroidManifest.xml`:
|
|
144
|
+
|
|
145
|
+
```xml
|
|
146
|
+
<!-- Photo Library Permissions -->
|
|
147
|
+
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
|
148
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
|
149
|
+
|
|
150
|
+
<!-- Location Permission -->
|
|
151
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Permission Details:**
|
|
155
|
+
- `READ_MEDIA_IMAGES`: For Android 13+ (API 33+), granular image access
|
|
156
|
+
- `READ_EXTERNAL_STORAGE`: For Android 12 and below (API ≤32), legacy storage access
|
|
157
|
+
- `ACCESS_FINE_LOCATION`: Required for location-based filtering
|
|
158
|
+
|
|
159
|
+
### 4. Initialize (once at app startup)
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { ExifGallery } from '@kesbyte/capacitor-exif-gallery';
|
|
163
|
+
|
|
164
|
+
// During app initialization
|
|
165
|
+
await ExifGallery.initialize();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 5. Open gallery with filters
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const result = await ExifGallery.pick({
|
|
172
|
+
filter: {
|
|
173
|
+
location: {
|
|
174
|
+
coordinates: [{ lat: 52.52, lng: 13.40 }],
|
|
175
|
+
radius: 5000 // 5km radius
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Access selected images
|
|
181
|
+
if (!result.cancelled) {
|
|
182
|
+
result.images.forEach(image => {
|
|
183
|
+
console.log(`Selected: ${image.uri}`);
|
|
184
|
+
console.log(`Location: ${image.exif?.lat}, ${image.exif?.lng}`);
|
|
185
|
+
console.log(`Timestamp: ${image.exif?.timestamp}`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Features in Detail
|
|
191
|
+
|
|
192
|
+
### Location-Based Filtering
|
|
193
|
+
|
|
194
|
+
Filter images by geographic location with two flexible input formats.
|
|
195
|
+
|
|
196
|
+
**Using coordinates:**
|
|
197
|
+
```typescript
|
|
198
|
+
const result = await ExifGallery.pick({
|
|
199
|
+
filter: {
|
|
200
|
+
location: {
|
|
201
|
+
coordinates: [
|
|
202
|
+
{ lat: 52.5163, lng: 13.3777 }, // Berlin
|
|
203
|
+
{ lat: 48.1374, lng: 11.5755 } // Munich
|
|
204
|
+
],
|
|
205
|
+
radius: 10000 // 10km around each point
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Using encoded polylines (Google Maps format):**
|
|
212
|
+
```typescript
|
|
213
|
+
// Get polyline from Google Directions API
|
|
214
|
+
const directionsResult = await fetch(
|
|
215
|
+
'https://maps.googleapis.com/maps/api/directions/json?' +
|
|
216
|
+
'origin=Berlin&destination=Munich&key=YOUR_API_KEY'
|
|
217
|
+
);
|
|
218
|
+
const route = directionsResult.routes[0].overview_polyline.points;
|
|
219
|
+
|
|
220
|
+
const result = await ExifGallery.pick({
|
|
221
|
+
filter: {
|
|
222
|
+
location: {
|
|
223
|
+
polyline: route, // Encoded string like "_p~iF~ps|U_ulLnnqC"
|
|
224
|
+
radius: 5000 // 5km corridor around route
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Why encoded polylines?**
|
|
231
|
+
- 92% smaller payload (5KB → 400 bytes for 100 points)
|
|
232
|
+
- URL-safe format
|
|
233
|
+
- Direct compatibility with Google Maps, Mapbox, OpenStreetMap
|
|
234
|
+
|
|
235
|
+
### Time Range Filtering
|
|
236
|
+
|
|
237
|
+
Select images taken within a specific date/time window.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const result = await ExifGallery.pick({
|
|
241
|
+
filter: {
|
|
242
|
+
timeRange: {
|
|
243
|
+
start: new Date('2025-01-01'),
|
|
244
|
+
end: new Date('2025-12-31')
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Combined Filters (Location AND Time)
|
|
251
|
+
|
|
252
|
+
Apply multiple filters simultaneously - images must match ALL criteria.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const result = await ExifGallery.pick({
|
|
256
|
+
filter: {
|
|
257
|
+
location: {
|
|
258
|
+
coordinates: [{ lat: 52.52, lng: 13.40 }],
|
|
259
|
+
radius: 5000
|
|
260
|
+
},
|
|
261
|
+
timeRange: {
|
|
262
|
+
start: new Date('2025-01-01'),
|
|
263
|
+
end: new Date('2025-12-31')
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### EXIF Metadata Extraction
|
|
270
|
+
|
|
271
|
+
Access GPS and timestamp information from selected images.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
const result = await ExifGallery.pick();
|
|
275
|
+
|
|
276
|
+
result.images.forEach(image => {
|
|
277
|
+
const exif = image.exif;
|
|
278
|
+
if (exif) {
|
|
279
|
+
console.log(`Latitude: ${exif.lat}`);
|
|
280
|
+
console.log(`Longitude: ${exif.lng}`);
|
|
281
|
+
console.log(`Taken at: ${exif.timestamp}`);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Custom UI Text
|
|
287
|
+
|
|
288
|
+
Override default translations with custom text in any language.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
await ExifGallery.initialize({
|
|
292
|
+
locale: 'en',
|
|
293
|
+
customTexts: {
|
|
294
|
+
galleryTitle: 'Select Your Photos',
|
|
295
|
+
selectButton: 'Pick Images',
|
|
296
|
+
cancelButton: 'Close',
|
|
297
|
+
filterDialogTitle: 'Advanced Filters',
|
|
298
|
+
emptyMessage: 'No photos found matching your criteria'
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Automatic Permission Handling
|
|
304
|
+
|
|
305
|
+
The plugin requests permissions just-in-time (when `pick()` is called) by default. To request permissions upfront during onboarding:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
await ExifGallery.initialize({
|
|
309
|
+
requestPermissionsUpfront: true
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Usage Examples
|
|
314
|
+
|
|
315
|
+
### Basic Gallery
|
|
316
|
+
|
|
317
|
+
Open gallery without filters - user can manually select images.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import { ExifGallery } from '@kesbyte/capacitor-exif-gallery';
|
|
321
|
+
|
|
322
|
+
const result = await ExifGallery.pick();
|
|
323
|
+
|
|
324
|
+
if (!result.cancelled) {
|
|
325
|
+
const fileUris = result.images.map(img => img.uri);
|
|
326
|
+
console.log(`Selected ${fileUris.length} images`);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Location Filter with Fallback
|
|
331
|
+
|
|
332
|
+
Automatically switches to time-based filtering if too few location results.
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const result = await ExifGallery.pick({
|
|
336
|
+
filter: {
|
|
337
|
+
location: {
|
|
338
|
+
coordinates: [{ lat: 52.52, lng: 13.40 }],
|
|
339
|
+
radius: 1000 // 1km - strict radius
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
fallbackThreshold: 5 // Switch to time filter if < 5 images found
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Pre-configured Filters (Read-only)
|
|
347
|
+
|
|
348
|
+
Enforce specific filters without allowing user adjustment.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
const result = await ExifGallery.pick({
|
|
352
|
+
filter: {
|
|
353
|
+
location: {
|
|
354
|
+
coordinates: [{ lat: 48.85, lng: 2.29 }], // Paris
|
|
355
|
+
radius: 20000
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
allowManualAdjustment: false // User cannot change filters
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Interactive Filter UI
|
|
363
|
+
|
|
364
|
+
Allow users to adjust filters in the gallery interface.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
const result = await ExifGallery.pick({
|
|
368
|
+
filter: {
|
|
369
|
+
timeRange: {
|
|
370
|
+
start: new Date('2025-01-01'),
|
|
371
|
+
end: new Date('2025-12-31')
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
allowManualAdjustment: true // Default - user can adjust
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Multi-language Support
|
|
379
|
+
|
|
380
|
+
Switch UI language at runtime.
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// German UI
|
|
384
|
+
await ExifGallery.initialize({ locale: 'de' });
|
|
385
|
+
const resultDE = await ExifGallery.pick();
|
|
386
|
+
|
|
387
|
+
// French UI
|
|
388
|
+
await ExifGallery.initialize({ locale: 'fr' });
|
|
389
|
+
const resultFR = await ExifGallery.pick();
|
|
390
|
+
|
|
391
|
+
// Spanish UI
|
|
392
|
+
await ExifGallery.initialize({ locale: 'es' });
|
|
393
|
+
const resultES = await ExifGallery.pick();
|
|
394
|
+
|
|
395
|
+
// Fallback to English
|
|
396
|
+
await ExifGallery.initialize({ locale: 'en' });
|
|
397
|
+
const resultEN = await ExifGallery.pick();
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Advanced: Route-Based Filtering
|
|
401
|
+
|
|
402
|
+
Filter images along a recorded hiking or travel route.
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
// Your recorded GPS points from a Strava activity or hiking app
|
|
406
|
+
const routePolyline = "_p~iF~ps|U_ulLnnqC_seK`xwE"; // Encoded polyline
|
|
407
|
+
|
|
408
|
+
const result = await ExifGallery.pick({
|
|
409
|
+
filter: {
|
|
410
|
+
location: {
|
|
411
|
+
polyline: routePolyline,
|
|
412
|
+
radius: 2000 // 2km corridor on each side of route
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
console.log(`Found ${result.images.length} photos along your route`);
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## API Documentation
|
|
421
|
+
|
|
422
|
+
### Main Methods
|
|
423
|
+
|
|
424
|
+
#### initialize(config?: InitConfig)
|
|
425
|
+
|
|
426
|
+
Initialize the plugin with optional configuration.
|
|
427
|
+
|
|
428
|
+
Must be called before `pick()`. Can be called multiple times to update configuration.
|
|
429
|
+
|
|
430
|
+
**Default behavior (no config):**
|
|
431
|
+
- Detects system language automatically
|
|
432
|
+
- Uses built-in English/German/French/Spanish translations
|
|
433
|
+
- Requests permissions just-in-time (when pick() is called)
|
|
434
|
+
|
|
435
|
+
**Parameters:**
|
|
436
|
+
- `config` (optional) - `InitConfig` object
|
|
437
|
+
|
|
438
|
+
**Example:**
|
|
439
|
+
```typescript
|
|
440
|
+
await ExifGallery.initialize({
|
|
441
|
+
locale: 'de',
|
|
442
|
+
requestPermissionsUpfront: true
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
#### pick(options?: PickOptions)
|
|
449
|
+
|
|
450
|
+
Open native gallery with optional filters and return selected images.
|
|
451
|
+
|
|
452
|
+
Must call `initialize()` first, otherwise throws `initialization_required` error.
|
|
453
|
+
|
|
454
|
+
**Filter behavior:**
|
|
455
|
+
- If filter provided: Gallery opens with pre-configured filters
|
|
456
|
+
- If no filter: User can manually set filters in gallery UI
|
|
457
|
+
- Auto-fallback: If location filter returns fewer images than `fallbackThreshold`, falls back to time filter
|
|
458
|
+
|
|
459
|
+
**Parameters:**
|
|
460
|
+
- `options` (optional) - `PickOptions` object
|
|
461
|
+
|
|
462
|
+
**Returns:** `Promise<PickResult>`
|
|
463
|
+
|
|
464
|
+
**Example:**
|
|
465
|
+
```typescript
|
|
466
|
+
const result = await ExifGallery.pick({
|
|
467
|
+
filter: {
|
|
468
|
+
location: {
|
|
469
|
+
coordinates: [{ lat: 52.52, lng: 13.40 }],
|
|
470
|
+
radius: 5000
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
### Interfaces
|
|
479
|
+
|
|
480
|
+
#### InitConfig
|
|
481
|
+
|
|
482
|
+
Plugin initialization configuration. All properties are optional.
|
|
483
|
+
|
|
484
|
+
| Property | Type | Description |
|
|
485
|
+
|----------|------|-------------|
|
|
486
|
+
| **locale** | `'en' \| 'de' \| 'fr' \| 'es'` | Optional locale for UI text. Auto-detects system language if not provided. Falls back to English if system language not supported. |
|
|
487
|
+
| **customTexts** | `Partial<TranslationSet>` | Optional custom text overrides. Merges with default translations. Override only the keys you need. |
|
|
488
|
+
| **requestPermissionsUpfront** | `boolean` | Request photo library permissions during initialization. Default: `false` (permissions requested just-in-time when `pick()` is called). Set to `true` for upfront onboarding permission requests. |
|
|
489
|
+
|
|
490
|
+
**Example:**
|
|
491
|
+
```typescript
|
|
492
|
+
const config: InitConfig = {
|
|
493
|
+
locale: 'de',
|
|
494
|
+
customTexts: {
|
|
495
|
+
galleryTitle: 'Fotos auswählen',
|
|
496
|
+
selectButton: 'Auswählen'
|
|
497
|
+
},
|
|
498
|
+
requestPermissionsUpfront: true
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
await ExifGallery.initialize(config);
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
#### PickOptions
|
|
507
|
+
|
|
508
|
+
Options for the `pick()` method.
|
|
509
|
+
|
|
510
|
+
| Property | Type | Description |
|
|
511
|
+
|----------|------|-------------|
|
|
512
|
+
| **filter** | `FilterConfig` | Optional filter configuration to pre-configure the gallery. If not provided, user can manually set filters in the gallery UI. |
|
|
513
|
+
| **fallbackThreshold** | `number` | Minimum number of results before automatic fallback to time filter. If location filter returns fewer images, plugin switches to time-based filtering. Default: `5` |
|
|
514
|
+
| **allowManualAdjustment** | `boolean` | Allow user to manually adjust filters in the gallery UI. Default: `true`. Set to `false` to enforce the provided filter configuration. |
|
|
515
|
+
|
|
516
|
+
**Example:**
|
|
517
|
+
```typescript
|
|
518
|
+
const options: PickOptions = {
|
|
519
|
+
filter: {
|
|
520
|
+
location: {
|
|
521
|
+
coordinates: [{ lat: 52.52, lng: 13.40 }],
|
|
522
|
+
radius: 5000
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
fallbackThreshold: 10,
|
|
526
|
+
allowManualAdjustment: true
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const result = await ExifGallery.pick(options);
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
#### PickResult
|
|
535
|
+
|
|
536
|
+
Result from the `pick()` method.
|
|
537
|
+
|
|
538
|
+
| Property | Type | Description |
|
|
539
|
+
|----------|------|-------------|
|
|
540
|
+
| **images** | `ImageResult[]` | Array of selected images. Empty if user cancelled or no images matched filters. |
|
|
541
|
+
| **cancelled** | `boolean` | `true` if user explicitly cancelled. `false` if user confirmed selection (even if empty). |
|
|
542
|
+
|
|
543
|
+
**Example:**
|
|
544
|
+
```typescript
|
|
545
|
+
const result = await ExifGallery.pick();
|
|
546
|
+
|
|
547
|
+
if (result.cancelled) {
|
|
548
|
+
console.log('User cancelled');
|
|
549
|
+
} else {
|
|
550
|
+
console.log(`Selected ${result.images.length} images`);
|
|
551
|
+
result.images.forEach(img => {
|
|
552
|
+
console.log(`URI: ${img.uri}`);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
#### ImageResult
|
|
560
|
+
|
|
561
|
+
Single image result from `pick()`.
|
|
562
|
+
|
|
563
|
+
| Property | Type | Description |
|
|
564
|
+
|----------|------|-------------|
|
|
565
|
+
| **uri** | `string` | File URI for the image (`file://` path). Can be used to display or upload the image. |
|
|
566
|
+
| **exif** | `ImageExif \| undefined` | EXIF metadata if available. May be undefined if image has no EXIF data. |
|
|
567
|
+
| **filteredBy** | `'time' \| 'location'` | How this image was filtered: `'location'` = matched location filter, `'time'` = matched time filter (or fallback from location filter). |
|
|
568
|
+
|
|
569
|
+
**Example:**
|
|
570
|
+
```typescript
|
|
571
|
+
result.images.forEach(image => {
|
|
572
|
+
console.log(`URI: ${image.uri}`);
|
|
573
|
+
console.log(`Filtered by: ${image.filteredBy}`);
|
|
574
|
+
if (image.exif) {
|
|
575
|
+
console.log(`Location: ${image.exif.lat}, ${image.exif.lng}`);
|
|
576
|
+
console.log(`Date: ${image.exif.timestamp}`);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
#### ImageExif
|
|
584
|
+
|
|
585
|
+
EXIF metadata extracted from an image.
|
|
586
|
+
|
|
587
|
+
| Property | Type | Description |
|
|
588
|
+
|----------|------|-------------|
|
|
589
|
+
| **lat** | `number \| undefined` | Latitude from GPS EXIF data (if available). |
|
|
590
|
+
| **lng** | `number \| undefined` | Longitude from GPS EXIF data (if available). |
|
|
591
|
+
| **timestamp** | `Date \| undefined` | Timestamp from EXIF DateTimeOriginal (if available). |
|
|
592
|
+
|
|
593
|
+
**Example:**
|
|
594
|
+
```typescript
|
|
595
|
+
const exif = image.exif;
|
|
596
|
+
if (exif?.lat && exif?.lng) {
|
|
597
|
+
console.log(`Coordinates: ${exif.lat}, ${exif.lng}`);
|
|
598
|
+
}
|
|
599
|
+
if (exif?.timestamp) {
|
|
600
|
+
console.log(`Taken: ${exif.timestamp.toLocaleDateString()}`);
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
#### FilterConfig
|
|
607
|
+
|
|
608
|
+
Combined filter configuration for location and/or time.
|
|
609
|
+
|
|
610
|
+
| Property | Type | Description |
|
|
611
|
+
|----------|------|-------------|
|
|
612
|
+
| **location** | `LocationFilter` | Optional location-based filter. If provided with `timeRange`, both filters are applied (AND condition). |
|
|
613
|
+
| **timeRange** | `TimeRangeFilter` | Optional time range filter. If provided with `location`, both filters are applied (AND condition). |
|
|
614
|
+
|
|
615
|
+
**Example:**
|
|
616
|
+
```typescript
|
|
617
|
+
const filter: FilterConfig = {
|
|
618
|
+
location: {
|
|
619
|
+
coordinates: [{ lat: 52.52, lng: 13.40 }],
|
|
620
|
+
radius: 5000
|
|
621
|
+
},
|
|
622
|
+
timeRange: {
|
|
623
|
+
start: new Date('2025-01-01'),
|
|
624
|
+
end: new Date('2025-12-31')
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const result = await ExifGallery.pick({ filter });
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
#### LocationFilter
|
|
634
|
+
|
|
635
|
+
Location-based filter configuration.
|
|
636
|
+
|
|
637
|
+
| Property | Type | Description |
|
|
638
|
+
|----------|------|-------------|
|
|
639
|
+
| **polyline** | `LatLng[]` | GPS track as array of coordinates (e.g., from a recorded route). Images within `radius` meters of any point on the polyline will match. |
|
|
640
|
+
| **coordinates** | `LatLng[]` | Individual coordinate points (e.g., from map markers). Images within `radius` meters of any coordinate will match. |
|
|
641
|
+
| **radius** | `number` | Search radius in meters. Default: `100` |
|
|
642
|
+
|
|
643
|
+
**Polyline Note:** Can be either:
|
|
644
|
+
- An array of `LatLng` objects: `[{ lat: 52.52, lng: 13.40 }, ...]`
|
|
645
|
+
- An encoded polyline string (Google Maps format): `"_p~iF~ps|U_ulLnnqC"`
|
|
646
|
+
|
|
647
|
+
**Example:**
|
|
648
|
+
```typescript
|
|
649
|
+
// Using coordinates
|
|
650
|
+
const filter1: LocationFilter = {
|
|
651
|
+
coordinates: [{ lat: 52.52, lng: 13.40 }],
|
|
652
|
+
radius: 5000
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// Using polyline
|
|
656
|
+
const filter2: LocationFilter = {
|
|
657
|
+
polyline: "_p~iF~ps|U_ulLnnqC",
|
|
658
|
+
radius: 2000
|
|
659
|
+
};
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
#### LatLng
|
|
665
|
+
|
|
666
|
+
Geographic coordinate with latitude and longitude.
|
|
667
|
+
|
|
668
|
+
| Property | Type | Description |
|
|
669
|
+
|----------|------|-------------|
|
|
670
|
+
| **lat** | `number` | Latitude in decimal degrees (-90 to +90) |
|
|
671
|
+
| **lng** | `number` | Longitude in decimal degrees (-180 to +180) |
|
|
672
|
+
|
|
673
|
+
**Example:**
|
|
674
|
+
```typescript
|
|
675
|
+
const berlin: LatLng = { lat: 52.5163, lng: 13.3777 };
|
|
676
|
+
const paris: LatLng = { lat: 48.8566, lng: 2.3522 };
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
#### TimeRangeFilter
|
|
682
|
+
|
|
683
|
+
Time range filter configuration.
|
|
684
|
+
|
|
685
|
+
| Property | Type | Description |
|
|
686
|
+
|----------|------|-------------|
|
|
687
|
+
| **start** | `Date` | Start date/time for the filter. Images taken at or after this time will match. |
|
|
688
|
+
| **end** | `Date` | End date/time for the filter. Images taken at or before this time will match. |
|
|
689
|
+
|
|
690
|
+
**Example:**
|
|
691
|
+
```typescript
|
|
692
|
+
const filter: TimeRangeFilter = {
|
|
693
|
+
start: new Date('2025-01-01'),
|
|
694
|
+
end: new Date('2025-12-31')
|
|
695
|
+
};
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
#### TranslationSet
|
|
701
|
+
|
|
702
|
+
Complete set of UI text keys used by the plugin. All keys are available for customization.
|
|
703
|
+
|
|
704
|
+
| Key | Description |
|
|
705
|
+
|-----|-------------|
|
|
706
|
+
| `galleryTitle` | Gallery screen title |
|
|
707
|
+
| `selectButton` | "Select" button text |
|
|
708
|
+
| `cancelButton` | "Cancel" button text |
|
|
709
|
+
| `selectAllButton` | "Select All" button text |
|
|
710
|
+
| `deselectAllButton` | "Deselect All" button text |
|
|
711
|
+
| `selectionCounter` | Selection counter (supports `{count}` and `{total}` placeholders) |
|
|
712
|
+
| `confirmButton` | "Confirm" button text |
|
|
713
|
+
| `filterDialogTitle` | Filter dialog title |
|
|
714
|
+
| `radiusLabel` | "Radius (meters)" label |
|
|
715
|
+
| `startDateLabel` | "Start Date" label |
|
|
716
|
+
| `endDateLabel` | "End Date" label |
|
|
717
|
+
| `loadingMessage` | "Loading images..." message |
|
|
718
|
+
| `emptyMessage` | "No images found" message |
|
|
719
|
+
| `errorMessage` | "An error occurred" message |
|
|
720
|
+
| `retryButton` | "Retry" button text |
|
|
721
|
+
| `initializationError` | "Plugin not initialized" error |
|
|
722
|
+
| `permissionError` | "Permission denied" error |
|
|
723
|
+
| `filterError` | "Invalid filter parameters" error |
|
|
724
|
+
|
|
725
|
+
**Example:**
|
|
726
|
+
```typescript
|
|
727
|
+
await ExifGallery.initialize({
|
|
728
|
+
customTexts: {
|
|
729
|
+
galleryTitle: 'Select Photos',
|
|
730
|
+
selectButton: 'Choose',
|
|
731
|
+
cancelButton: 'Dismiss',
|
|
732
|
+
emptyMessage: 'No photos found'
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
### Type Aliases
|
|
740
|
+
|
|
741
|
+
#### SupportedLocale
|
|
742
|
+
|
|
743
|
+
Supported languages for built-in translations.
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
type SupportedLocale = 'en' | 'de' | 'fr' | 'es';
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## Platform Support
|
|
752
|
+
|
|
753
|
+
### iOS
|
|
754
|
+
|
|
755
|
+
- **Minimum Version:** iOS 15.0
|
|
756
|
+
- **Language:** Swift 5.5+
|
|
757
|
+
- **Framework:** Capacitor 8.0+
|
|
758
|
+
|
|
759
|
+
**Tested on:**
|
|
760
|
+
- iOS 15.x, 16.x, 17.x, 18.x
|
|
761
|
+
- iPhone SE, iPhone 14, iPhone 15 Pro Max
|
|
762
|
+
- iPad (all sizes)
|
|
763
|
+
|
|
764
|
+
### Android
|
|
765
|
+
|
|
766
|
+
- **Minimum Version:** Android 7.0 (API 24)
|
|
767
|
+
- **Language:** Kotlin 1.9+
|
|
768
|
+
- **Framework:** Capacitor 8.0+
|
|
769
|
+
|
|
770
|
+
**Tested on:**
|
|
771
|
+
- Android 7.0+, 10, 11, 12, 13, 14
|
|
772
|
+
- Pixel 4, Pixel 5, Pixel 6, Pixel 7
|
|
773
|
+
- Samsung Galaxy S21, Galaxy S22
|
|
774
|
+
- Galaxy Tab S8
|
|
775
|
+
|
|
776
|
+
## License
|
|
777
|
+
|
|
778
|
+
**Commercial License - License Key Required for Production Use**
|
|
779
|
+
|
|
780
|
+
Copyright (c) 2025 KesByte Digital. All rights reserved.
|
|
781
|
+
|
|
782
|
+
This is a **commercial plugin** that requires a valid license key for production builds.
|
|
783
|
+
|
|
784
|
+
### Debug Builds (Free for Testing)
|
|
785
|
+
- ✅ **No license key required** for debug/development builds
|
|
786
|
+
- ✅ Full functionality available for integration and testing
|
|
787
|
+
- ✅ Integrate the plugin into your app and test all features freely
|
|
788
|
+
|
|
789
|
+
### Production Builds (License Required)
|
|
790
|
+
- ⚠️ **License key REQUIRED** for production/release builds
|
|
791
|
+
- ⚠️ Production builds will **fail validation** without a valid license
|
|
792
|
+
- ✅ Purchase a license at: **[exif-gallery.kesbyte-digital.com](https://exif-gallery.kesbyte-digital.com)**
|
|
793
|
+
|
|
794
|
+
### How It Works
|
|
795
|
+
1. **Development:** Install and test the plugin freely in debug builds
|
|
796
|
+
2. **Production:** Purchase a license key before releasing your app
|
|
797
|
+
3. **Integration:** Add the license key to your app configuration
|
|
798
|
+
4. **Build:** Production builds validate the license automatically
|
|
799
|
+
|
|
800
|
+
### License Purchase
|
|
801
|
+
Visit **[exif-gallery.kesbyte-digital.com](https://exif-gallery.kesbyte-digital.com)** to:
|
|
802
|
+
- Purchase a license for your project
|
|
803
|
+
- View licensing options and pricing
|
|
804
|
+
- Access your license dashboard
|
|
805
|
+
- Get support and documentation
|
|
806
|
+
|
|
807
|
+
**Important:** This plugin is NOT open source. The source code is proprietary and protected. Only compiled binaries are distributed via npm.
|
|
808
|
+
|
|
809
|
+
## Support
|
|
810
|
+
|
|
811
|
+
- **Documentation:** See the README and API documentation above
|
|
812
|
+
- **Issues:** [GitHub Issues](https://github.com/KesByte-Digital/capacitor-exif-gallery/issues)
|
|
813
|
+
- **Source:** [GitHub Repository](https://github.com/KesByte-Digital/capacitor-exif-gallery)
|