@javascriptcommon/react-native-carplay 2.4.5 → 2.4.8
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/README.md +633 -0
- package/ios/RNCarPlay.h +1 -0
- package/ios/RNCarPlay.m +576 -121
- package/lib/templates/ListTemplate.d.ts +16 -0
- package/lib/templates/ListTemplate.d.ts.map +1 -1
- package/lib/templates/ListTemplate.js +5 -0
- package/lib/templates/Template.d.ts.map +1 -1
- package/lib/templates/Template.js +0 -4
- package/package.json +1 -1
- package/src/templates/ListTemplate.ts +24 -0
- package/src/templates/Template.ts +0 -4
package/ios/RNCarPlay.m
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
#import "RNCarPlayViewController.h"
|
|
3
3
|
#import <React/RCTConvert.h>
|
|
4
4
|
#import <React/RCTRootView.h>
|
|
5
|
+
#import <React/RCTImageSource.h>
|
|
5
6
|
|
|
6
7
|
@implementation RNCarPlay
|
|
7
8
|
{
|
|
8
9
|
bool hasListeners;
|
|
10
|
+
NSCache<NSString *, UIImage *> *_imageCache;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
@synthesize interfaceController;
|
|
@@ -59,6 +61,8 @@ RCT_EXPORT_MODULE();
|
|
|
59
61
|
static dispatch_once_t onceToken;
|
|
60
62
|
dispatch_once(&onceToken, ^{
|
|
61
63
|
sharedInstance = [super allocWithZone:zone];
|
|
64
|
+
sharedInstance->_imageCache = [[NSCache alloc] init];
|
|
65
|
+
sharedInstance->_imageCache.countLimit = 100; // Cache up to 100 images
|
|
62
66
|
});
|
|
63
67
|
return sharedInstance;
|
|
64
68
|
}
|
|
@@ -84,6 +88,7 @@ RCT_EXPORT_MODULE();
|
|
|
84
88
|
// list
|
|
85
89
|
@"didSelectListItem",
|
|
86
90
|
@"didSelectListItemRowImage",
|
|
91
|
+
@"scrollToBottom",
|
|
87
92
|
// search
|
|
88
93
|
@"updatedSearchText",
|
|
89
94
|
@"searchButtonPressed",
|
|
@@ -166,69 +171,191 @@ RCT_EXPORT_MODULE();
|
|
|
166
171
|
return resizedImage;
|
|
167
172
|
}
|
|
168
173
|
|
|
174
|
+
// Helper method to load image asynchronously using React Native's image loader
|
|
175
|
+
- (void)loadImageAsync:(id)imageSource completion:(void (^)(UIImage *image))completion {
|
|
176
|
+
if (imageSource == nil || [imageSource isKindOfClass:[NSNull class]]) {
|
|
177
|
+
if (completion) completion(nil);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check if it's a dictionary with a 'uri' key (React Native image source)
|
|
182
|
+
if ([imageSource isKindOfClass:[NSDictionary class]]) {
|
|
183
|
+
NSDictionary *imageDict = (NSDictionary *)imageSource;
|
|
184
|
+
NSString *uri = imageDict[@"uri"];
|
|
185
|
+
|
|
186
|
+
if (uri) {
|
|
187
|
+
// Check cache first
|
|
188
|
+
UIImage *cachedImage = [_imageCache objectForKey:uri];
|
|
189
|
+
if (cachedImage) {
|
|
190
|
+
if (completion) completion(cachedImage);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Use React Native's image loader for proper async loading with caching
|
|
195
|
+
RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]];
|
|
196
|
+
RCTImageSource *source = [[RCTImageSource alloc] initWithURLRequest:[RCTConvert NSURLRequest:imageSource] size:CGSizeZero scale:1];
|
|
197
|
+
|
|
198
|
+
[imageLoader loadImageWithURLRequest:source.request callback:^(NSError *error, UIImage *image) {
|
|
199
|
+
if (image) {
|
|
200
|
+
[self->_imageCache setObject:image forKey:uri];
|
|
201
|
+
}
|
|
202
|
+
if (completion) completion(image);
|
|
203
|
+
}];
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Not a URL, use synchronous loading (local assets)
|
|
209
|
+
UIImage *image = [RCTConvert UIImage:imageSource];
|
|
210
|
+
if (completion) completion(image);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Helper to load multiple images asynchronously
|
|
214
|
+
- (void)loadImagesAsync:(NSArray *)imageSources completion:(void (^)(NSArray<UIImage *> *images))completion {
|
|
215
|
+
if (imageSources == nil || imageSources.count == 0) {
|
|
216
|
+
if (completion) completion(@[]);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
NSMutableArray<UIImage *> *results = [NSMutableArray arrayWithCapacity:imageSources.count];
|
|
221
|
+
for (NSUInteger i = 0; i < imageSources.count; i++) {
|
|
222
|
+
[results addObject:[NSNull null]]; // Placeholder
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
__block NSUInteger completedCount = 0;
|
|
226
|
+
NSUInteger totalCount = imageSources.count;
|
|
227
|
+
|
|
228
|
+
for (NSUInteger i = 0; i < imageSources.count; i++) {
|
|
229
|
+
id source = imageSources[i];
|
|
230
|
+
[self loadImageAsync:source completion:^(UIImage *image) {
|
|
231
|
+
if (image) {
|
|
232
|
+
results[i] = image;
|
|
233
|
+
}
|
|
234
|
+
completedCount++;
|
|
235
|
+
if (completedCount == totalCount) {
|
|
236
|
+
// Remove NSNull placeholders
|
|
237
|
+
NSMutableArray<UIImage *> *finalResults = [NSMutableArray array];
|
|
238
|
+
for (id obj in results) {
|
|
239
|
+
if ([obj isKindOfClass:[UIImage class]]) {
|
|
240
|
+
[finalResults addObject:obj];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (completion) completion(finalResults);
|
|
244
|
+
}
|
|
245
|
+
}];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Helper to load images for NowPlayingButtons - maintains order, returns array with images or NSNull for failed loads
|
|
250
|
+
- (void)loadImagesForNowPlayingButtons:(NSArray *)imageSources completion:(void (^)(NSArray *images))completion {
|
|
251
|
+
if (imageSources == nil || imageSources.count == 0) {
|
|
252
|
+
if (completion) completion(@[]);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
NSMutableArray *results = [NSMutableArray arrayWithCapacity:imageSources.count];
|
|
257
|
+
for (NSUInteger i = 0; i < imageSources.count; i++) {
|
|
258
|
+
[results addObject:[NSNull null]]; // Placeholder
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
__block NSUInteger completedCount = 0;
|
|
262
|
+
NSUInteger totalCount = imageSources.count;
|
|
263
|
+
|
|
264
|
+
for (NSUInteger i = 0; i < imageSources.count; i++) {
|
|
265
|
+
id source = imageSources[i];
|
|
266
|
+
if ([source isKindOfClass:[NSNull class]]) {
|
|
267
|
+
completedCount++;
|
|
268
|
+
if (completedCount == totalCount && completion) {
|
|
269
|
+
completion([results copy]);
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
[self loadImageAsync:source completion:^(UIImage *image) {
|
|
275
|
+
if (image) {
|
|
276
|
+
results[i] = image;
|
|
277
|
+
}
|
|
278
|
+
// NSNull remains for failed loads
|
|
279
|
+
completedCount++;
|
|
280
|
+
if (completedCount == totalCount && completion) {
|
|
281
|
+
completion([results copy]);
|
|
282
|
+
}
|
|
283
|
+
}];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
169
287
|
- (void)updateItemImageWithURL:(CPListItem *)item imgUrl:(NSString *)imgUrlString placeholderImage:(UIImage *)placeholderImage {
|
|
288
|
+
// Check cache first
|
|
289
|
+
UIImage *cachedImage = [_imageCache objectForKey:imgUrlString];
|
|
290
|
+
if (cachedImage) {
|
|
291
|
+
[item setImage:cachedImage];
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
170
295
|
if (placeholderImage != nil) {
|
|
171
|
-
|
|
172
|
-
[item setImage:placeholderImage];
|
|
173
|
-
});
|
|
296
|
+
[item setImage:placeholderImage];
|
|
174
297
|
}
|
|
175
298
|
|
|
176
|
-
|
|
299
|
+
// Use React Native's image loader
|
|
300
|
+
RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]];
|
|
301
|
+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imgUrlString]];
|
|
177
302
|
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} else {
|
|
185
|
-
NSLog(@"Failed to load image from URL: %@", imgUrl);
|
|
303
|
+
[imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
|
|
304
|
+
if (image) {
|
|
305
|
+
[self->_imageCache setObject:image forKey:imgUrlString];
|
|
306
|
+
[item setImage:image];
|
|
307
|
+
} else if (error) {
|
|
308
|
+
NSLog(@"Failed to load image from URL: %@ - %@", imgUrlString, error);
|
|
186
309
|
}
|
|
187
310
|
}];
|
|
188
|
-
[task resume];
|
|
189
311
|
}
|
|
190
312
|
|
|
191
313
|
- (void)updateListRowItemImageWithURL:(CPListImageRowItem *)item imgUrl:(NSString *)imgUrlString index:(int)index placeholderImage:(UIImage *)placeholderImage {
|
|
314
|
+
// Check cache first
|
|
315
|
+
UIImage *cachedImage = [_imageCache objectForKey:imgUrlString];
|
|
316
|
+
if (cachedImage) {
|
|
317
|
+
NSMutableArray* newImages = [item.gridImages mutableCopy];
|
|
318
|
+
@try {
|
|
319
|
+
newImages[index] = cachedImage;
|
|
320
|
+
[item updateImages:newImages];
|
|
321
|
+
}
|
|
322
|
+
@catch (NSException *exception) {
|
|
323
|
+
NSLog(@"Failed to update images array of CPListImageRowItem");
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
192
328
|
if (placeholderImage != nil) {
|
|
193
|
-
|
|
329
|
+
NSMutableArray* newImages = [item.gridImages mutableCopy];
|
|
330
|
+
@try {
|
|
331
|
+
newImages[index] = placeholderImage;
|
|
332
|
+
[item updateImages:newImages];
|
|
333
|
+
}
|
|
334
|
+
@catch (NSException *exception) {
|
|
335
|
+
NSLog(@"Failed to update images array of CPListImageRowItem");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Use React Native's image loader
|
|
340
|
+
RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]];
|
|
341
|
+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imgUrlString]];
|
|
342
|
+
|
|
343
|
+
[imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
|
|
344
|
+
if (image) {
|
|
345
|
+
[self->_imageCache setObject:image forKey:imgUrlString];
|
|
346
|
+
|
|
194
347
|
NSMutableArray* newImages = [item.gridImages mutableCopy];
|
|
195
|
-
|
|
196
348
|
@try {
|
|
197
|
-
newImages[index] =
|
|
349
|
+
newImages[index] = image;
|
|
350
|
+
[item updateImages:newImages];
|
|
198
351
|
}
|
|
199
352
|
@catch (NSException *exception) {
|
|
200
|
-
// Best effort updating the array
|
|
201
353
|
NSLog(@"Failed to update images array of CPListImageRowItem");
|
|
202
354
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
NSURL *imgUrl = [NSURL URLWithString:imgUrlString];
|
|
209
|
-
|
|
210
|
-
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:imgUrl completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
|
211
|
-
if (data) {
|
|
212
|
-
UIImage *image = [UIImage imageWithData:data];
|
|
213
|
-
|
|
214
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
215
|
-
NSMutableArray* newImages = [item.gridImages mutableCopy];
|
|
216
|
-
|
|
217
|
-
@try {
|
|
218
|
-
newImages[index] = image;
|
|
219
|
-
}
|
|
220
|
-
@catch (NSException *exception) {
|
|
221
|
-
// Best effort updating the array
|
|
222
|
-
NSLog(@"Failed to update images array of CPListImageRowItem");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
[item updateImages:newImages];
|
|
226
|
-
});
|
|
227
|
-
} else {
|
|
228
|
-
NSLog(@"Failed to load image for CPListImageRowItem from URL: %@", imgUrl);
|
|
355
|
+
} else if (error) {
|
|
356
|
+
NSLog(@"Failed to load image for CPListImageRowItem from URL: %@ - %@", imgUrlString, error);
|
|
229
357
|
}
|
|
230
358
|
}];
|
|
231
|
-
[task resume];
|
|
232
359
|
}
|
|
233
360
|
|
|
234
361
|
RCT_EXPORT_METHOD(checkForConnection) {
|
|
@@ -324,44 +451,80 @@ RCT_EXPORT_METHOD(createTemplate:(NSString *)templateId config:(NSDictionary*)co
|
|
|
324
451
|
[nowPlayingTemplate setAlbumArtistButtonEnabled:[RCTConvert BOOL:config[@"albumArtistButtonEnabled"]]];
|
|
325
452
|
[nowPlayingTemplate setUpNextTitle:[RCTConvert NSString:config[@"upNextButtonTitle"]]];
|
|
326
453
|
[nowPlayingTemplate setUpNextButtonEnabled:[RCTConvert BOOL:config[@"upNextButtonEnabled"]]];
|
|
327
|
-
|
|
454
|
+
|
|
328
455
|
NSArray<NSDictionary*> *_buttons = [RCTConvert NSDictionaryArray:config[@"buttons"]];
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
@"playback": CPNowPlayingPlaybackRateButton.class,
|
|
335
|
-
@"repeat": CPNowPlayingRepeatButton.class,
|
|
336
|
-
@"image": CPNowPlayingImageButton.class
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
for (NSDictionary *_button in _buttons) {
|
|
456
|
+
|
|
457
|
+
// Collect all image sources for image-type buttons
|
|
458
|
+
NSMutableArray *imageSources = [NSMutableArray new];
|
|
459
|
+
for (NSUInteger i = 0; i < _buttons.count; i++) {
|
|
460
|
+
NSDictionary *_button = _buttons[i];
|
|
340
461
|
NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
462
|
+
if ([buttonType isEqualToString:@"image"]) {
|
|
463
|
+
id imageSource = [_button objectForKey:@"image"];
|
|
464
|
+
[imageSources addObject:imageSource ?: [NSNull null]];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Pre-load all images, then create buttons
|
|
469
|
+
[self loadImagesForNowPlayingButtons:imageSources completion:^(NSArray *loadedImages) {
|
|
470
|
+
NSMutableArray<CPNowPlayingButton *> *buttons = [NSMutableArray new];
|
|
471
|
+
|
|
472
|
+
NSDictionary *buttonTypeMapping = @{
|
|
473
|
+
@"shuffle": CPNowPlayingShuffleButton.class,
|
|
474
|
+
@"add-to-library": CPNowPlayingAddToLibraryButton.class,
|
|
475
|
+
@"more": CPNowPlayingMoreButton.class,
|
|
476
|
+
@"playback": CPNowPlayingPlaybackRateButton.class,
|
|
477
|
+
@"repeat": CPNowPlayingRepeatButton.class,
|
|
478
|
+
@"image": CPNowPlayingImageButton.class
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
NSUInteger imageIndex = 0;
|
|
482
|
+
for (NSUInteger i = 0; i < _buttons.count; i++) {
|
|
483
|
+
NSDictionary *_button = _buttons[i];
|
|
484
|
+
NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
|
|
485
|
+
NSDictionary *body = @{@"templateId":templateId, @"id": _button[@"id"] ?: @"" };
|
|
486
|
+
Class buttonClass = buttonTypeMapping[buttonType];
|
|
487
|
+
|
|
488
|
+
if (buttonClass) {
|
|
489
|
+
CPNowPlayingButton *button;
|
|
490
|
+
|
|
491
|
+
if ([buttonType isEqualToString:@"image"]) {
|
|
492
|
+
id imageObj = (imageIndex < loadedImages.count) ? loadedImages[imageIndex] : nil;
|
|
493
|
+
UIImage *image = ([imageObj isKindOfClass:[UIImage class]]) ? (UIImage *)imageObj : nil;
|
|
494
|
+
imageIndex++;
|
|
495
|
+
|
|
496
|
+
if (image) {
|
|
497
|
+
button = [[CPNowPlayingImageButton alloc] initWithImage:image handler:^(__kindof CPNowPlayingImageButton * _Nonnull) {
|
|
498
|
+
if (self->hasListeners) {
|
|
499
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
500
|
+
}
|
|
501
|
+
}];
|
|
502
|
+
} else {
|
|
503
|
+
// Fallback: create a minimal placeholder if image failed to load
|
|
504
|
+
UIImage *placeholder = [UIImage systemImageNamed:@"circle"];
|
|
505
|
+
button = [[CPNowPlayingImageButton alloc] initWithImage:placeholder handler:^(__kindof CPNowPlayingImageButton * _Nonnull) {
|
|
506
|
+
if (self->hasListeners) {
|
|
507
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
508
|
+
}
|
|
509
|
+
}];
|
|
357
510
|
}
|
|
358
|
-
}
|
|
511
|
+
} else {
|
|
512
|
+
button = [[buttonClass alloc] initWithHandler:^(__kindof CPNowPlayingButton * _Nonnull) {
|
|
513
|
+
if (self->hasListeners) {
|
|
514
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
515
|
+
}
|
|
516
|
+
}];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
[buttons addObject:button];
|
|
359
520
|
}
|
|
360
|
-
|
|
361
|
-
[buttons addObject:button];
|
|
362
521
|
}
|
|
363
|
-
|
|
364
|
-
|
|
522
|
+
|
|
523
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
524
|
+
[nowPlayingTemplate updateNowPlayingButtons:buttons];
|
|
525
|
+
});
|
|
526
|
+
}];
|
|
527
|
+
|
|
365
528
|
carPlayTemplate = nowPlayingTemplate;
|
|
366
529
|
} else if ([type isEqualToString:@"tabbar"]) {
|
|
367
530
|
CPTabBarTemplate *tabBarTemplate = [[CPTabBarTemplate alloc] initWithTemplates:[self parseTemplatesFrom:config]];
|
|
@@ -369,7 +532,8 @@ RCT_EXPORT_METHOD(createTemplate:(NSString *)templateId config:(NSDictionary*)co
|
|
|
369
532
|
carPlayTemplate = tabBarTemplate;
|
|
370
533
|
} else if ([type isEqualToString:@"contact"]) {
|
|
371
534
|
NSString *nm = [RCTConvert NSString:config[@"name"]];
|
|
372
|
-
|
|
535
|
+
id imageSource = config[@"image"];
|
|
536
|
+
UIImage *img = [self isImageSourceURL:imageSource] ? nil : [RCTConvert UIImage:imageSource];
|
|
373
537
|
CPContact *contact = [[CPContact alloc] initWithName:nm image:img];
|
|
374
538
|
[contact setSubtitle:config[@"subtitle"]];
|
|
375
539
|
[contact setActions:[self parseButtons:config[@"actions"] templateId:templateId]];
|
|
@@ -451,13 +615,26 @@ RCT_EXPORT_METHOD(createTemplate:(NSString *)templateId config:(NSDictionary*)co
|
|
|
451
615
|
carPlayTemplate.tabImage = [UIImage systemImageNamed:[RCTConvert NSString:config[@"tabSystemImageName"]]];
|
|
452
616
|
}
|
|
453
617
|
if (config[@"tabImage"]) {
|
|
454
|
-
|
|
618
|
+
id tabImageSource = config[@"tabImage"];
|
|
619
|
+
if ([self isImageSourceURL:tabImageSource]) {
|
|
620
|
+
// Tab images with URLs need to be loaded async, but tabImage setter doesn't update later
|
|
621
|
+
// For now, skip URL-based tab images
|
|
622
|
+
} else {
|
|
623
|
+
carPlayTemplate.tabImage = [RCTConvert UIImage:tabImageSource];
|
|
624
|
+
}
|
|
455
625
|
}
|
|
456
626
|
if (config[@"tabTitle"]) {
|
|
457
627
|
carPlayTemplate.tabTitle = [RCTConvert NSString:config[@"tabTitle"]];
|
|
458
628
|
}
|
|
459
629
|
|
|
460
|
-
|
|
630
|
+
// Set userInfo with templateId and optional scrollBottomThreshold for list templates
|
|
631
|
+
NSMutableDictionary *userInfo = [@{ @"templateId": templateId } mutableCopy];
|
|
632
|
+
if ([type isEqualToString:@"list"] && config[@"scrollBottomThreshold"]) {
|
|
633
|
+
userInfo[@"scrollBottomThreshold"] = [RCTConvert NSNumber:config[@"scrollBottomThreshold"]];
|
|
634
|
+
} else if ([type isEqualToString:@"list"]) {
|
|
635
|
+
userInfo[@"scrollBottomThreshold"] = @(5); // default threshold
|
|
636
|
+
}
|
|
637
|
+
[carPlayTemplate setUserInfo:userInfo];
|
|
461
638
|
[store setTemplate:templateId template:carPlayTemplate];
|
|
462
639
|
}
|
|
463
640
|
|
|
@@ -561,11 +738,12 @@ RCT_EXPORT_METHOD(setRootTemplate:(NSString *)templateId animated:(BOOL)animated
|
|
|
561
738
|
|
|
562
739
|
if (template) {
|
|
563
740
|
[store.interfaceController setRootTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
564
|
-
|
|
565
|
-
|
|
741
|
+
if (err) {
|
|
742
|
+
NSLog(@"Set root template error: %@", err);
|
|
743
|
+
}
|
|
566
744
|
}];
|
|
567
745
|
} else {
|
|
568
|
-
NSLog(@"Failed to find template %@",
|
|
746
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
569
747
|
}
|
|
570
748
|
}
|
|
571
749
|
|
|
@@ -573,12 +751,19 @@ RCT_EXPORT_METHOD(pushTemplate:(NSString *)templateId animated:(BOOL)animated) {
|
|
|
573
751
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
574
752
|
CPTemplate *template = [store findTemplateById:templateId];
|
|
575
753
|
if (template) {
|
|
754
|
+
// Check if template is already on the navigation stack
|
|
755
|
+
NSArray *currentTemplates = store.interfaceController.templates;
|
|
756
|
+
if ([currentTemplates containsObject:template]) {
|
|
757
|
+
NSLog(@"Template %@ is already on the navigation stack, skipping push", templateId);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
576
760
|
[store.interfaceController pushTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
577
|
-
|
|
578
|
-
|
|
761
|
+
if (err) {
|
|
762
|
+
NSLog(@"Push template error: %@", err);
|
|
763
|
+
}
|
|
579
764
|
}];
|
|
580
765
|
} else {
|
|
581
|
-
NSLog(@"Failed to find template %@",
|
|
766
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
582
767
|
}
|
|
583
768
|
}
|
|
584
769
|
|
|
@@ -587,27 +772,30 @@ RCT_EXPORT_METHOD(popToTemplate:(NSString *)templateId animated:(BOOL)animated)
|
|
|
587
772
|
CPTemplate *template = [store findTemplateById:templateId];
|
|
588
773
|
if (template) {
|
|
589
774
|
[store.interfaceController popToTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
590
|
-
|
|
591
|
-
|
|
775
|
+
if (err) {
|
|
776
|
+
NSLog(@"Pop to template error: %@", err);
|
|
777
|
+
}
|
|
592
778
|
}];
|
|
593
779
|
} else {
|
|
594
|
-
NSLog(@"Failed to find template %@",
|
|
780
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
595
781
|
}
|
|
596
782
|
}
|
|
597
783
|
|
|
598
784
|
RCT_EXPORT_METHOD(popToRootTemplate:(BOOL)animated) {
|
|
599
785
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
600
786
|
[store.interfaceController popToRootTemplateAnimated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
601
|
-
|
|
602
|
-
|
|
787
|
+
if (err) {
|
|
788
|
+
NSLog(@"Pop to root template error: %@", err);
|
|
789
|
+
}
|
|
603
790
|
}];
|
|
604
791
|
}
|
|
605
792
|
|
|
606
793
|
RCT_EXPORT_METHOD(popTemplate:(BOOL)animated) {
|
|
607
794
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
608
795
|
[store.interfaceController popTemplateAnimated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
609
|
-
|
|
610
|
-
|
|
796
|
+
if (err) {
|
|
797
|
+
NSLog(@"Pop template error: %@", err);
|
|
798
|
+
}
|
|
611
799
|
}];
|
|
612
800
|
}
|
|
613
801
|
|
|
@@ -615,12 +803,18 @@ RCT_EXPORT_METHOD(presentTemplate:(NSString *)templateId animated:(BOOL)animated
|
|
|
615
803
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
616
804
|
CPTemplate *template = [store findTemplateById:templateId];
|
|
617
805
|
if (template) {
|
|
806
|
+
// Check if template is already presented
|
|
807
|
+
if (store.interfaceController.presentedTemplate == template) {
|
|
808
|
+
NSLog(@"Template %@ is already presented, skipping", templateId);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
618
811
|
[store.interfaceController presentTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
619
|
-
|
|
620
|
-
|
|
812
|
+
if (err) {
|
|
813
|
+
NSLog(@"Present template error: %@", err);
|
|
814
|
+
}
|
|
621
815
|
}];
|
|
622
816
|
} else {
|
|
623
|
-
NSLog(@"Failed to find template %@",
|
|
817
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
624
818
|
}
|
|
625
819
|
}
|
|
626
820
|
|
|
@@ -629,6 +823,152 @@ RCT_EXPORT_METHOD(dismissTemplate:(BOOL)animated) {
|
|
|
629
823
|
[store.interfaceController dismissTemplateAnimated:animated];
|
|
630
824
|
}
|
|
631
825
|
|
|
826
|
+
RCT_EXPORT_METHOD(updateTemplate:(NSString *)templateId config:(NSDictionary*)config) {
|
|
827
|
+
RNCPStore *store = [RNCPStore sharedManager];
|
|
828
|
+
CPTemplate *template = [store findTemplateById:templateId];
|
|
829
|
+
NSString *type = [RCTConvert NSString:config[@"type"]];
|
|
830
|
+
|
|
831
|
+
if (!template) {
|
|
832
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Route to specific update methods based on template type
|
|
837
|
+
if ([template isKindOfClass:[CPListTemplate class]]) {
|
|
838
|
+
CPListTemplate *listTemplate = (CPListTemplate *)template;
|
|
839
|
+
if (config[@"leadingNavigationBarButtons"]) {
|
|
840
|
+
NSArray *leadingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"leadingNavigationBarButtons"]] templateId:templateId];
|
|
841
|
+
[listTemplate setLeadingNavigationBarButtons:leadingNavigationBarButtons];
|
|
842
|
+
}
|
|
843
|
+
if (config[@"trailingNavigationBarButtons"]) {
|
|
844
|
+
NSArray *trailingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"trailingNavigationBarButtons"]] templateId:templateId];
|
|
845
|
+
[listTemplate setTrailingNavigationBarButtons:trailingNavigationBarButtons];
|
|
846
|
+
}
|
|
847
|
+
if (config[@"emptyViewTitleVariants"]) {
|
|
848
|
+
listTemplate.emptyViewTitleVariants = [RCTConvert NSArray:config[@"emptyViewTitleVariants"]];
|
|
849
|
+
}
|
|
850
|
+
if (config[@"emptyViewSubtitleVariants"]) {
|
|
851
|
+
listTemplate.emptyViewSubtitleVariants = [RCTConvert NSArray:config[@"emptyViewSubtitleVariants"]];
|
|
852
|
+
}
|
|
853
|
+
if (config[@"sections"]) {
|
|
854
|
+
[listTemplate updateSections:[self parseSections:[RCTConvert NSArray:config[@"sections"]] templateId:templateId]];
|
|
855
|
+
}
|
|
856
|
+
} else if ([template isKindOfClass:[CPGridTemplate class]]) {
|
|
857
|
+
CPGridTemplate *gridTemplate = (CPGridTemplate *)template;
|
|
858
|
+
if (config[@"leadingNavigationBarButtons"]) {
|
|
859
|
+
NSArray *leadingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"leadingNavigationBarButtons"]] templateId:templateId];
|
|
860
|
+
[gridTemplate setLeadingNavigationBarButtons:leadingNavigationBarButtons];
|
|
861
|
+
}
|
|
862
|
+
if (config[@"trailingNavigationBarButtons"]) {
|
|
863
|
+
NSArray *trailingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"trailingNavigationBarButtons"]] templateId:templateId];
|
|
864
|
+
[gridTemplate setTrailingNavigationBarButtons:trailingNavigationBarButtons];
|
|
865
|
+
}
|
|
866
|
+
if (config[@"buttons"]) {
|
|
867
|
+
NSArray *buttons = [self parseGridButtons:[RCTConvert NSArray:config[@"buttons"]] templateId:templateId];
|
|
868
|
+
[gridTemplate updateGridButtons:buttons];
|
|
869
|
+
}
|
|
870
|
+
} else if ([template isKindOfClass:[CPNowPlayingTemplate class]]) {
|
|
871
|
+
CPNowPlayingTemplate *nowPlayingTemplate = (CPNowPlayingTemplate *)template;
|
|
872
|
+
if (config[@"albumArtistButtonEnabled"]) {
|
|
873
|
+
[nowPlayingTemplate setAlbumArtistButtonEnabled:[RCTConvert BOOL:config[@"albumArtistButtonEnabled"]]];
|
|
874
|
+
}
|
|
875
|
+
if (config[@"upNextButtonTitle"]) {
|
|
876
|
+
[nowPlayingTemplate setUpNextTitle:[RCTConvert NSString:config[@"upNextButtonTitle"]]];
|
|
877
|
+
}
|
|
878
|
+
if (config[@"upNextButtonEnabled"]) {
|
|
879
|
+
[nowPlayingTemplate setUpNextButtonEnabled:[RCTConvert BOOL:config[@"upNextButtonEnabled"]]];
|
|
880
|
+
}
|
|
881
|
+
if (config[@"buttons"]) {
|
|
882
|
+
NSArray<NSDictionary*> *_buttons = [RCTConvert NSDictionaryArray:config[@"buttons"]];
|
|
883
|
+
|
|
884
|
+
// Collect all image sources for image-type buttons
|
|
885
|
+
NSMutableArray *imageSources = [NSMutableArray new];
|
|
886
|
+
NSMutableArray *imageIndices = [NSMutableArray new]; // Track which buttons need images
|
|
887
|
+
|
|
888
|
+
for (NSUInteger i = 0; i < _buttons.count; i++) {
|
|
889
|
+
NSDictionary *_button = _buttons[i];
|
|
890
|
+
NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
|
|
891
|
+
if ([buttonType isEqualToString:@"image"]) {
|
|
892
|
+
id imageSource = [_button objectForKey:@"image"];
|
|
893
|
+
[imageSources addObject:imageSource ?: [NSNull null]];
|
|
894
|
+
[imageIndices addObject:@(i)];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Pre-load all images, then create buttons
|
|
899
|
+
[self loadImagesForNowPlayingButtons:imageSources completion:^(NSArray *loadedImages) {
|
|
900
|
+
NSMutableArray<CPNowPlayingButton *> *buttons = [NSMutableArray new];
|
|
901
|
+
|
|
902
|
+
NSDictionary *buttonTypeMapping = @{
|
|
903
|
+
@"shuffle": CPNowPlayingShuffleButton.class,
|
|
904
|
+
@"add-to-library": CPNowPlayingAddToLibraryButton.class,
|
|
905
|
+
@"more": CPNowPlayingMoreButton.class,
|
|
906
|
+
@"playback": CPNowPlayingPlaybackRateButton.class,
|
|
907
|
+
@"repeat": CPNowPlayingRepeatButton.class,
|
|
908
|
+
@"image": CPNowPlayingImageButton.class
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
NSUInteger imageIndex = 0;
|
|
912
|
+
for (NSUInteger i = 0; i < _buttons.count; i++) {
|
|
913
|
+
NSDictionary *_button = _buttons[i];
|
|
914
|
+
NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
|
|
915
|
+
NSDictionary *body = @{@"templateId":templateId, @"id": _button[@"id"] ?: @"" };
|
|
916
|
+
Class buttonClass = buttonTypeMapping[buttonType];
|
|
917
|
+
|
|
918
|
+
if (buttonClass) {
|
|
919
|
+
CPNowPlayingButton *button;
|
|
920
|
+
|
|
921
|
+
if ([buttonType isEqualToString:@"image"]) {
|
|
922
|
+
id imageObj = (imageIndex < loadedImages.count) ? loadedImages[imageIndex] : nil;
|
|
923
|
+
UIImage *image = ([imageObj isKindOfClass:[UIImage class]]) ? (UIImage *)imageObj : nil;
|
|
924
|
+
imageIndex++;
|
|
925
|
+
|
|
926
|
+
if (image) {
|
|
927
|
+
button = [[CPNowPlayingImageButton alloc] initWithImage:image handler:^(__kindof CPNowPlayingImageButton * _Nonnull btn) {
|
|
928
|
+
if (self->hasListeners) {
|
|
929
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
930
|
+
}
|
|
931
|
+
}];
|
|
932
|
+
} else {
|
|
933
|
+
// Fallback: create a minimal placeholder if image failed to load
|
|
934
|
+
UIImage *placeholder = [UIImage systemImageNamed:@"circle"];
|
|
935
|
+
button = [[CPNowPlayingImageButton alloc] initWithImage:placeholder handler:^(__kindof CPNowPlayingImageButton * _Nonnull btn) {
|
|
936
|
+
if (self->hasListeners) {
|
|
937
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
938
|
+
}
|
|
939
|
+
}];
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
button = [[buttonClass alloc] initWithHandler:^(__kindof CPNowPlayingButton * _Nonnull btn) {
|
|
943
|
+
if (self->hasListeners) {
|
|
944
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
945
|
+
}
|
|
946
|
+
}];
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
[buttons addObject:button];
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
954
|
+
[nowPlayingTemplate updateNowPlayingButtons:buttons];
|
|
955
|
+
});
|
|
956
|
+
}];
|
|
957
|
+
}
|
|
958
|
+
} else if ([template isKindOfClass:[CPMapTemplate class]]) {
|
|
959
|
+
CPMapTemplate *mapTemplate = (CPMapTemplate *)template;
|
|
960
|
+
[self applyConfigForMapTemplate:mapTemplate templateId:templateId config:config];
|
|
961
|
+
} else if ([template isKindOfClass:[CPInformationTemplate class]]) {
|
|
962
|
+
CPInformationTemplate *informationTemplate = (CPInformationTemplate *)template;
|
|
963
|
+
if (config[@"items"]) {
|
|
964
|
+
informationTemplate.items = [self parseInformationItems:[RCTConvert NSArray:config[@"items"]]];
|
|
965
|
+
}
|
|
966
|
+
if (config[@"actions"]) {
|
|
967
|
+
informationTemplate.actions = [self parseInformationActions:[RCTConvert NSArray:config[@"actions"]] templateId:templateId];
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
632
972
|
RCT_EXPORT_METHOD(updateListTemplate:(NSString*)templateId config:(NSDictionary*)config) {
|
|
633
973
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
634
974
|
CPTemplate *template = [store findTemplateById:templateId];
|
|
@@ -694,14 +1034,21 @@ RCT_EXPORT_METHOD(updateListTemplateItem:(NSString *)templateId config:(NSDictio
|
|
|
694
1034
|
CPListItem *item = (CPListItem *)section.items[index];
|
|
695
1035
|
if (config[@"imgUrl"]) {
|
|
696
1036
|
NSString *imgUrlString = [RCTConvert NSString:config[@"imgUrl"]];
|
|
697
|
-
UIImage *placeholderImage;
|
|
698
|
-
|
|
699
|
-
|
|
1037
|
+
UIImage *placeholderImage = nil;
|
|
1038
|
+
id placeholderSource = config[@"placeholderImage"];
|
|
1039
|
+
if (placeholderSource != nil && ![self isImageSourceURL:placeholderSource]) {
|
|
1040
|
+
placeholderImage = [RCTConvert UIImage:placeholderSource];
|
|
700
1041
|
}
|
|
701
1042
|
[self updateItemImageWithURL:item imgUrl:imgUrlString placeholderImage:placeholderImage];
|
|
702
1043
|
}
|
|
703
1044
|
if (config[@"image"]) {
|
|
704
|
-
|
|
1045
|
+
id imageSource = config[@"image"];
|
|
1046
|
+
if ([self isImageSourceURL:imageSource]) {
|
|
1047
|
+
NSString *imgUrlString = [self getURLStringFromImageSource:imageSource];
|
|
1048
|
+
[self updateItemImageWithURL:item imgUrl:imgUrlString placeholderImage:nil];
|
|
1049
|
+
} else {
|
|
1050
|
+
[item setImage:[RCTConvert UIImage:imageSource]];
|
|
1051
|
+
}
|
|
705
1052
|
}
|
|
706
1053
|
if (config[@"text"]) {
|
|
707
1054
|
[item setText:[RCTConvert NSString:config[@"text"]]];
|
|
@@ -716,7 +1063,16 @@ RCT_EXPORT_METHOD(updateListTemplateItem:(NSString *)templateId config:(NSDictio
|
|
|
716
1063
|
[item setPlaybackProgress:[RCTConvert CGFloat:config[@"playbackProgress"]]];
|
|
717
1064
|
}
|
|
718
1065
|
if (@available(iOS 14.0, *) && config[@"accessoryImage"]) {
|
|
719
|
-
|
|
1066
|
+
id accessorySource = config[@"accessoryImage"];
|
|
1067
|
+
if ([self isImageSourceURL:accessorySource]) {
|
|
1068
|
+
[self loadImageAsync:accessorySource completion:^(UIImage *image) {
|
|
1069
|
+
if (image) {
|
|
1070
|
+
[item setAccessoryImage:image];
|
|
1071
|
+
}
|
|
1072
|
+
}];
|
|
1073
|
+
} else {
|
|
1074
|
+
[item setAccessoryImage:[RCTConvert UIImage:accessorySource]];
|
|
1075
|
+
}
|
|
720
1076
|
}
|
|
721
1077
|
} else {
|
|
722
1078
|
NSLog(@"Failed to find template %@", template);
|
|
@@ -1078,8 +1434,18 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1078
1434
|
NSString *_title = [barButton objectForKey:@"title"];
|
|
1079
1435
|
[_barButton setTitle:_title];
|
|
1080
1436
|
} else if (_type == CPBarButtonTypeImage) {
|
|
1081
|
-
|
|
1082
|
-
[
|
|
1437
|
+
id imageSource = [barButton objectForKey:@"image"];
|
|
1438
|
+
if ([self isImageSourceURL:imageSource]) {
|
|
1439
|
+
// Load image asynchronously
|
|
1440
|
+
[self loadImageAsync:imageSource completion:^(UIImage *image) {
|
|
1441
|
+
if (image) {
|
|
1442
|
+
[_barButton setImage:image];
|
|
1443
|
+
}
|
|
1444
|
+
}];
|
|
1445
|
+
} else {
|
|
1446
|
+
UIImage *_image = [RCTConvert UIImage:imageSource];
|
|
1447
|
+
[_barButton setImage:_image];
|
|
1448
|
+
}
|
|
1083
1449
|
}
|
|
1084
1450
|
[result addObject:_barButton];
|
|
1085
1451
|
}
|
|
@@ -1102,6 +1468,27 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1102
1468
|
return result;
|
|
1103
1469
|
}
|
|
1104
1470
|
|
|
1471
|
+
// Helper to check if image source is a URL
|
|
1472
|
+
- (BOOL)isImageSourceURL:(id)imageSource {
|
|
1473
|
+
if ([imageSource isKindOfClass:[NSDictionary class]]) {
|
|
1474
|
+
NSDictionary *imageDict = (NSDictionary *)imageSource;
|
|
1475
|
+
NSString *uri = imageDict[@"uri"];
|
|
1476
|
+
if (uri && ([uri hasPrefix:@"http://"] || [uri hasPrefix:@"https://"])) {
|
|
1477
|
+
return YES;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return NO;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Helper to get URL string from image source
|
|
1484
|
+
- (NSString *)getURLStringFromImageSource:(id)imageSource {
|
|
1485
|
+
if ([imageSource isKindOfClass:[NSDictionary class]]) {
|
|
1486
|
+
NSDictionary *imageDict = (NSDictionary *)imageSource;
|
|
1487
|
+
return imageDict[@"uri"];
|
|
1488
|
+
}
|
|
1489
|
+
return nil;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1105
1492
|
- (NSArray<CPSelectableListItem>*)parseListItems:(NSArray*)items startIndex:(int)startIndex templateId:(NSString *)templateId {
|
|
1106
1493
|
NSMutableArray *_items = [NSMutableArray array];
|
|
1107
1494
|
int listIndex = startIndex;
|
|
@@ -1110,12 +1497,14 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1110
1497
|
NSString *_detailText = [item objectForKey:@"detailText"];
|
|
1111
1498
|
NSString *_text = [item objectForKey:@"text"];
|
|
1112
1499
|
NSObject *_imageObj = [item objectForKey:@"image"];
|
|
1113
|
-
|
|
1500
|
+
|
|
1114
1501
|
NSArray *_imageItems = [item objectForKey:@"images"];
|
|
1115
1502
|
NSArray *_imageUrls = [item objectForKey:@"imgUrls"];
|
|
1116
|
-
|
|
1503
|
+
|
|
1117
1504
|
if (_imageItems == nil && _imageUrls == nil) {
|
|
1118
|
-
|
|
1505
|
+
// Check if image is a URL - if so, use async loading
|
|
1506
|
+
BOOL isURL = [self isImageSourceURL:_imageObj];
|
|
1507
|
+
UIImage *_image = isURL ? nil : [RCTConvert UIImage:_imageObj];
|
|
1119
1508
|
CPListItem *_item;
|
|
1120
1509
|
if (@available(iOS 14.0, *)) {
|
|
1121
1510
|
CPListItemAccessoryType accessoryType = _showsDisclosureIndicator ? CPListItemAccessoryTypeDisclosureIndicator : CPListItemAccessoryTypeNone;
|
|
@@ -1128,11 +1517,16 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1128
1517
|
}
|
|
1129
1518
|
if (item[@"imgUrl"]) {
|
|
1130
1519
|
NSString *imgUrlString = [RCTConvert NSString:item[@"imgUrl"]];
|
|
1131
|
-
UIImage *placeholderImage;
|
|
1132
|
-
|
|
1133
|
-
|
|
1520
|
+
UIImage *placeholderImage = nil;
|
|
1521
|
+
id placeholderSource = [item objectForKey:@"placeholderImage"];
|
|
1522
|
+
if (placeholderSource != nil && ![self isImageSourceURL:placeholderSource]) {
|
|
1523
|
+
placeholderImage = [RCTConvert UIImage:placeholderSource];
|
|
1134
1524
|
}
|
|
1135
1525
|
[self updateItemImageWithURL:_item imgUrl:imgUrlString placeholderImage:placeholderImage];
|
|
1526
|
+
} else if (isURL) {
|
|
1527
|
+
// Load image asynchronously for URL-based images
|
|
1528
|
+
NSString *imgUrlString = [self getURLStringFromImageSource:_imageObj];
|
|
1529
|
+
[self updateItemImageWithURL:_item imgUrl:imgUrlString placeholderImage:nil];
|
|
1136
1530
|
}
|
|
1137
1531
|
[_item setUserInfo:@{ @"index": @(listIndex) }];
|
|
1138
1532
|
[_items addObject:_item];
|
|
@@ -1144,8 +1538,13 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1144
1538
|
NSArray* slicedArray = [_imageItems subarrayWithRange:NSMakeRange(0, MIN(CPMaximumNumberOfGridImages, _imageItems.count))];
|
|
1145
1539
|
|
|
1146
1540
|
for (NSObject *imageObj in slicedArray){
|
|
1147
|
-
|
|
1148
|
-
[
|
|
1541
|
+
// Skip URL-based images for initial creation (use placeholder)
|
|
1542
|
+
if ([self isImageSourceURL:imageObj]) {
|
|
1543
|
+
[_images addObject:[[UIImage alloc] init]];
|
|
1544
|
+
} else {
|
|
1545
|
+
UIImage *_image = [RCTConvert UIImage:imageObj];
|
|
1546
|
+
[_images addObject:_image];
|
|
1547
|
+
}
|
|
1149
1548
|
}
|
|
1150
1549
|
}
|
|
1151
1550
|
if (@available(iOS 14.0, *)) {
|
|
@@ -1173,7 +1572,8 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1173
1572
|
UIImage *placeholderImage = nil;
|
|
1174
1573
|
if (_placeholderImages != nil && _index < _placeholderImages.count) {
|
|
1175
1574
|
id placeholderImageObj = _placeholderImages[_index];
|
|
1176
|
-
|
|
1575
|
+
// Only load placeholder synchronously if it's not a URL
|
|
1576
|
+
if (placeholderImageObj != nil && placeholderImageObj != [NSNull null] && ![self isImageSourceURL:placeholderImageObj]) {
|
|
1177
1577
|
placeholderImage = [RCTConvert UIImage:placeholderImageObj];
|
|
1178
1578
|
}
|
|
1179
1579
|
}
|
|
@@ -1231,10 +1631,22 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1231
1631
|
for (NSDictionary *button in buttons) {
|
|
1232
1632
|
NSString *_id = [button objectForKey:@"id"];
|
|
1233
1633
|
NSArray<NSString*> *_titleVariants = [button objectForKey:@"titleVariants"];
|
|
1234
|
-
|
|
1634
|
+
id imageSource = [button objectForKey:@"image"];
|
|
1635
|
+
UIImage *_image;
|
|
1636
|
+
|
|
1637
|
+
if ([self isImageSourceURL:imageSource]) {
|
|
1638
|
+
// CPGridButton doesn't support updating image after creation
|
|
1639
|
+
// For URL-based images, we create with placeholder and load async
|
|
1640
|
+
_image = [[UIImage alloc] init];
|
|
1641
|
+
// Note: In production, images should be bundled locally to avoid this issue
|
|
1642
|
+
} else {
|
|
1643
|
+
_image = [RCTConvert UIImage:imageSource];
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
int currentIndex = index; // Capture for block
|
|
1235
1647
|
CPGridButton *_button = [[CPGridButton alloc] initWithTitleVariants:_titleVariants image:_image handler:^(CPGridButton * _Nonnull barButton) {
|
|
1236
1648
|
if (self->hasListeners) {
|
|
1237
|
-
[self sendEventWithName:@"gridButtonPressed" body:@{@"id": _id, @"templateId":templateId, @"index": @(
|
|
1649
|
+
[self sendEventWithName:@"gridButtonPressed" body:@{@"id": _id, @"templateId":templateId, @"index": @(currentIndex) }];
|
|
1238
1650
|
}
|
|
1239
1651
|
}];
|
|
1240
1652
|
BOOL _disabled = [button objectForKey:@"disabled"];
|
|
@@ -1273,8 +1685,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1273
1685
|
CPManeuver* maneuver = [[CPManeuver alloc] init];
|
|
1274
1686
|
|
|
1275
1687
|
if ([json objectForKey:@"junctionImage"]) {
|
|
1276
|
-
|
|
1277
|
-
|
|
1688
|
+
id junctionSource = json[@"junctionImage"];
|
|
1689
|
+
if (![self isImageSourceURL:junctionSource]) {
|
|
1690
|
+
UIImage *junctionImage = [RCTConvert UIImage:junctionSource];
|
|
1691
|
+
[maneuver setJunctionImage:[self imageWithTint:junctionImage andTintColor:[UIColor whiteColor]]];
|
|
1692
|
+
}
|
|
1278
1693
|
}
|
|
1279
1694
|
|
|
1280
1695
|
if ([json objectForKey:@"initialTravelEstimates"]) {
|
|
@@ -1283,7 +1698,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1283
1698
|
}
|
|
1284
1699
|
|
|
1285
1700
|
if ([json objectForKey:@"symbolImage"]) {
|
|
1286
|
-
|
|
1701
|
+
id symbolSource = json[@"symbolImage"];
|
|
1702
|
+
if ([self isImageSourceURL:symbolSource]) {
|
|
1703
|
+
return maneuver; // Skip URL-based symbol images for now
|
|
1704
|
+
}
|
|
1705
|
+
UIImage *symbolImage = [RCTConvert UIImage:symbolSource];
|
|
1287
1706
|
|
|
1288
1707
|
if ([json objectForKey:@"symbolImageSize"]) {
|
|
1289
1708
|
NSDictionary *size = [RCTConvert NSDictionary:json[@"symbolImageSize"]];
|
|
@@ -1340,9 +1759,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1340
1759
|
}
|
|
1341
1760
|
|
|
1342
1761
|
- (CPNavigationAlert*)parseNavigationAlert:(NSDictionary*)json templateId:(NSString*)templateId {
|
|
1343
|
-
CPImageSet *imageSet;
|
|
1344
|
-
|
|
1345
|
-
|
|
1762
|
+
CPImageSet *imageSet = nil;
|
|
1763
|
+
id lightSource = json[@"lightImage"];
|
|
1764
|
+
id darkSource = json[@"darkImage"];
|
|
1765
|
+
if (lightSource && darkSource && ![self isImageSourceURL:lightSource] && ![self isImageSourceURL:darkSource]) {
|
|
1766
|
+
imageSet = [[CPImageSet alloc] initWithLightContentImage:[RCTConvert UIImage:lightSource] darkContentImage:[RCTConvert UIImage:darkSource]];
|
|
1346
1767
|
}
|
|
1347
1768
|
CPAlertAction *secondaryAction = [json objectForKey:@"secondaryAction"] ? [self parseAlertAction:json[@"secondaryAction"] body:@{ @"templateId": templateId, @"secondary": @(YES) }] : nil;
|
|
1348
1769
|
|
|
@@ -1366,7 +1787,12 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1366
1787
|
}
|
|
1367
1788
|
|
|
1368
1789
|
- (CPVoiceControlState*)parseVoiceControlState:(NSDictionary*)json {
|
|
1369
|
-
|
|
1790
|
+
id imageSource = json[@"image"];
|
|
1791
|
+
UIImage *image = nil;
|
|
1792
|
+
if (imageSource && ![self isImageSourceURL:imageSource]) {
|
|
1793
|
+
image = [RCTConvert UIImage:imageSource];
|
|
1794
|
+
}
|
|
1795
|
+
return [[CPVoiceControlState alloc] initWithIdentifier:[RCTConvert NSString:json[@"identifier"]] titleVariants:[RCTConvert NSStringArray:json[@"titleVariants"]] image:image repeats:[RCTConvert BOOL:json[@"repeats"]]];
|
|
1370
1796
|
}
|
|
1371
1797
|
|
|
1372
1798
|
- (NSString*)panDirectionToString:(CPPanDirection)panDirection {
|
|
@@ -1522,6 +1948,35 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1522
1948
|
NSNumber* index = [item.userInfo objectForKey:@"index"];
|
|
1523
1949
|
[self sendTemplateEventWithName:listTemplate name:@"didSelectListItem" json:@{ @"index": index }];
|
|
1524
1950
|
self.selectedResultBlock = completionHandler;
|
|
1951
|
+
|
|
1952
|
+
// Check if we should trigger scroll to bottom callback for infinite scroll
|
|
1953
|
+
NSDictionary *templateUserInfo = [listTemplate userInfo];
|
|
1954
|
+
NSNumber *scrollBottomThreshold = [templateUserInfo objectForKey:@"scrollBottomThreshold"];
|
|
1955
|
+
|
|
1956
|
+
if (scrollBottomThreshold) {
|
|
1957
|
+
// Calculate total number of items across all sections
|
|
1958
|
+
NSInteger totalItems = 0;
|
|
1959
|
+
for (CPListSection *section in listTemplate.sections) {
|
|
1960
|
+
totalItems += section.items.count;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Get the current index
|
|
1964
|
+
NSInteger currentIndex = [index integerValue];
|
|
1965
|
+
|
|
1966
|
+
// Calculate remaining items
|
|
1967
|
+
NSInteger remainingItems = totalItems - currentIndex - 1;
|
|
1968
|
+
|
|
1969
|
+
// If we're within the threshold from the end, fire the event
|
|
1970
|
+
if (remainingItems <= [scrollBottomThreshold integerValue]) {
|
|
1971
|
+
NSString *templateId = [templateUserInfo objectForKey:@"templateId"];
|
|
1972
|
+
if (self->hasListeners && templateId) {
|
|
1973
|
+
[self sendEventWithName:@"scrollToBottom" body:@{
|
|
1974
|
+
@"templateId": templateId,
|
|
1975
|
+
@"remainingItems": @(remainingItems)
|
|
1976
|
+
}];
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1525
1980
|
}
|
|
1526
1981
|
|
|
1527
1982
|
# pragma TabBarTemplate
|