@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
@@ -0,0 +1,539 @@
1
+ /**
2
+ * Supported languages for built-in translations.
3
+ */
4
+ export type SupportedLocale = 'en' | 'de' | 'fr' | 'es';
5
+ /**
6
+ * Distance unit for radius display in filter dialog.
7
+ *
8
+ * @since 1.1.0
9
+ */
10
+ export type DistanceUnit = 'kilometers' | 'miles';
11
+ /**
12
+ * Complete set of UI text keys used by the plugin.
13
+ * All keys are required for a complete translation.
14
+ */
15
+ export interface TranslationSet {
16
+ /** Gallery screen title */
17
+ galleryTitle: string;
18
+ /** "Select" button text */
19
+ selectButton: string;
20
+ /** "Cancel" button text (also used for accessibility label on icon-only cancel button) */
21
+ cancelButton: string;
22
+ /** "Select All" button text */
23
+ selectAllButton: string;
24
+ /** "Deselect All" button text */
25
+ deselectAllButton: string;
26
+ /** Selection counter text. Placeholders: {count}, {total} */
27
+ selectionCounter: string;
28
+ /** "Confirm" button text (also used for accessibility label on icon-only confirm button) */
29
+ confirmButton: string;
30
+ /** "Filter" button accessibility label (optional, falls back to filterDialogTitle) */
31
+ filterButton?: string;
32
+ /** Filter dialog title */
33
+ filterDialogTitle: string;
34
+ /** "Radius (meters)" label */
35
+ radiusLabel: string;
36
+ /** "Start Date" label */
37
+ startDateLabel: string;
38
+ /** "End Date" label */
39
+ endDateLabel: string;
40
+ /** "Loading images..." message */
41
+ loadingMessage: string;
42
+ /** "No images found" message */
43
+ emptyMessage: string;
44
+ /** "An error occurred" message */
45
+ errorMessage: string;
46
+ /** "Retry" button text */
47
+ retryButton: string;
48
+ /** "Plugin not initialized" error */
49
+ initializationError: string;
50
+ /** "Permission denied" error */
51
+ permissionError: string;
52
+ /** "Invalid filter parameters" error */
53
+ filterError: string;
54
+ /** Photo access required message (shown when user denies photo permission) */
55
+ noStoragePermission?: string;
56
+ /** Permanent photo denial message (shown when user selects "Don't ask again" on Android) */
57
+ noStoragePermissionPermanent?: string;
58
+ /** Photo access restricted message (shown on iOS when restricted by parental controls/MDM) */
59
+ noStoragePermissionRestricted?: string;
60
+ /** Location access required message (shown when user denies location permission) */
61
+ noLocationPermission?: string;
62
+ /** Location permission fallback message (informational, shown when falling back to time filtering) */
63
+ locationPermissionFallback?: string;
64
+ /** "Open Settings" button text (opens system settings for permission management) */
65
+ openSettings?: string;
66
+ /** "OK" button text (generic confirmation, used in location fallback dialog) */
67
+ ok?: string;
68
+ }
69
+ /**
70
+ * Plugin initialization configuration.
71
+ * All properties are optional.
72
+ */
73
+ export interface InitConfig {
74
+ /**
75
+ * Optional locale to use for UI text.
76
+ * If not provided, system language is detected automatically.
77
+ * Falls back to English if system language is not supported.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * await ExifGallery.initialize({ locale: 'de' });
82
+ * ```
83
+ */
84
+ locale?: SupportedLocale;
85
+ /**
86
+ * Optional custom text overrides.
87
+ * Merges on top of default translations for the selected locale.
88
+ * Partial object - only override the keys you need.
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * await ExifGallery.initialize({
93
+ * customTexts: {
94
+ * galleryTitle: 'Choose Your Photos',
95
+ * confirmButton: 'Done'
96
+ * }
97
+ * });
98
+ * ```
99
+ */
100
+ customTexts?: Partial<TranslationSet>;
101
+ /**
102
+ * Whether to request photo library permissions immediately during initialization.
103
+ * Default: false (permissions requested just-in-time when pick() is called)
104
+ *
105
+ * Set to true if you want to request permissions upfront (e.g., during onboarding).
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * await ExifGallery.initialize({ requestPermissionsUpfront: true });
110
+ * ```
111
+ */
112
+ requestPermissionsUpfront?: boolean;
113
+ }
114
+ /**
115
+ * Geographic coordinate with latitude and longitude.
116
+ */
117
+ export interface LatLng {
118
+ /** Latitude in decimal degrees */
119
+ lat: number;
120
+ /** Longitude in decimal degrees */
121
+ lng: number;
122
+ }
123
+ /**
124
+ * Location-based filter configuration.
125
+ */
126
+ export interface LocationFilter {
127
+ /**
128
+ * GPS track as array of coordinates OR encoded polyline string.
129
+ *
130
+ * **Coordinate Array Format:**
131
+ * ```typescript
132
+ * polyline: [{ lat: 38.5, lng: -120.2 }, { lat: 40.7, lng: -120.95 }]
133
+ * ```
134
+ *
135
+ * **Encoded Polyline Format (Google's Polyline Encoding Algorithm, precision 5):**
136
+ * ```typescript
137
+ * polyline: "_p~iF~ps|U_ulLnnqC"
138
+ * ```
139
+ *
140
+ * Note: Precision 5 provides ±1 meter accuracy (Google Maps default).
141
+ * Higher precision polylines (6+) are also supported.
142
+ *
143
+ * Images within `radius` meters of any point on the polyline will match.
144
+ *
145
+ * **Limits:**
146
+ * - Max encoded string: 50 KB
147
+ * - Max decoded points: 1,000
148
+ * - Min points: 2 (to define a path)
149
+ *
150
+ * @see https://developers.google.com/maps/documentation/utilities/polylinealgorithm
151
+ * @example
152
+ * // Using encoded polyline (from Google Maps API)
153
+ * const result = await ExifGallery.pick({
154
+ * filter: {
155
+ * location: {
156
+ * polyline: "_p~iF~ps|U_ulLnnqC",
157
+ * radius: 5000
158
+ * }
159
+ * }
160
+ * });
161
+ *
162
+ * @example
163
+ * // Using coordinate array
164
+ * const result = await ExifGallery.pick({
165
+ * filter: {
166
+ * location: {
167
+ * polyline: [
168
+ * { lat: 48.8566, lng: 2.3522 },
169
+ * { lat: 48.8606, lng: 2.3376 }
170
+ * ],
171
+ * radius: 5000
172
+ * }
173
+ * }
174
+ * });
175
+ */
176
+ polyline?: LatLng[] | string;
177
+ /**
178
+ * Individual coordinate points (e.g., from map markers).
179
+ * Images within `radius` meters of any coordinate will match.
180
+ */
181
+ coordinates?: LatLng[];
182
+ /**
183
+ * Search radius in meters.
184
+ * Default: 100
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * {
189
+ * polyline: [{lat: 48.1, lng: 11.5}, {lat: 48.2, lng: 11.6}],
190
+ * radius: 1000 // 1km radius
191
+ * }
192
+ * ```
193
+ */
194
+ radius?: number;
195
+ }
196
+ /**
197
+ * Time range filter configuration.
198
+ */
199
+ export interface TimeRangeFilter {
200
+ /**
201
+ * Start date/time for the filter.
202
+ * Images taken at or after this time will match.
203
+ */
204
+ start: Date;
205
+ /**
206
+ * End date/time for the filter.
207
+ * Images taken at or before this time will match.
208
+ */
209
+ end: Date;
210
+ }
211
+ /**
212
+ * Combined filter configuration for location and/or time.
213
+ */
214
+ export interface FilterConfig {
215
+ /**
216
+ * Optional location-based filter.
217
+ * If provided with timeRange, both filters are applied (AND condition).
218
+ */
219
+ location?: LocationFilter;
220
+ /**
221
+ * Optional time range filter.
222
+ * If provided with location, both filters are applied (AND condition).
223
+ */
224
+ timeRange?: TimeRangeFilter;
225
+ }
226
+ /**
227
+ * Options for the pick() method.
228
+ */
229
+ export interface PickOptions {
230
+ /**
231
+ * Optional filter configuration to pre-configure the gallery.
232
+ * If not provided, user can manually set filters in the gallery UI.
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * await ExifGallery.pick({
237
+ * filter: {
238
+ * location: {
239
+ * coordinates: [{lat: 48.1, lng: 11.5}],
240
+ * radius: 500
241
+ * }
242
+ * }
243
+ * });
244
+ * ```
245
+ */
246
+ filter?: FilterConfig;
247
+ /**
248
+ * Minimum number of results required before automatic fallback to time filter.
249
+ * If location filter returns fewer images than this threshold,
250
+ * the plugin automatically falls back to time-based filtering.
251
+ *
252
+ * Default: 5
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * await ExifGallery.pick({
257
+ * filter: { location: {...} },
258
+ * fallbackThreshold: 10 // Fallback if < 10 images found
259
+ * });
260
+ * ```
261
+ */
262
+ fallbackThreshold?: number;
263
+ /**
264
+ * Whether to allow user to manually adjust filters in the gallery UI.
265
+ * Default: true
266
+ *
267
+ * Set to false if you want to enforce the provided filter configuration.
268
+ */
269
+ allowManualAdjustment?: boolean;
270
+ /**
271
+ * Distance unit for radius display in filter dialog.
272
+ *
273
+ * **Default:** `'kilometers'`
274
+ *
275
+ * **Supported units:**
276
+ * - `'kilometers'`: Display as km (1000m = 1km)
277
+ * - `'miles'`: Display as miles (1609.34m = 1mi)
278
+ *
279
+ * @since 1.1.0
280
+ *
281
+ * @example
282
+ * ```typescript
283
+ * // Use miles for US users
284
+ * await ExifGallery.pick({
285
+ * filter: { location: {...} },
286
+ * distanceUnit: 'miles'
287
+ * });
288
+ *
289
+ * // Use kilometers (default)
290
+ * await ExifGallery.pick({
291
+ * filter: { location: {...} }
292
+ * });
293
+ *
294
+ * // Based on user preference
295
+ * const userUnit = userSettings.useMetric ? 'kilometers' : 'miles';
296
+ * await ExifGallery.pick({
297
+ * filter: { location: {...} },
298
+ * distanceUnit: userUnit
299
+ * });
300
+ * ```
301
+ */
302
+ distanceUnit?: DistanceUnit;
303
+ /**
304
+ * Step size for distance filter slider (in kilometers).
305
+ *
306
+ * Defines the granularity of the radius adjustment slider in the filter dialog.
307
+ *
308
+ * **Default:** `5` (5km steps)
309
+ *
310
+ * **Range:** 1 - 25 km
311
+ *
312
+ * @since 1.2.0
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * // Fine-grained control: 1km steps (5, 6, 7, 8, ...)
317
+ * await ExifGallery.pick({
318
+ * filter: { location: {...} },
319
+ * distanceStep: 1
320
+ * });
321
+ *
322
+ * // Default: 5km steps (5, 10, 15, 20, ...)
323
+ * await ExifGallery.pick({
324
+ * filter: { location: {...} }
325
+ * // distanceStep defaults to 5
326
+ * });
327
+ *
328
+ * // Coarse control: 10km steps (10, 20, 30, 40, 50)
329
+ * await ExifGallery.pick({
330
+ * filter: { location: {...} },
331
+ * distanceStep: 10
332
+ * });
333
+ *
334
+ * // Can be combined with distanceUnit
335
+ * await ExifGallery.pick({
336
+ * filter: { location: {...} },
337
+ * distanceUnit: 'miles',
338
+ * distanceStep: 5 // 5km steps, displayed as miles
339
+ * });
340
+ * ```
341
+ */
342
+ distanceStep?: number;
343
+ }
344
+ /**
345
+ * EXIF metadata extracted from an image.
346
+ */
347
+ export interface ImageExif {
348
+ /** Latitude from GPS EXIF data (if available) */
349
+ lat?: number;
350
+ /** Longitude from GPS EXIF data (if available) */
351
+ lng?: number;
352
+ /** Timestamp from EXIF DateTimeOriginal (if available) */
353
+ timestamp?: Date;
354
+ }
355
+ /**
356
+ * Single image result from pick().
357
+ */
358
+ export interface ImageResult {
359
+ /**
360
+ * File URI for the image (file:// path).
361
+ * Can be used to display or upload the image.
362
+ */
363
+ uri: string;
364
+ /**
365
+ * Web-safe path for displaying the image in a WebView.
366
+ * This is the Capacitor-converted URL (capacitor://localhost/_capacitor_file_/...).
367
+ *
368
+ * Use this for <img src> tags - no manual Capacitor.convertFileSrc() needed.
369
+ *
370
+ * @since 1.2.0
371
+ */
372
+ webPath?: string;
373
+ /**
374
+ * EXIF metadata if available.
375
+ * May be undefined if image has no EXIF data.
376
+ */
377
+ exif?: ImageExif;
378
+ /**
379
+ * How this image was filtered.
380
+ * - 'location': Matched location filter
381
+ * - 'time': Matched time filter (or fallback from location filter)
382
+ */
383
+ filteredBy: 'location' | 'time';
384
+ }
385
+ /**
386
+ * Result from pick() method.
387
+ */
388
+ export interface PickResult {
389
+ /**
390
+ * Array of selected images.
391
+ * Empty if user cancelled or no images matched filters.
392
+ */
393
+ images: ImageResult[];
394
+ /**
395
+ * True if user explicitly cancelled the selection.
396
+ * False if user confirmed selection (even if no images selected).
397
+ */
398
+ cancelled: boolean;
399
+ }
400
+ /**
401
+ * Main plugin interface.
402
+ *
403
+ * @example
404
+ * ```typescript
405
+ * import { ExifGallery } from '@kesbyte/capacitor-exif-gallery';
406
+ *
407
+ * // Initialize plugin (once at app startup)
408
+ * await ExifGallery.initialize({
409
+ * locale: 'de',
410
+ * customTexts: {
411
+ * galleryTitle: 'Wähle Fotos'
412
+ * }
413
+ * });
414
+ *
415
+ * // Open gallery with filter
416
+ * const result = await ExifGallery.pick({
417
+ * filter: {
418
+ * location: {
419
+ * polyline: gpsTrack, // LatLng[]
420
+ * radius: 1000
421
+ * }
422
+ * },
423
+ * fallbackThreshold: 5
424
+ * });
425
+ *
426
+ * if (!result.cancelled) {
427
+ * console.log(`Selected ${result.images.length} images`);
428
+ * result.images.forEach(img => {
429
+ * console.log(`Image: ${img.uri}, filtered by: ${img.filteredBy}`);
430
+ * });
431
+ * }
432
+ * ```
433
+ */
434
+ export interface ExifGalleryPlugin {
435
+ /**
436
+ * Initialize the plugin with optional configuration.
437
+ *
438
+ * Must be called before pick(). Can be called multiple times to update configuration.
439
+ *
440
+ * **Default behavior (no config):**
441
+ * - Detects system language automatically
442
+ * - Uses built-in English/German/French/Spanish translations
443
+ * - Requests permissions just-in-time (when pick() is called)
444
+ *
445
+ * @param config - Optional configuration object
446
+ * @returns Promise that resolves when initialization is complete
447
+ *
448
+ * @throws {Error} If locale is invalid or customTexts contain unknown keys
449
+ *
450
+ * @example
451
+ * ```typescript
452
+ * // Minimal - use defaults
453
+ * await ExifGallery.initialize();
454
+ *
455
+ * // With locale
456
+ * await ExifGallery.initialize({ locale: 'de' });
457
+ *
458
+ * // With custom text overrides
459
+ * await ExifGallery.initialize({
460
+ * customTexts: {
461
+ * galleryTitle: 'Choose Photos',
462
+ * confirmButton: 'Done'
463
+ * }
464
+ * });
465
+ *
466
+ * // Request permissions upfront
467
+ * await ExifGallery.initialize({
468
+ * requestPermissionsUpfront: true
469
+ * });
470
+ * ```
471
+ */
472
+ initialize(config?: InitConfig): Promise<void>;
473
+ /**
474
+ * Open native gallery with optional filters and return selected images.
475
+ *
476
+ * Must call initialize() first, otherwise throws initialization_required error.
477
+ *
478
+ * **Filter behavior:**
479
+ * - If filter provided: Gallery opens with pre-configured filters
480
+ * - If no filter: User can manually set filters in gallery UI
481
+ * - Auto-fallback: If location filter returns < fallbackThreshold images, falls back to time filter
482
+ *
483
+ * @param options - Optional pick options with filters
484
+ * @returns Promise with selected images or cancellation status
485
+ *
486
+ * @throws {Error} 'initialization_required' if initialize() not called
487
+ * @throws {Error} 'no_permission' if required permissions denied
488
+ * @throws {Error} 'filter_error' if filter parameters are invalid
489
+ *
490
+ * @example
491
+ * ```typescript
492
+ * // No filter - user sets filters manually
493
+ * const result = await ExifGallery.pick();
494
+ *
495
+ * // Location filter with polyline
496
+ * const result = await ExifGallery.pick({
497
+ * filter: {
498
+ * location: {
499
+ * polyline: [{lat: 48.1, lng: 11.5}, {lat: 48.2, lng: 11.6}],
500
+ * radius: 1000
501
+ * }
502
+ * }
503
+ * });
504
+ *
505
+ * // Time range filter
506
+ * const result = await ExifGallery.pick({
507
+ * filter: {
508
+ * timeRange: {
509
+ * start: new Date('2024-01-01'),
510
+ * end: new Date('2024-01-31')
511
+ * }
512
+ * }
513
+ * });
514
+ *
515
+ * // Combined filters
516
+ * const result = await ExifGallery.pick({
517
+ * filter: {
518
+ * location: { coordinates: [{lat: 48.1, lng: 11.5}], radius: 500 },
519
+ * timeRange: { start: new Date('2024-01-01'), end: new Date() }
520
+ * },
521
+ * fallbackThreshold: 10,
522
+ * allowManualAdjustment: false
523
+ * });
524
+ *
525
+ * // Handle result
526
+ * if (!result.cancelled) {
527
+ * console.log(`Selected ${result.images.length} images`);
528
+ * for (const img of result.images) {
529
+ * console.log(`${img.uri} - filtered by ${img.filteredBy}`);
530
+ * if (img.exif) {
531
+ * console.log(` GPS: ${img.exif.lat}, ${img.exif.lng}`);
532
+ * console.log(` Time: ${img.exif.timestamp}`);
533
+ * }
534
+ * }
535
+ * }
536
+ * ```
537
+ */
538
+ pick(options?: PickOptions): Promise<PickResult>;
539
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=definitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Supported languages for built-in translations.\n */\nexport type SupportedLocale = 'en' | 'de' | 'fr' | 'es';\n\n/**\n * Distance unit for radius display in filter dialog.\n *\n * @since 1.1.0\n */\nexport type DistanceUnit = 'kilometers' | 'miles';\n\n/**\n * Complete set of UI text keys used by the plugin.\n * All keys are required for a complete translation.\n */\nexport interface TranslationSet {\n // Gallery UI (8 keys)\n /** Gallery screen title */\n galleryTitle: string;\n /** \"Select\" button text */\n selectButton: string;\n /** \"Cancel\" button text (also used for accessibility label on icon-only cancel button) */\n cancelButton: string;\n /** \"Select All\" button text */\n selectAllButton: string;\n /** \"Deselect All\" button text */\n deselectAllButton: string;\n /** Selection counter text. Placeholders: {count}, {total} */\n selectionCounter: string;\n /** \"Confirm\" button text (also used for accessibility label on icon-only confirm button) */\n confirmButton: string;\n /** \"Filter\" button accessibility label (optional, falls back to filterDialogTitle) */\n filterButton?: string;\n\n // Filter Dialog (4 keys)\n /** Filter dialog title */\n filterDialogTitle: string;\n /** \"Radius (meters)\" label */\n radiusLabel: string;\n /** \"Start Date\" label */\n startDateLabel: string;\n /** \"End Date\" label */\n endDateLabel: string;\n\n // UI States (4 keys)\n /** \"Loading images...\" message */\n loadingMessage: string;\n /** \"No images found\" message */\n emptyMessage: string;\n /** \"An error occurred\" message */\n errorMessage: string;\n /** \"Retry\" button text */\n retryButton: string;\n\n // Error Messages (3 keys)\n /** \"Plugin not initialized\" error */\n initializationError: string;\n /** \"Permission denied\" error */\n permissionError: string;\n /** \"Invalid filter parameters\" error */\n filterError: string;\n\n // Permission Error Messages (Story 5.4) (7 keys - all optional)\n /** Photo access required message (shown when user denies photo permission) */\n noStoragePermission?: string;\n /** Permanent photo denial message (shown when user selects \"Don't ask again\" on Android) */\n noStoragePermissionPermanent?: string;\n /** Photo access restricted message (shown on iOS when restricted by parental controls/MDM) */\n noStoragePermissionRestricted?: string;\n /** Location access required message (shown when user denies location permission) */\n noLocationPermission?: string;\n /** Location permission fallback message (informational, shown when falling back to time filtering) */\n locationPermissionFallback?: string;\n /** \"Open Settings\" button text (opens system settings for permission management) */\n openSettings?: string;\n /** \"OK\" button text (generic confirmation, used in location fallback dialog) */\n ok?: string;\n}\n\n/**\n * Plugin initialization configuration.\n * All properties are optional.\n */\nexport interface InitConfig {\n /**\n * Optional locale to use for UI text.\n * If not provided, system language is detected automatically.\n * Falls back to English if system language is not supported.\n *\n * @example\n * ```typescript\n * await ExifGallery.initialize({ locale: 'de' });\n * ```\n */\n locale?: SupportedLocale;\n\n /**\n * Optional custom text overrides.\n * Merges on top of default translations for the selected locale.\n * Partial object - only override the keys you need.\n *\n * @example\n * ```typescript\n * await ExifGallery.initialize({\n * customTexts: {\n * galleryTitle: 'Choose Your Photos',\n * confirmButton: 'Done'\n * }\n * });\n * ```\n */\n customTexts?: Partial<TranslationSet>;\n\n /**\n * Whether to request photo library permissions immediately during initialization.\n * Default: false (permissions requested just-in-time when pick() is called)\n *\n * Set to true if you want to request permissions upfront (e.g., during onboarding).\n *\n * @example\n * ```typescript\n * await ExifGallery.initialize({ requestPermissionsUpfront: true });\n * ```\n */\n requestPermissionsUpfront?: boolean;\n}\n\n/**\n * Geographic coordinate with latitude and longitude.\n */\nexport interface LatLng {\n /** Latitude in decimal degrees */\n lat: number;\n /** Longitude in decimal degrees */\n lng: number;\n}\n\n/**\n * Location-based filter configuration.\n */\nexport interface LocationFilter {\n /**\n * GPS track as array of coordinates OR encoded polyline string.\n *\n * **Coordinate Array Format:**\n * ```typescript\n * polyline: [{ lat: 38.5, lng: -120.2 }, { lat: 40.7, lng: -120.95 }]\n * ```\n *\n * **Encoded Polyline Format (Google's Polyline Encoding Algorithm, precision 5):**\n * ```typescript\n * polyline: \"_p~iF~ps|U_ulLnnqC\"\n * ```\n *\n * Note: Precision 5 provides ±1 meter accuracy (Google Maps default).\n * Higher precision polylines (6+) are also supported.\n *\n * Images within `radius` meters of any point on the polyline will match.\n *\n * **Limits:**\n * - Max encoded string: 50 KB\n * - Max decoded points: 1,000\n * - Min points: 2 (to define a path)\n *\n * @see https://developers.google.com/maps/documentation/utilities/polylinealgorithm\n * @example\n * // Using encoded polyline (from Google Maps API)\n * const result = await ExifGallery.pick({\n * filter: {\n * location: {\n * polyline: \"_p~iF~ps|U_ulLnnqC\",\n * radius: 5000\n * }\n * }\n * });\n *\n * @example\n * // Using coordinate array\n * const result = await ExifGallery.pick({\n * filter: {\n * location: {\n * polyline: [\n * { lat: 48.8566, lng: 2.3522 },\n * { lat: 48.8606, lng: 2.3376 }\n * ],\n * radius: 5000\n * }\n * }\n * });\n */\n polyline?: LatLng[] | string;\n\n /**\n * Individual coordinate points (e.g., from map markers).\n * Images within `radius` meters of any coordinate will match.\n */\n coordinates?: LatLng[];\n\n /**\n * Search radius in meters.\n * Default: 100\n *\n * @example\n * ```typescript\n * {\n * polyline: [{lat: 48.1, lng: 11.5}, {lat: 48.2, lng: 11.6}],\n * radius: 1000 // 1km radius\n * }\n * ```\n */\n radius?: number;\n}\n\n/**\n * Time range filter configuration.\n */\nexport interface TimeRangeFilter {\n /**\n * Start date/time for the filter.\n * Images taken at or after this time will match.\n */\n start: Date;\n\n /**\n * End date/time for the filter.\n * Images taken at or before this time will match.\n */\n end: Date;\n}\n\n/**\n * Combined filter configuration for location and/or time.\n */\nexport interface FilterConfig {\n /**\n * Optional location-based filter.\n * If provided with timeRange, both filters are applied (AND condition).\n */\n location?: LocationFilter;\n\n /**\n * Optional time range filter.\n * If provided with location, both filters are applied (AND condition).\n */\n timeRange?: TimeRangeFilter;\n}\n\n/**\n * Options for the pick() method.\n */\nexport interface PickOptions {\n /**\n * Optional filter configuration to pre-configure the gallery.\n * If not provided, user can manually set filters in the gallery UI.\n *\n * @example\n * ```typescript\n * await ExifGallery.pick({\n * filter: {\n * location: {\n * coordinates: [{lat: 48.1, lng: 11.5}],\n * radius: 500\n * }\n * }\n * });\n * ```\n */\n filter?: FilterConfig;\n\n /**\n * Minimum number of results required before automatic fallback to time filter.\n * If location filter returns fewer images than this threshold,\n * the plugin automatically falls back to time-based filtering.\n *\n * Default: 5\n *\n * @example\n * ```typescript\n * await ExifGallery.pick({\n * filter: { location: {...} },\n * fallbackThreshold: 10 // Fallback if < 10 images found\n * });\n * ```\n */\n fallbackThreshold?: number;\n\n /**\n * Whether to allow user to manually adjust filters in the gallery UI.\n * Default: true\n *\n * Set to false if you want to enforce the provided filter configuration.\n */\n allowManualAdjustment?: boolean;\n\n /**\n * Distance unit for radius display in filter dialog.\n *\n * **Default:** `'kilometers'`\n *\n * **Supported units:**\n * - `'kilometers'`: Display as km (1000m = 1km)\n * - `'miles'`: Display as miles (1609.34m = 1mi)\n *\n * @since 1.1.0\n *\n * @example\n * ```typescript\n * // Use miles for US users\n * await ExifGallery.pick({\n * filter: { location: {...} },\n * distanceUnit: 'miles'\n * });\n *\n * // Use kilometers (default)\n * await ExifGallery.pick({\n * filter: { location: {...} }\n * });\n *\n * // Based on user preference\n * const userUnit = userSettings.useMetric ? 'kilometers' : 'miles';\n * await ExifGallery.pick({\n * filter: { location: {...} },\n * distanceUnit: userUnit\n * });\n * ```\n */\n distanceUnit?: DistanceUnit;\n\n /**\n * Step size for distance filter slider (in kilometers).\n *\n * Defines the granularity of the radius adjustment slider in the filter dialog.\n *\n * **Default:** `5` (5km steps)\n *\n * **Range:** 1 - 25 km\n *\n * @since 1.2.0\n *\n * @example\n * ```typescript\n * // Fine-grained control: 1km steps (5, 6, 7, 8, ...)\n * await ExifGallery.pick({\n * filter: { location: {...} },\n * distanceStep: 1\n * });\n *\n * // Default: 5km steps (5, 10, 15, 20, ...)\n * await ExifGallery.pick({\n * filter: { location: {...} }\n * // distanceStep defaults to 5\n * });\n *\n * // Coarse control: 10km steps (10, 20, 30, 40, 50)\n * await ExifGallery.pick({\n * filter: { location: {...} },\n * distanceStep: 10\n * });\n *\n * // Can be combined with distanceUnit\n * await ExifGallery.pick({\n * filter: { location: {...} },\n * distanceUnit: 'miles',\n * distanceStep: 5 // 5km steps, displayed as miles\n * });\n * ```\n */\n distanceStep?: number;\n}\n\n/**\n * EXIF metadata extracted from an image.\n */\nexport interface ImageExif {\n /** Latitude from GPS EXIF data (if available) */\n lat?: number;\n /** Longitude from GPS EXIF data (if available) */\n lng?: number;\n /** Timestamp from EXIF DateTimeOriginal (if available) */\n timestamp?: Date;\n}\n\n/**\n * Single image result from pick().\n */\nexport interface ImageResult {\n /**\n * File URI for the image (file:// path).\n * Can be used to display or upload the image.\n */\n uri: string;\n\n /**\n * Web-safe path for displaying the image in a WebView.\n * This is the Capacitor-converted URL (capacitor://localhost/_capacitor_file_/...).\n *\n * Use this for <img src> tags - no manual Capacitor.convertFileSrc() needed.\n *\n * @since 1.2.0\n */\n webPath?: string;\n\n /**\n * EXIF metadata if available.\n * May be undefined if image has no EXIF data.\n */\n exif?: ImageExif;\n\n /**\n * How this image was filtered.\n * - 'location': Matched location filter\n * - 'time': Matched time filter (or fallback from location filter)\n */\n filteredBy: 'location' | 'time';\n}\n\n/**\n * Result from pick() method.\n */\nexport interface PickResult {\n /**\n * Array of selected images.\n * Empty if user cancelled or no images matched filters.\n */\n images: ImageResult[];\n\n /**\n * True if user explicitly cancelled the selection.\n * False if user confirmed selection (even if no images selected).\n */\n cancelled: boolean;\n}\n\n/**\n * Main plugin interface.\n *\n * @example\n * ```typescript\n * import { ExifGallery } from '@kesbyte/capacitor-exif-gallery';\n *\n * // Initialize plugin (once at app startup)\n * await ExifGallery.initialize({\n * locale: 'de',\n * customTexts: {\n * galleryTitle: 'Wähle Fotos'\n * }\n * });\n *\n * // Open gallery with filter\n * const result = await ExifGallery.pick({\n * filter: {\n * location: {\n * polyline: gpsTrack, // LatLng[]\n * radius: 1000\n * }\n * },\n * fallbackThreshold: 5\n * });\n *\n * if (!result.cancelled) {\n * console.log(`Selected ${result.images.length} images`);\n * result.images.forEach(img => {\n * console.log(`Image: ${img.uri}, filtered by: ${img.filteredBy}`);\n * });\n * }\n * ```\n */\nexport interface ExifGalleryPlugin {\n /**\n * Initialize the plugin with optional configuration.\n *\n * Must be called before pick(). Can be called multiple times to update configuration.\n *\n * **Default behavior (no config):**\n * - Detects system language automatically\n * - Uses built-in English/German/French/Spanish translations\n * - Requests permissions just-in-time (when pick() is called)\n *\n * @param config - Optional configuration object\n * @returns Promise that resolves when initialization is complete\n *\n * @throws {Error} If locale is invalid or customTexts contain unknown keys\n *\n * @example\n * ```typescript\n * // Minimal - use defaults\n * await ExifGallery.initialize();\n *\n * // With locale\n * await ExifGallery.initialize({ locale: 'de' });\n *\n * // With custom text overrides\n * await ExifGallery.initialize({\n * customTexts: {\n * galleryTitle: 'Choose Photos',\n * confirmButton: 'Done'\n * }\n * });\n *\n * // Request permissions upfront\n * await ExifGallery.initialize({\n * requestPermissionsUpfront: true\n * });\n * ```\n */\n initialize(config?: InitConfig): Promise<void>;\n\n /**\n * Open native gallery with optional filters and return selected images.\n *\n * Must call initialize() first, otherwise throws initialization_required error.\n *\n * **Filter behavior:**\n * - If filter provided: Gallery opens with pre-configured filters\n * - If no filter: User can manually set filters in gallery UI\n * - Auto-fallback: If location filter returns < fallbackThreshold images, falls back to time filter\n *\n * @param options - Optional pick options with filters\n * @returns Promise with selected images or cancellation status\n *\n * @throws {Error} 'initialization_required' if initialize() not called\n * @throws {Error} 'no_permission' if required permissions denied\n * @throws {Error} 'filter_error' if filter parameters are invalid\n *\n * @example\n * ```typescript\n * // No filter - user sets filters manually\n * const result = await ExifGallery.pick();\n *\n * // Location filter with polyline\n * const result = await ExifGallery.pick({\n * filter: {\n * location: {\n * polyline: [{lat: 48.1, lng: 11.5}, {lat: 48.2, lng: 11.6}],\n * radius: 1000\n * }\n * }\n * });\n *\n * // Time range filter\n * const result = await ExifGallery.pick({\n * filter: {\n * timeRange: {\n * start: new Date('2024-01-01'),\n * end: new Date('2024-01-31')\n * }\n * }\n * });\n *\n * // Combined filters\n * const result = await ExifGallery.pick({\n * filter: {\n * location: { coordinates: [{lat: 48.1, lng: 11.5}], radius: 500 },\n * timeRange: { start: new Date('2024-01-01'), end: new Date() }\n * },\n * fallbackThreshold: 10,\n * allowManualAdjustment: false\n * });\n *\n * // Handle result\n * if (!result.cancelled) {\n * console.log(`Selected ${result.images.length} images`);\n * for (const img of result.images) {\n * console.log(`${img.uri} - filtered by ${img.filteredBy}`);\n * if (img.exif) {\n * console.log(` GPS: ${img.exif.lat}, ${img.exif.lng}`);\n * console.log(` Time: ${img.exif.timestamp}`);\n * }\n * }\n * }\n * ```\n */\n pick(options?: PickOptions): Promise<PickResult>;\n}\n"]}