@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,176 @@
1
+ import type { ExifGalleryPlugin, InitConfig, PickOptions, PickResult } from './definitions';
2
+ /**
3
+ * TypeScript implementation of ExifGalleryPlugin.
4
+ *
5
+ * Handles:
6
+ * - Translation loading and merging
7
+ * - Plugin state management
8
+ * - Validation
9
+ * - Native bridge communication
10
+ *
11
+ * @internal
12
+ */
13
+ export declare class ExifGalleryImpl implements ExifGalleryPlugin {
14
+ /**
15
+ * Native plugin instance for Capacitor Bridge communication.
16
+ * @private
17
+ */
18
+ private nativePlugin;
19
+ /**
20
+ * Create ExifGalleryImpl instance with native bridge dependency.
21
+ *
22
+ * @param nativePlugin - Native plugin instance from Capacitor Bridge
23
+ */
24
+ constructor(nativePlugin: ExifGalleryPlugin);
25
+ /**
26
+ * Initialize the plugin with optional configuration.
27
+ *
28
+ * This method performs the following steps:
29
+ * 1. Language Detection: Detects system language via navigator.language
30
+ * (e.g., 'de-DE' → 'de'), or uses explicit config.locale
31
+ * 2. Default Loading: Loads appropriate translation file (en.json, de.json, fr.json, es.json)
32
+ * 3. Fallback: Falls back to English if system language is not supported
33
+ * 4. Merging: Merges config.customTexts on top of default translations
34
+ * 5. Validation: Validates locale and customTexts keys
35
+ * 6. Native Bridge: Passes translations and permissions to native layers
36
+ * 7. State Update: Stores merged translations in PluginState singleton (only after native success)
37
+ *
38
+ * All InitConfig properties are optional:
39
+ * - locale?: 'en' | 'de' | 'fr' | 'es' - Explicit language override
40
+ * - customTexts?: Partial<TranslationSet> - Custom text overrides
41
+ * - requestPermissionsUpfront?: boolean - Request Photo Library permissions immediately
42
+ *
43
+ * Permission Behavior:
44
+ * - If requestPermissionsUpfront is true, native layers request Photo Library access
45
+ * - The promise resolves regardless of whether permission was granted or denied
46
+ * - If permission is denied, pick() will request it again when called
47
+ * - Location permissions are NEVER requested upfront (only when filter uses location)
48
+ * - Check logs for permission grant/deny status on Android
49
+ *
50
+ * Placeholder Syntax:
51
+ * - {count}: Current selection count (e.g., "3")
52
+ * - {total}: Total available items (e.g., "24")
53
+ * - Example: "{count} of {total} selected" → "3 of 24 selected"
54
+ *
55
+ * @param config - Optional initialization configuration
56
+ * @returns Promise that resolves when initialization is complete
57
+ * @throws {Error} If locale is invalid (not 'en' | 'de' | 'fr' | 'es')
58
+ * @throws {Error} If customTexts contains invalid keys
59
+ * @throws {Error} If native initialization fails
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * // System language detection (navigator.language)
64
+ * await ExifGallery.initialize();
65
+ *
66
+ * // Explicit locale
67
+ * await ExifGallery.initialize({ locale: 'de' });
68
+ *
69
+ * // Custom text overrides with system language detection
70
+ * await ExifGallery.initialize({
71
+ * customTexts: {
72
+ * galleryTitle: 'Pick Your Photos',
73
+ * confirmButton: 'Done',
74
+ * },
75
+ * });
76
+ *
77
+ * // Explicit locale + custom overrides
78
+ * await ExifGallery.initialize({
79
+ * locale: 'en',
80
+ * customTexts: {
81
+ * galleryTitle: 'Choose Images',
82
+ * },
83
+ * });
84
+ *
85
+ * // Request permissions upfront
86
+ * await ExifGallery.initialize({
87
+ * requestPermissionsUpfront: true,
88
+ * });
89
+ * ```
90
+ */
91
+ initialize(config?: InitConfig): Promise<void>;
92
+ /**
93
+ * Open native gallery with optional filter configuration.
94
+ *
95
+ * This method performs the following steps:
96
+ * 1. Initialization Check: Verifies initialize() was called
97
+ * 2. Concurrent Check: Prevents opening multiple pickers simultaneously
98
+ * 3. Filter Validation: Validates all filter parameters
99
+ * 4. Default Values: Applies defaults for fallbackThreshold and allowManualAdjustment
100
+ * 5. Native Bridge: Calls native layer with filter configuration
101
+ * 6. Result Handling: Returns PickResult with selected images
102
+ *
103
+ * Filter Options:
104
+ * - filter.location.polyline: Array of LatLng defining a path (min 2 points)
105
+ * - filter.location.coordinates: Array of LatLng for specific locations
106
+ * - filter.location.radius: Search radius in meters (default: 100, must be > 0)
107
+ * - filter.timeRange.start: Start date/time (must be before end)
108
+ * - filter.timeRange.end: End date/time (must be after start)
109
+ * - fallbackThreshold: Minimum images to show filter UI (default: 5)
110
+ * - allowManualAdjustment: Allow user to adjust filters (default: true)
111
+ *
112
+ * @param options - Optional picker configuration
113
+ * @returns Promise<PickResult> with selected images array and cancelled flag
114
+ * @throws {Error} 'initialization_required' if initialize() not called
115
+ * @throws {Error} 'picker_in_progress' if picker is already open
116
+ * @throws {Error} 'filter_error' if filter parameters are invalid
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * // Simple pick without filters
121
+ * const result = await ExifGallery.pick();
122
+ *
123
+ * // Location filter with coordinates
124
+ * const result = await ExifGallery.pick({
125
+ * filter: {
126
+ * location: {
127
+ * coordinates: [{ lat: 48.8566, lng: 2.3522 }], // Paris
128
+ * radius: 500 // 500 meters
129
+ * }
130
+ * }
131
+ * });
132
+ *
133
+ * // Time range filter
134
+ * const result = await ExifGallery.pick({
135
+ * filter: {
136
+ * timeRange: {
137
+ * start: new Date('2024-01-01'),
138
+ * end: new Date('2024-12-31')
139
+ * }
140
+ * }
141
+ * });
142
+ *
143
+ * // Combined filters with custom options
144
+ * const result = await ExifGallery.pick({
145
+ * filter: {
146
+ * location: {
147
+ * polyline: [
148
+ * { lat: 48.8566, lng: 2.3522 },
149
+ * { lat: 48.8606, lng: 2.3376 }
150
+ * ],
151
+ * radius: 200
152
+ * },
153
+ * timeRange: {
154
+ * start: new Date('2024-06-01'),
155
+ * end: new Date('2024-06-30')
156
+ * }
157
+ * },
158
+ * fallbackThreshold: 10,
159
+ * allowManualAdjustment: false
160
+ * });
161
+ * ```
162
+ */
163
+ pick(options?: PickOptions): Promise<PickResult>;
164
+ /**
165
+ * Detects if active filters are present in pick options.
166
+ *
167
+ * **Story 8.2:** Used to determine filter button visibility.
168
+ *
169
+ * Note: This is called AFTER FilterValidator.validateFilterConfig(),
170
+ * so encoded polyline strings have already been decoded to arrays.
171
+ *
172
+ * @param options - Pick options to check
173
+ * @returns true if location or time filters are present
174
+ */
175
+ private hasActiveFilters;
176
+ }
@@ -0,0 +1,295 @@
1
+ import { FilterValidator } from './FilterValidator';
2
+ import { PluginState } from './PluginState';
3
+ import { TranslationLoader } from './TranslationLoader';
4
+ import { InitializationRequiredError, PickerInProgressError, FilterError } from './errors';
5
+ /**
6
+ * TypeScript implementation of ExifGalleryPlugin.
7
+ *
8
+ * Handles:
9
+ * - Translation loading and merging
10
+ * - Plugin state management
11
+ * - Validation
12
+ * - Native bridge communication
13
+ *
14
+ * @internal
15
+ */
16
+ export class ExifGalleryImpl {
17
+ /**
18
+ * Create ExifGalleryImpl instance with native bridge dependency.
19
+ *
20
+ * @param nativePlugin - Native plugin instance from Capacitor Bridge
21
+ */
22
+ constructor(nativePlugin) {
23
+ this.nativePlugin = nativePlugin;
24
+ }
25
+ /**
26
+ * Initialize the plugin with optional configuration.
27
+ *
28
+ * This method performs the following steps:
29
+ * 1. Language Detection: Detects system language via navigator.language
30
+ * (e.g., 'de-DE' → 'de'), or uses explicit config.locale
31
+ * 2. Default Loading: Loads appropriate translation file (en.json, de.json, fr.json, es.json)
32
+ * 3. Fallback: Falls back to English if system language is not supported
33
+ * 4. Merging: Merges config.customTexts on top of default translations
34
+ * 5. Validation: Validates locale and customTexts keys
35
+ * 6. Native Bridge: Passes translations and permissions to native layers
36
+ * 7. State Update: Stores merged translations in PluginState singleton (only after native success)
37
+ *
38
+ * All InitConfig properties are optional:
39
+ * - locale?: 'en' | 'de' | 'fr' | 'es' - Explicit language override
40
+ * - customTexts?: Partial<TranslationSet> - Custom text overrides
41
+ * - requestPermissionsUpfront?: boolean - Request Photo Library permissions immediately
42
+ *
43
+ * Permission Behavior:
44
+ * - If requestPermissionsUpfront is true, native layers request Photo Library access
45
+ * - The promise resolves regardless of whether permission was granted or denied
46
+ * - If permission is denied, pick() will request it again when called
47
+ * - Location permissions are NEVER requested upfront (only when filter uses location)
48
+ * - Check logs for permission grant/deny status on Android
49
+ *
50
+ * Placeholder Syntax:
51
+ * - {count}: Current selection count (e.g., "3")
52
+ * - {total}: Total available items (e.g., "24")
53
+ * - Example: "{count} of {total} selected" → "3 of 24 selected"
54
+ *
55
+ * @param config - Optional initialization configuration
56
+ * @returns Promise that resolves when initialization is complete
57
+ * @throws {Error} If locale is invalid (not 'en' | 'de' | 'fr' | 'es')
58
+ * @throws {Error} If customTexts contains invalid keys
59
+ * @throws {Error} If native initialization fails
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * // System language detection (navigator.language)
64
+ * await ExifGallery.initialize();
65
+ *
66
+ * // Explicit locale
67
+ * await ExifGallery.initialize({ locale: 'de' });
68
+ *
69
+ * // Custom text overrides with system language detection
70
+ * await ExifGallery.initialize({
71
+ * customTexts: {
72
+ * galleryTitle: 'Pick Your Photos',
73
+ * confirmButton: 'Done',
74
+ * },
75
+ * });
76
+ *
77
+ * // Explicit locale + custom overrides
78
+ * await ExifGallery.initialize({
79
+ * locale: 'en',
80
+ * customTexts: {
81
+ * galleryTitle: 'Choose Images',
82
+ * },
83
+ * });
84
+ *
85
+ * // Request permissions upfront
86
+ * await ExifGallery.initialize({
87
+ * requestPermissionsUpfront: true,
88
+ * });
89
+ * ```
90
+ */
91
+ async initialize(config) {
92
+ var _a;
93
+ const state = PluginState.getInstance();
94
+ // Load and merge translations (async: detects device language)
95
+ const mergedTranslations = await TranslationLoader.loadTranslations(config === null || config === void 0 ? void 0 : config.locale, config === null || config === void 0 ? void 0 : config.customTexts);
96
+ const requestPermissionsUpfront = (_a = config === null || config === void 0 ? void 0 : config.requestPermissionsUpfront) !== null && _a !== void 0 ? _a : false;
97
+ // Call native layer FIRST (before updating state)
98
+ // If native call fails, state remains unchanged
99
+ await this.nativePlugin.initialize({
100
+ locale: undefined, // Not needed, we pass full translations
101
+ customTexts: mergedTranslations, // Full merged TranslationSet
102
+ requestPermissionsUpfront,
103
+ });
104
+ // Only update state after native confirmation
105
+ state.setRequestPermissionsUpfront(requestPermissionsUpfront);
106
+ state.setMergedTranslations(mergedTranslations);
107
+ state.setInitialized(true);
108
+ }
109
+ /**
110
+ * Open native gallery with optional filter configuration.
111
+ *
112
+ * This method performs the following steps:
113
+ * 1. Initialization Check: Verifies initialize() was called
114
+ * 2. Concurrent Check: Prevents opening multiple pickers simultaneously
115
+ * 3. Filter Validation: Validates all filter parameters
116
+ * 4. Default Values: Applies defaults for fallbackThreshold and allowManualAdjustment
117
+ * 5. Native Bridge: Calls native layer with filter configuration
118
+ * 6. Result Handling: Returns PickResult with selected images
119
+ *
120
+ * Filter Options:
121
+ * - filter.location.polyline: Array of LatLng defining a path (min 2 points)
122
+ * - filter.location.coordinates: Array of LatLng for specific locations
123
+ * - filter.location.radius: Search radius in meters (default: 100, must be > 0)
124
+ * - filter.timeRange.start: Start date/time (must be before end)
125
+ * - filter.timeRange.end: End date/time (must be after start)
126
+ * - fallbackThreshold: Minimum images to show filter UI (default: 5)
127
+ * - allowManualAdjustment: Allow user to adjust filters (default: true)
128
+ *
129
+ * @param options - Optional picker configuration
130
+ * @returns Promise<PickResult> with selected images array and cancelled flag
131
+ * @throws {Error} 'initialization_required' if initialize() not called
132
+ * @throws {Error} 'picker_in_progress' if picker is already open
133
+ * @throws {Error} 'filter_error' if filter parameters are invalid
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * // Simple pick without filters
138
+ * const result = await ExifGallery.pick();
139
+ *
140
+ * // Location filter with coordinates
141
+ * const result = await ExifGallery.pick({
142
+ * filter: {
143
+ * location: {
144
+ * coordinates: [{ lat: 48.8566, lng: 2.3522 }], // Paris
145
+ * radius: 500 // 500 meters
146
+ * }
147
+ * }
148
+ * });
149
+ *
150
+ * // Time range filter
151
+ * const result = await ExifGallery.pick({
152
+ * filter: {
153
+ * timeRange: {
154
+ * start: new Date('2024-01-01'),
155
+ * end: new Date('2024-12-31')
156
+ * }
157
+ * }
158
+ * });
159
+ *
160
+ * // Combined filters with custom options
161
+ * const result = await ExifGallery.pick({
162
+ * filter: {
163
+ * location: {
164
+ * polyline: [
165
+ * { lat: 48.8566, lng: 2.3522 },
166
+ * { lat: 48.8606, lng: 2.3376 }
167
+ * ],
168
+ * radius: 200
169
+ * },
170
+ * timeRange: {
171
+ * start: new Date('2024-06-01'),
172
+ * end: new Date('2024-06-30')
173
+ * }
174
+ * },
175
+ * fallbackThreshold: 10,
176
+ * allowManualAdjustment: false
177
+ * });
178
+ * ```
179
+ */
180
+ async pick(options) {
181
+ var _a, _b, _c, _d;
182
+ const state = PluginState.getInstance();
183
+ // 1. Verify plugin is initialized
184
+ if (!state.isInitialized()) {
185
+ throw new InitializationRequiredError();
186
+ }
187
+ // 2. Prevent concurrent picker operations (atomic check-and-set)
188
+ if (!state.trySetPickerInProgress()) {
189
+ throw new PickerInProgressError();
190
+ }
191
+ try {
192
+ // Picker is now marked as in progress (atomic operation above)
193
+ // 3. Validate filter configuration if provided
194
+ if (options === null || options === void 0 ? void 0 : options.filter) {
195
+ FilterValidator.validateFilterConfig(options.filter);
196
+ }
197
+ // 4. Apply default values and convert Date objects to timestamps
198
+ const pickOptions = {
199
+ fallbackThreshold: (_a = options === null || options === void 0 ? void 0 : options.fallbackThreshold) !== null && _a !== void 0 ? _a : 5,
200
+ allowManualAdjustment: (_b = options === null || options === void 0 ? void 0 : options.allowManualAdjustment) !== null && _b !== void 0 ? _b : true,
201
+ hasActiveFilters: this.hasActiveFilters(options), // Story 8.2: Detect active filters
202
+ distanceUnit: (_c = options === null || options === void 0 ? void 0 : options.distanceUnit) !== null && _c !== void 0 ? _c : 'kilometers',
203
+ distanceStep: (_d = options === null || options === void 0 ? void 0 : options.distanceStep) !== null && _d !== void 0 ? _d : 5,
204
+ };
205
+ // Convert filter configuration for native bridge
206
+ if (options === null || options === void 0 ? void 0 : options.filter) {
207
+ pickOptions.filter = {};
208
+ // Copy location filter as-is
209
+ if (options.filter.location) {
210
+ pickOptions.filter.location = options.filter.location;
211
+ }
212
+ // Convert Date objects to timestamps (milliseconds) for native bridge
213
+ if (options.filter.timeRange) {
214
+ pickOptions.filter.timeRange = {
215
+ start: options.filter.timeRange.start.getTime(),
216
+ end: options.filter.timeRange.end.getTime(),
217
+ };
218
+ }
219
+ }
220
+ // Validate fallbackThreshold
221
+ if (pickOptions.fallbackThreshold !== undefined) {
222
+ if (typeof pickOptions.fallbackThreshold !== 'number' || !isFinite(pickOptions.fallbackThreshold)) {
223
+ throw new FilterError('fallbackThreshold must be a finite number');
224
+ }
225
+ if (pickOptions.fallbackThreshold < 0) {
226
+ throw new FilterError('fallbackThreshold must be greater than or equal to 0');
227
+ }
228
+ // Prevent unreasonably large threshold
229
+ if (pickOptions.fallbackThreshold > 10000) {
230
+ throw new FilterError('fallbackThreshold must not exceed 10,000');
231
+ }
232
+ }
233
+ // Validate allowManualAdjustment
234
+ if (pickOptions.allowManualAdjustment !== undefined) {
235
+ if (typeof pickOptions.allowManualAdjustment !== 'boolean') {
236
+ throw new FilterError('allowManualAdjustment must be a boolean');
237
+ }
238
+ }
239
+ // Validate distanceUnit
240
+ if (pickOptions.distanceUnit !== undefined) {
241
+ const validUnits = ['kilometers', 'miles'];
242
+ if (!validUnits.includes(pickOptions.distanceUnit)) {
243
+ throw new FilterError(`distanceUnit must be one of: ${validUnits.join(', ')}`);
244
+ }
245
+ }
246
+ // Validate distanceStep
247
+ if (pickOptions.distanceStep !== undefined) {
248
+ if (typeof pickOptions.distanceStep !== 'number' || !isFinite(pickOptions.distanceStep)) {
249
+ throw new FilterError('distanceStep must be a finite number');
250
+ }
251
+ if (pickOptions.distanceStep < 1) {
252
+ throw new FilterError('distanceStep must be at least 1 km');
253
+ }
254
+ if (pickOptions.distanceStep > 25) {
255
+ throw new FilterError('distanceStep must not exceed 25 km');
256
+ }
257
+ }
258
+ // 5. Call native layer via Capacitor Bridge
259
+ const result = await this.nativePlugin.pick(pickOptions);
260
+ return result;
261
+ }
262
+ finally {
263
+ // 6. Always clear picker in progress flag (success or error)
264
+ state.setPickerInProgress(false);
265
+ }
266
+ }
267
+ /**
268
+ * Detects if active filters are present in pick options.
269
+ *
270
+ * **Story 8.2:** Used to determine filter button visibility.
271
+ *
272
+ * Note: This is called AFTER FilterValidator.validateFilterConfig(),
273
+ * so encoded polyline strings have already been decoded to arrays.
274
+ *
275
+ * @param options - Pick options to check
276
+ * @returns true if location or time filters are present
277
+ */
278
+ hasActiveFilters(options) {
279
+ var _a, _b, _c, _d;
280
+ if (!(options === null || options === void 0 ? void 0 : options.filter))
281
+ return false;
282
+ // Check for location filter (coordinates array or polyline)
283
+ // Note: polyline can be string OR array, but after validation it's always an array
284
+ const polyline = (_a = options.filter.location) === null || _a === void 0 ? void 0 : _a.polyline;
285
+ const hasPolyline = Array.isArray(polyline)
286
+ ? polyline.length > 0
287
+ : typeof polyline === 'string'
288
+ ? polyline.trim().length > 0
289
+ : false;
290
+ const hasLocationFilter = !!(((_c = (_b = options.filter.location) === null || _b === void 0 ? void 0 : _b.coordinates) === null || _c === void 0 ? void 0 : _c.length) || hasPolyline);
291
+ const hasTimeFilter = !!(((_d = options.filter.timeRange) === null || _d === void 0 ? void 0 : _d.start) && options.filter.timeRange.end);
292
+ return hasLocationFilter || hasTimeFilter;
293
+ }
294
+ }
295
+ //# sourceMappingURL=ExifGalleryImpl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExifGalleryImpl.js","sourceRoot":"","sources":["../../src/ExifGalleryImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import { FilterValidator } from './FilterValidator';\nimport { PluginState } from './PluginState';\nimport { TranslationLoader } from './TranslationLoader';\nimport type { ExifGalleryPlugin, InitConfig, PickOptions, PickResult } from './definitions';\nimport { InitializationRequiredError, PickerInProgressError, FilterError } from './errors';\n\n/**\n * TypeScript implementation of ExifGalleryPlugin.\n *\n * Handles:\n * - Translation loading and merging\n * - Plugin state management\n * - Validation\n * - Native bridge communication\n *\n * @internal\n */\nexport class ExifGalleryImpl implements ExifGalleryPlugin {\n /**\n * Native plugin instance for Capacitor Bridge communication.\n * @private\n */\n private nativePlugin: ExifGalleryPlugin;\n\n /**\n * Create ExifGalleryImpl instance with native bridge dependency.\n *\n * @param nativePlugin - Native plugin instance from Capacitor Bridge\n */\n constructor(nativePlugin: ExifGalleryPlugin) {\n this.nativePlugin = nativePlugin;\n }\n /**\n * Initialize the plugin with optional configuration.\n *\n * This method performs the following steps:\n * 1. Language Detection: Detects system language via navigator.language\n * (e.g., 'de-DE' → 'de'), or uses explicit config.locale\n * 2. Default Loading: Loads appropriate translation file (en.json, de.json, fr.json, es.json)\n * 3. Fallback: Falls back to English if system language is not supported\n * 4. Merging: Merges config.customTexts on top of default translations\n * 5. Validation: Validates locale and customTexts keys\n * 6. Native Bridge: Passes translations and permissions to native layers\n * 7. State Update: Stores merged translations in PluginState singleton (only after native success)\n *\n * All InitConfig properties are optional:\n * - locale?: 'en' | 'de' | 'fr' | 'es' - Explicit language override\n * - customTexts?: Partial<TranslationSet> - Custom text overrides\n * - requestPermissionsUpfront?: boolean - Request Photo Library permissions immediately\n *\n * Permission Behavior:\n * - If requestPermissionsUpfront is true, native layers request Photo Library access\n * - The promise resolves regardless of whether permission was granted or denied\n * - If permission is denied, pick() will request it again when called\n * - Location permissions are NEVER requested upfront (only when filter uses location)\n * - Check logs for permission grant/deny status on Android\n *\n * Placeholder Syntax:\n * - {count}: Current selection count (e.g., \"3\")\n * - {total}: Total available items (e.g., \"24\")\n * - Example: \"{count} of {total} selected\" → \"3 of 24 selected\"\n *\n * @param config - Optional initialization configuration\n * @returns Promise that resolves when initialization is complete\n * @throws {Error} If locale is invalid (not 'en' | 'de' | 'fr' | 'es')\n * @throws {Error} If customTexts contains invalid keys\n * @throws {Error} If native initialization fails\n *\n * @example\n * ```typescript\n * // System language detection (navigator.language)\n * await ExifGallery.initialize();\n *\n * // Explicit locale\n * await ExifGallery.initialize({ locale: 'de' });\n *\n * // Custom text overrides with system language detection\n * await ExifGallery.initialize({\n * customTexts: {\n * galleryTitle: 'Pick Your Photos',\n * confirmButton: 'Done',\n * },\n * });\n *\n * // Explicit locale + custom overrides\n * await ExifGallery.initialize({\n * locale: 'en',\n * customTexts: {\n * galleryTitle: 'Choose Images',\n * },\n * });\n *\n * // Request permissions upfront\n * await ExifGallery.initialize({\n * requestPermissionsUpfront: true,\n * });\n * ```\n */\n async initialize(config?: InitConfig): Promise<void> {\n const state = PluginState.getInstance();\n\n // Load and merge translations (async: detects device language)\n const mergedTranslations = await TranslationLoader.loadTranslations(config?.locale, config?.customTexts);\n\n const requestPermissionsUpfront = config?.requestPermissionsUpfront ?? false;\n\n // Call native layer FIRST (before updating state)\n // If native call fails, state remains unchanged\n await this.nativePlugin.initialize({\n locale: undefined, // Not needed, we pass full translations\n customTexts: mergedTranslations, // Full merged TranslationSet\n requestPermissionsUpfront,\n });\n\n // Only update state after native confirmation\n state.setRequestPermissionsUpfront(requestPermissionsUpfront);\n state.setMergedTranslations(mergedTranslations);\n state.setInitialized(true);\n }\n\n /**\n * Open native gallery with optional filter configuration.\n *\n * This method performs the following steps:\n * 1. Initialization Check: Verifies initialize() was called\n * 2. Concurrent Check: Prevents opening multiple pickers simultaneously\n * 3. Filter Validation: Validates all filter parameters\n * 4. Default Values: Applies defaults for fallbackThreshold and allowManualAdjustment\n * 5. Native Bridge: Calls native layer with filter configuration\n * 6. Result Handling: Returns PickResult with selected images\n *\n * Filter Options:\n * - filter.location.polyline: Array of LatLng defining a path (min 2 points)\n * - filter.location.coordinates: Array of LatLng for specific locations\n * - filter.location.radius: Search radius in meters (default: 100, must be > 0)\n * - filter.timeRange.start: Start date/time (must be before end)\n * - filter.timeRange.end: End date/time (must be after start)\n * - fallbackThreshold: Minimum images to show filter UI (default: 5)\n * - allowManualAdjustment: Allow user to adjust filters (default: true)\n *\n * @param options - Optional picker configuration\n * @returns Promise<PickResult> with selected images array and cancelled flag\n * @throws {Error} 'initialization_required' if initialize() not called\n * @throws {Error} 'picker_in_progress' if picker is already open\n * @throws {Error} 'filter_error' if filter parameters are invalid\n *\n * @example\n * ```typescript\n * // Simple pick without filters\n * const result = await ExifGallery.pick();\n *\n * // Location filter with coordinates\n * const result = await ExifGallery.pick({\n * filter: {\n * location: {\n * coordinates: [{ lat: 48.8566, lng: 2.3522 }], // Paris\n * radius: 500 // 500 meters\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-12-31')\n * }\n * }\n * });\n *\n * // Combined filters with custom options\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: 200\n * },\n * timeRange: {\n * start: new Date('2024-06-01'),\n * end: new Date('2024-06-30')\n * }\n * },\n * fallbackThreshold: 10,\n * allowManualAdjustment: false\n * });\n * ```\n */\n async pick(options?: PickOptions): Promise<PickResult> {\n const state = PluginState.getInstance();\n\n // 1. Verify plugin is initialized\n if (!state.isInitialized()) {\n throw new InitializationRequiredError();\n }\n\n // 2. Prevent concurrent picker operations (atomic check-and-set)\n if (!state.trySetPickerInProgress()) {\n throw new PickerInProgressError();\n }\n\n try {\n // Picker is now marked as in progress (atomic operation above)\n\n // 3. Validate filter configuration if provided\n if (options?.filter) {\n FilterValidator.validateFilterConfig(options.filter);\n }\n\n // 4. Apply default values and convert Date objects to timestamps\n const pickOptions: any = {\n fallbackThreshold: options?.fallbackThreshold ?? 5,\n allowManualAdjustment: options?.allowManualAdjustment ?? true,\n hasActiveFilters: this.hasActiveFilters(options), // Story 8.2: Detect active filters\n distanceUnit: options?.distanceUnit ?? 'kilometers',\n distanceStep: options?.distanceStep ?? 5,\n };\n\n // Convert filter configuration for native bridge\n if (options?.filter) {\n pickOptions.filter = {};\n\n // Copy location filter as-is\n if (options.filter.location) {\n pickOptions.filter.location = options.filter.location;\n }\n\n // Convert Date objects to timestamps (milliseconds) for native bridge\n if (options.filter.timeRange) {\n pickOptions.filter.timeRange = {\n start: options.filter.timeRange.start.getTime(),\n end: options.filter.timeRange.end.getTime(),\n };\n }\n }\n\n // Validate fallbackThreshold\n if (pickOptions.fallbackThreshold !== undefined) {\n if (typeof pickOptions.fallbackThreshold !== 'number' || !isFinite(pickOptions.fallbackThreshold)) {\n throw new FilterError('fallbackThreshold must be a finite number');\n }\n if (pickOptions.fallbackThreshold < 0) {\n throw new FilterError('fallbackThreshold must be greater than or equal to 0');\n }\n // Prevent unreasonably large threshold\n if (pickOptions.fallbackThreshold > 10000) {\n throw new FilterError('fallbackThreshold must not exceed 10,000');\n }\n }\n\n // Validate allowManualAdjustment\n if (pickOptions.allowManualAdjustment !== undefined) {\n if (typeof pickOptions.allowManualAdjustment !== 'boolean') {\n throw new FilterError('allowManualAdjustment must be a boolean');\n }\n }\n\n // Validate distanceUnit\n if (pickOptions.distanceUnit !== undefined) {\n const validUnits = ['kilometers', 'miles'];\n if (!validUnits.includes(pickOptions.distanceUnit)) {\n throw new FilterError(`distanceUnit must be one of: ${validUnits.join(', ')}`);\n }\n }\n\n // Validate distanceStep\n if (pickOptions.distanceStep !== undefined) {\n if (typeof pickOptions.distanceStep !== 'number' || !isFinite(pickOptions.distanceStep)) {\n throw new FilterError('distanceStep must be a finite number');\n }\n if (pickOptions.distanceStep < 1) {\n throw new FilterError('distanceStep must be at least 1 km');\n }\n if (pickOptions.distanceStep > 25) {\n throw new FilterError('distanceStep must not exceed 25 km');\n }\n }\n\n // 5. Call native layer via Capacitor Bridge\n const result = await this.nativePlugin.pick(pickOptions);\n\n return result;\n } finally {\n // 6. Always clear picker in progress flag (success or error)\n state.setPickerInProgress(false);\n }\n }\n\n /**\n * Detects if active filters are present in pick options.\n *\n * **Story 8.2:** Used to determine filter button visibility.\n *\n * Note: This is called AFTER FilterValidator.validateFilterConfig(),\n * so encoded polyline strings have already been decoded to arrays.\n *\n * @param options - Pick options to check\n * @returns true if location or time filters are present\n */\n private hasActiveFilters(options?: PickOptions): boolean {\n if (!options?.filter) return false;\n\n // Check for location filter (coordinates array or polyline)\n // Note: polyline can be string OR array, but after validation it's always an array\n const polyline = options.filter.location?.polyline;\n const hasPolyline = Array.isArray(polyline)\n ? polyline.length > 0\n : typeof polyline === 'string'\n ? polyline.trim().length > 0\n : false;\n\n const hasLocationFilter = !!(options.filter.location?.coordinates?.length || hasPolyline);\n\n const hasTimeFilter = !!(options.filter.timeRange?.start && options.filter.timeRange.end);\n\n return hasLocationFilter || hasTimeFilter;\n }\n}\n"]}
@@ -0,0 +1,126 @@
1
+ import type { FilterConfig, LatLng, LocationFilter, TimeRangeFilter } from './definitions';
2
+ /**
3
+ * Validation utilities for filter parameters.
4
+ *
5
+ * Ensures filter configurations are valid before passing to native layers.
6
+ *
7
+ * @internal
8
+ */
9
+ export declare class FilterValidator {
10
+ /**
11
+ * Validate that an object is a valid LatLng coordinate.
12
+ *
13
+ * Valid coordinates must have:
14
+ * - lat: number between -90 and 90
15
+ * - lng: number between -180 and 180
16
+ *
17
+ * @param obj - Object to validate
18
+ * @returns True if valid LatLng
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * isValidLatLng({ lat: 48.8566, lng: 2.3522 }); // true
23
+ * isValidLatLng({ lat: 200, lng: 0 }); // false (lat out of range)
24
+ * isValidLatLng({ lat: 0 }); // false (missing lng)
25
+ * ```
26
+ */
27
+ static isValidLatLng(obj: any): obj is LatLng;
28
+ /**
29
+ * Validate location filter configuration.
30
+ *
31
+ * Valid location filter must have:
32
+ * - Either polyline OR coordinates array (not both)
33
+ * - polyline/coordinates must be non-empty array of valid LatLng
34
+ * - radius must be > 0 if provided
35
+ *
36
+ * ⚠️ **MUTATION WARNING:** If polyline is provided as an encoded string,
37
+ * it will be decoded and replaced with a LatLng[] array in-place.
38
+ * This is an optimization to prevent double-decoding. If you need to
39
+ * preserve the original filter object, pass a deep copy.
40
+ *
41
+ * @mutates locationFilter.polyline - Replaces encoded string with decoded LatLng[]
42
+ * @param locationFilter - Location filter to validate
43
+ * @throws {Error} If validation fails
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Valid - coordinate array
48
+ * validateLocationFilter({
49
+ * coordinates: [{ lat: 48.8566, lng: 2.3522 }],
50
+ * radius: 500
51
+ * });
52
+ *
53
+ * // Valid - encoded polyline (will be decoded in-place)
54
+ * const filter = { polyline: "_p~iF~ps|U_ulLnnqC", radius: 1000 };
55
+ * validateLocationFilter(filter);
56
+ * // Note: filter.polyline is now LatLng[] (not string)
57
+ *
58
+ * // Invalid: empty coordinates array
59
+ * validateLocationFilter({ coordinates: [] }); // throws
60
+ *
61
+ * // Invalid: negative radius
62
+ * validateLocationFilter({ coordinates: [...], radius: -1 }); // throws
63
+ * ```
64
+ */
65
+ static validateLocationFilter(locationFilter: LocationFilter): void;
66
+ /**
67
+ * Validate time range filter configuration.
68
+ *
69
+ * Valid time range must have:
70
+ * - start and end as Date objects
71
+ * - start must be before end
72
+ *
73
+ * @param timeRange - Time range filter to validate
74
+ * @throws {Error} If validation fails
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * // Valid
79
+ * validateTimeRangeFilter({
80
+ * start: new Date('2024-01-01'),
81
+ * end: new Date('2024-12-31')
82
+ * });
83
+ *
84
+ * // Invalid: end before start
85
+ * validateTimeRangeFilter({
86
+ * start: new Date('2024-12-31'),
87
+ * end: new Date('2024-01-01')
88
+ * }); // throws
89
+ * ```
90
+ */
91
+ static validateTimeRangeFilter(timeRange: TimeRangeFilter): void;
92
+ /**
93
+ * Validate complete filter configuration.
94
+ *
95
+ * Valid filter must have:
96
+ * - At least one of: location or timeRange
97
+ * - Valid location filter (if present)
98
+ * - Valid time range filter (if present)
99
+ *
100
+ * @param filter - Filter configuration to validate
101
+ * @throws {Error} If validation fails
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // Valid: location only
106
+ * validateFilterConfig({
107
+ * location: { coordinates: [{ lat: 0, lng: 0 }] }
108
+ * });
109
+ *
110
+ * // Valid: time only
111
+ * validateFilterConfig({
112
+ * timeRange: { start: new Date(), end: new Date() }
113
+ * });
114
+ *
115
+ * // Valid: both
116
+ * validateFilterConfig({
117
+ * location: { coordinates: [...] },
118
+ * timeRange: { start: ..., end: ... }
119
+ * });
120
+ *
121
+ * // Invalid: neither
122
+ * validateFilterConfig({}); // throws
123
+ * ```
124
+ */
125
+ static validateFilterConfig(filter: FilterConfig): void;
126
+ }