@kesbyte/capacitor-exif-gallery 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapacitorExifGallery.podspec +17 -0
- package/LICENSE +8 -0
- package/Package.swift +28 -0
- package/README.md +813 -0
- package/dist/esm/ExifGalleryImpl.d.ts +176 -0
- package/dist/esm/ExifGalleryImpl.js +295 -0
- package/dist/esm/ExifGalleryImpl.js.map +1 -0
- package/dist/esm/FilterValidator.d.ts +126 -0
- package/dist/esm/FilterValidator.js +274 -0
- package/dist/esm/FilterValidator.js.map +1 -0
- package/dist/esm/PluginState.d.ts +128 -0
- package/dist/esm/PluginState.js +166 -0
- package/dist/esm/PluginState.js.map +1 -0
- package/dist/esm/PolylineDecoder.d.ts +23 -0
- package/dist/esm/PolylineDecoder.js +34 -0
- package/dist/esm/PolylineDecoder.js.map +1 -0
- package/dist/esm/TranslationLoader.d.ts +140 -0
- package/dist/esm/TranslationLoader.js +218 -0
- package/dist/esm/TranslationLoader.js.map +1 -0
- package/dist/esm/definitions.d.ts +539 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/errors.d.ts +252 -0
- package/dist/esm/errors.js +276 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +40 -0
- package/dist/esm/index.js +42 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/translations/de.json +20 -0
- package/dist/esm/translations/en.json +20 -0
- package/dist/esm/translations/es.json +20 -0
- package/dist/esm/translations/fr.json +20 -0
- package/dist/esm/web.d.ts +6 -0
- package/dist/esm/web.js +14 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +1820 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +1823 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +121 -0
|
@@ -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
|
+
}
|