@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.
Files changed (40) hide show
  1. package/CapacitorExifGallery.podspec +17 -0
  2. package/LICENSE +8 -0
  3. package/Package.swift +28 -0
  4. package/README.md +813 -0
  5. package/dist/esm/ExifGalleryImpl.d.ts +176 -0
  6. package/dist/esm/ExifGalleryImpl.js +295 -0
  7. package/dist/esm/ExifGalleryImpl.js.map +1 -0
  8. package/dist/esm/FilterValidator.d.ts +126 -0
  9. package/dist/esm/FilterValidator.js +274 -0
  10. package/dist/esm/FilterValidator.js.map +1 -0
  11. package/dist/esm/PluginState.d.ts +128 -0
  12. package/dist/esm/PluginState.js +166 -0
  13. package/dist/esm/PluginState.js.map +1 -0
  14. package/dist/esm/PolylineDecoder.d.ts +23 -0
  15. package/dist/esm/PolylineDecoder.js +34 -0
  16. package/dist/esm/PolylineDecoder.js.map +1 -0
  17. package/dist/esm/TranslationLoader.d.ts +140 -0
  18. package/dist/esm/TranslationLoader.js +218 -0
  19. package/dist/esm/TranslationLoader.js.map +1 -0
  20. package/dist/esm/definitions.d.ts +539 -0
  21. package/dist/esm/definitions.js +2 -0
  22. package/dist/esm/definitions.js.map +1 -0
  23. package/dist/esm/errors.d.ts +252 -0
  24. package/dist/esm/errors.js +276 -0
  25. package/dist/esm/errors.js.map +1 -0
  26. package/dist/esm/index.d.ts +40 -0
  27. package/dist/esm/index.js +42 -0
  28. package/dist/esm/index.js.map +1 -0
  29. package/dist/esm/translations/de.json +20 -0
  30. package/dist/esm/translations/en.json +20 -0
  31. package/dist/esm/translations/es.json +20 -0
  32. package/dist/esm/translations/fr.json +20 -0
  33. package/dist/esm/web.d.ts +6 -0
  34. package/dist/esm/web.js +14 -0
  35. package/dist/esm/web.js.map +1 -0
  36. package/dist/plugin.cjs.js +1820 -0
  37. package/dist/plugin.cjs.js.map +1 -0
  38. package/dist/plugin.js +1823 -0
  39. package/dist/plugin.js.map +1 -0
  40. 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
+ [![npm version](https://img.shields.io/npm/v/@kesbyte/capacitor-exif-gallery.svg)](https://www.npmjs.com/package/@kesbyte/capacitor-exif-gallery)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@kesbyte/capacitor-exif-gallery.svg)](https://www.npmjs.com/package/@kesbyte/capacitor-exif-gallery)
9
+ [![License: Commercial](https://img.shields.io/badge/License-Commercial-red.svg)](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)