@javascriptcommon/react-native-carplay 2.4.7 → 2.4.9
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 +556 -122
- package/ios/RNCarPlay.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/RNCarPlay.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/RNCarPlay.xcodeproj/project.xcworkspace/xcuserdata/birkir.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/RNCarPlay.xcodeproj/xcuserdata/birkir.xcuserdatad/xcschemes/xcschememanagement.plist +19 -0
- package/lib/templates/Template.d.ts.map +1 -1
- package/lib/templates/Template.js +14 -5
- package/package.json +1 -1
- package/src/templates/Template.ts +14 -5
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
|
}
|
|
@@ -167,69 +171,191 @@ RCT_EXPORT_MODULE();
|
|
|
167
171
|
return resizedImage;
|
|
168
172
|
}
|
|
169
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
|
+
|
|
170
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
|
+
|
|
171
295
|
if (placeholderImage != nil) {
|
|
172
|
-
|
|
173
|
-
[item setImage:placeholderImage];
|
|
174
|
-
});
|
|
296
|
+
[item setImage:placeholderImage];
|
|
175
297
|
}
|
|
176
298
|
|
|
177
|
-
|
|
299
|
+
// Use React Native's image loader
|
|
300
|
+
RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]];
|
|
301
|
+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imgUrlString]];
|
|
178
302
|
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
} else {
|
|
186
|
-
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);
|
|
187
309
|
}
|
|
188
310
|
}];
|
|
189
|
-
[task resume];
|
|
190
311
|
}
|
|
191
312
|
|
|
192
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
|
+
|
|
193
328
|
if (placeholderImage != nil) {
|
|
194
|
-
|
|
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
|
+
|
|
195
347
|
NSMutableArray* newImages = [item.gridImages mutableCopy];
|
|
196
|
-
|
|
197
348
|
@try {
|
|
198
|
-
newImages[index] =
|
|
349
|
+
newImages[index] = image;
|
|
350
|
+
[item updateImages:newImages];
|
|
199
351
|
}
|
|
200
352
|
@catch (NSException *exception) {
|
|
201
|
-
// Best effort updating the array
|
|
202
353
|
NSLog(@"Failed to update images array of CPListImageRowItem");
|
|
203
354
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
NSURL *imgUrl = [NSURL URLWithString:imgUrlString];
|
|
210
|
-
|
|
211
|
-
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:imgUrl completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
|
212
|
-
if (data) {
|
|
213
|
-
UIImage *image = [UIImage imageWithData:data];
|
|
214
|
-
|
|
215
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
216
|
-
NSMutableArray* newImages = [item.gridImages mutableCopy];
|
|
217
|
-
|
|
218
|
-
@try {
|
|
219
|
-
newImages[index] = image;
|
|
220
|
-
}
|
|
221
|
-
@catch (NSException *exception) {
|
|
222
|
-
// Best effort updating the array
|
|
223
|
-
NSLog(@"Failed to update images array of CPListImageRowItem");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
[item updateImages:newImages];
|
|
227
|
-
});
|
|
228
|
-
} else {
|
|
229
|
-
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);
|
|
230
357
|
}
|
|
231
358
|
}];
|
|
232
|
-
[task resume];
|
|
233
359
|
}
|
|
234
360
|
|
|
235
361
|
RCT_EXPORT_METHOD(checkForConnection) {
|
|
@@ -325,52 +451,102 @@ RCT_EXPORT_METHOD(createTemplate:(NSString *)templateId config:(NSDictionary*)co
|
|
|
325
451
|
[nowPlayingTemplate setAlbumArtistButtonEnabled:[RCTConvert BOOL:config[@"albumArtistButtonEnabled"]]];
|
|
326
452
|
[nowPlayingTemplate setUpNextTitle:[RCTConvert NSString:config[@"upNextButtonTitle"]]];
|
|
327
453
|
[nowPlayingTemplate setUpNextButtonEnabled:[RCTConvert BOOL:config[@"upNextButtonEnabled"]]];
|
|
328
|
-
|
|
454
|
+
|
|
329
455
|
NSArray<NSDictionary*> *_buttons = [RCTConvert NSDictionaryArray:config[@"buttons"]];
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
@"playback": CPNowPlayingPlaybackRateButton.class,
|
|
336
|
-
@"repeat": CPNowPlayingRepeatButton.class,
|
|
337
|
-
@"image": CPNowPlayingImageButton.class
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
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];
|
|
341
461
|
NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
}];
|
|
358
510
|
}
|
|
359
|
-
}
|
|
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];
|
|
360
520
|
}
|
|
361
|
-
|
|
362
|
-
[buttons addObject:button];
|
|
363
521
|
}
|
|
364
|
-
|
|
365
|
-
|
|
522
|
+
|
|
523
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
524
|
+
[nowPlayingTemplate updateNowPlayingButtons:buttons];
|
|
525
|
+
});
|
|
526
|
+
}];
|
|
527
|
+
|
|
366
528
|
carPlayTemplate = nowPlayingTemplate;
|
|
367
529
|
} else if ([type isEqualToString:@"tabbar"]) {
|
|
368
|
-
|
|
530
|
+
NSArray<CPTemplate*> *templates = [self parseTemplatesFrom:config];
|
|
531
|
+
|
|
532
|
+
// Set tabTitle BEFORE creating TabBarTemplate
|
|
533
|
+
NSArray<NSDictionary*> *tpls = [RCTConvert NSDictionaryArray:config[@"templates"]];
|
|
534
|
+
for (NSUInteger i = 0; i < templates.count && i < tpls.count; i++) {
|
|
535
|
+
NSDictionary *tplConfig = tpls[i];
|
|
536
|
+
CPTemplate *tpl = templates[i];
|
|
537
|
+
if (tplConfig[@"tabTitle"]) {
|
|
538
|
+
tpl.tabTitle = [RCTConvert NSString:tplConfig[@"tabTitle"]];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
CPTabBarTemplate *tabBarTemplate = [[CPTabBarTemplate alloc] initWithTemplates:templates];
|
|
369
543
|
tabBarTemplate.delegate = self;
|
|
544
|
+
|
|
370
545
|
carPlayTemplate = tabBarTemplate;
|
|
371
546
|
} else if ([type isEqualToString:@"contact"]) {
|
|
372
547
|
NSString *nm = [RCTConvert NSString:config[@"name"]];
|
|
373
|
-
|
|
548
|
+
id imageSource = config[@"image"];
|
|
549
|
+
UIImage *img = [self isImageSourceURL:imageSource] ? nil : [RCTConvert UIImage:imageSource];
|
|
374
550
|
CPContact *contact = [[CPContact alloc] initWithName:nm image:img];
|
|
375
551
|
[contact setSubtitle:config[@"subtitle"]];
|
|
376
552
|
[contact setActions:[self parseButtons:config[@"actions"] templateId:templateId]];
|
|
@@ -452,7 +628,13 @@ RCT_EXPORT_METHOD(createTemplate:(NSString *)templateId config:(NSDictionary*)co
|
|
|
452
628
|
carPlayTemplate.tabImage = [UIImage systemImageNamed:[RCTConvert NSString:config[@"tabSystemImageName"]]];
|
|
453
629
|
}
|
|
454
630
|
if (config[@"tabImage"]) {
|
|
455
|
-
|
|
631
|
+
id tabImageSource = config[@"tabImage"];
|
|
632
|
+
if ([self isImageSourceURL:tabImageSource]) {
|
|
633
|
+
// Tab images with URLs need to be loaded async, but tabImage setter doesn't update later
|
|
634
|
+
// For now, skip URL-based tab images
|
|
635
|
+
} else {
|
|
636
|
+
carPlayTemplate.tabImage = [RCTConvert UIImage:tabImageSource];
|
|
637
|
+
}
|
|
456
638
|
}
|
|
457
639
|
if (config[@"tabTitle"]) {
|
|
458
640
|
carPlayTemplate.tabTitle = [RCTConvert NSString:config[@"tabTitle"]];
|
|
@@ -569,11 +751,12 @@ RCT_EXPORT_METHOD(setRootTemplate:(NSString *)templateId animated:(BOOL)animated
|
|
|
569
751
|
|
|
570
752
|
if (template) {
|
|
571
753
|
[store.interfaceController setRootTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
572
|
-
|
|
573
|
-
|
|
754
|
+
if (err) {
|
|
755
|
+
NSLog(@"Set root template error: %@", err);
|
|
756
|
+
}
|
|
574
757
|
}];
|
|
575
758
|
} else {
|
|
576
|
-
NSLog(@"Failed to find template %@",
|
|
759
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
577
760
|
}
|
|
578
761
|
}
|
|
579
762
|
|
|
@@ -581,12 +764,19 @@ RCT_EXPORT_METHOD(pushTemplate:(NSString *)templateId animated:(BOOL)animated) {
|
|
|
581
764
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
582
765
|
CPTemplate *template = [store findTemplateById:templateId];
|
|
583
766
|
if (template) {
|
|
767
|
+
// Check if template is already on the navigation stack
|
|
768
|
+
NSArray *currentTemplates = store.interfaceController.templates;
|
|
769
|
+
if ([currentTemplates containsObject:template]) {
|
|
770
|
+
NSLog(@"Template %@ is already on the navigation stack, skipping push", templateId);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
584
773
|
[store.interfaceController pushTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
585
|
-
|
|
586
|
-
|
|
774
|
+
if (err) {
|
|
775
|
+
NSLog(@"Push template error: %@", err);
|
|
776
|
+
}
|
|
587
777
|
}];
|
|
588
778
|
} else {
|
|
589
|
-
NSLog(@"Failed to find template %@",
|
|
779
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
590
780
|
}
|
|
591
781
|
}
|
|
592
782
|
|
|
@@ -595,27 +785,30 @@ RCT_EXPORT_METHOD(popToTemplate:(NSString *)templateId animated:(BOOL)animated)
|
|
|
595
785
|
CPTemplate *template = [store findTemplateById:templateId];
|
|
596
786
|
if (template) {
|
|
597
787
|
[store.interfaceController popToTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
598
|
-
|
|
599
|
-
|
|
788
|
+
if (err) {
|
|
789
|
+
NSLog(@"Pop to template error: %@", err);
|
|
790
|
+
}
|
|
600
791
|
}];
|
|
601
792
|
} else {
|
|
602
|
-
NSLog(@"Failed to find template %@",
|
|
793
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
603
794
|
}
|
|
604
795
|
}
|
|
605
796
|
|
|
606
797
|
RCT_EXPORT_METHOD(popToRootTemplate:(BOOL)animated) {
|
|
607
798
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
608
799
|
[store.interfaceController popToRootTemplateAnimated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
609
|
-
|
|
610
|
-
|
|
800
|
+
if (err) {
|
|
801
|
+
NSLog(@"Pop to root template error: %@", err);
|
|
802
|
+
}
|
|
611
803
|
}];
|
|
612
804
|
}
|
|
613
805
|
|
|
614
806
|
RCT_EXPORT_METHOD(popTemplate:(BOOL)animated) {
|
|
615
807
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
616
808
|
[store.interfaceController popTemplateAnimated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
617
|
-
|
|
618
|
-
|
|
809
|
+
if (err) {
|
|
810
|
+
NSLog(@"Pop template error: %@", err);
|
|
811
|
+
}
|
|
619
812
|
}];
|
|
620
813
|
}
|
|
621
814
|
|
|
@@ -623,12 +816,18 @@ RCT_EXPORT_METHOD(presentTemplate:(NSString *)templateId animated:(BOOL)animated
|
|
|
623
816
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
624
817
|
CPTemplate *template = [store findTemplateById:templateId];
|
|
625
818
|
if (template) {
|
|
819
|
+
// Check if template is already presented
|
|
820
|
+
if (store.interfaceController.presentedTemplate == template) {
|
|
821
|
+
NSLog(@"Template %@ is already presented, skipping", templateId);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
626
824
|
[store.interfaceController presentTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
|
|
627
|
-
|
|
628
|
-
|
|
825
|
+
if (err) {
|
|
826
|
+
NSLog(@"Present template error: %@", err);
|
|
827
|
+
}
|
|
629
828
|
}];
|
|
630
829
|
} else {
|
|
631
|
-
NSLog(@"Failed to find template %@",
|
|
830
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
632
831
|
}
|
|
633
832
|
}
|
|
634
833
|
|
|
@@ -637,6 +836,152 @@ RCT_EXPORT_METHOD(dismissTemplate:(BOOL)animated) {
|
|
|
637
836
|
[store.interfaceController dismissTemplateAnimated:animated];
|
|
638
837
|
}
|
|
639
838
|
|
|
839
|
+
RCT_EXPORT_METHOD(updateTemplate:(NSString *)templateId config:(NSDictionary*)config) {
|
|
840
|
+
RNCPStore *store = [RNCPStore sharedManager];
|
|
841
|
+
CPTemplate *template = [store findTemplateById:templateId];
|
|
842
|
+
NSString *type = [RCTConvert NSString:config[@"type"]];
|
|
843
|
+
|
|
844
|
+
if (!template) {
|
|
845
|
+
NSLog(@"Failed to find template %@", templateId);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Route to specific update methods based on template type
|
|
850
|
+
if ([template isKindOfClass:[CPListTemplate class]]) {
|
|
851
|
+
CPListTemplate *listTemplate = (CPListTemplate *)template;
|
|
852
|
+
if (config[@"leadingNavigationBarButtons"]) {
|
|
853
|
+
NSArray *leadingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"leadingNavigationBarButtons"]] templateId:templateId];
|
|
854
|
+
[listTemplate setLeadingNavigationBarButtons:leadingNavigationBarButtons];
|
|
855
|
+
}
|
|
856
|
+
if (config[@"trailingNavigationBarButtons"]) {
|
|
857
|
+
NSArray *trailingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"trailingNavigationBarButtons"]] templateId:templateId];
|
|
858
|
+
[listTemplate setTrailingNavigationBarButtons:trailingNavigationBarButtons];
|
|
859
|
+
}
|
|
860
|
+
if (config[@"emptyViewTitleVariants"]) {
|
|
861
|
+
listTemplate.emptyViewTitleVariants = [RCTConvert NSArray:config[@"emptyViewTitleVariants"]];
|
|
862
|
+
}
|
|
863
|
+
if (config[@"emptyViewSubtitleVariants"]) {
|
|
864
|
+
listTemplate.emptyViewSubtitleVariants = [RCTConvert NSArray:config[@"emptyViewSubtitleVariants"]];
|
|
865
|
+
}
|
|
866
|
+
if (config[@"sections"]) {
|
|
867
|
+
[listTemplate updateSections:[self parseSections:[RCTConvert NSArray:config[@"sections"]] templateId:templateId]];
|
|
868
|
+
}
|
|
869
|
+
} else if ([template isKindOfClass:[CPGridTemplate class]]) {
|
|
870
|
+
CPGridTemplate *gridTemplate = (CPGridTemplate *)template;
|
|
871
|
+
if (config[@"leadingNavigationBarButtons"]) {
|
|
872
|
+
NSArray *leadingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"leadingNavigationBarButtons"]] templateId:templateId];
|
|
873
|
+
[gridTemplate setLeadingNavigationBarButtons:leadingNavigationBarButtons];
|
|
874
|
+
}
|
|
875
|
+
if (config[@"trailingNavigationBarButtons"]) {
|
|
876
|
+
NSArray *trailingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"trailingNavigationBarButtons"]] templateId:templateId];
|
|
877
|
+
[gridTemplate setTrailingNavigationBarButtons:trailingNavigationBarButtons];
|
|
878
|
+
}
|
|
879
|
+
if (config[@"buttons"]) {
|
|
880
|
+
NSArray *buttons = [self parseGridButtons:[RCTConvert NSArray:config[@"buttons"]] templateId:templateId];
|
|
881
|
+
[gridTemplate updateGridButtons:buttons];
|
|
882
|
+
}
|
|
883
|
+
} else if ([template isKindOfClass:[CPNowPlayingTemplate class]]) {
|
|
884
|
+
CPNowPlayingTemplate *nowPlayingTemplate = (CPNowPlayingTemplate *)template;
|
|
885
|
+
if (config[@"albumArtistButtonEnabled"]) {
|
|
886
|
+
[nowPlayingTemplate setAlbumArtistButtonEnabled:[RCTConvert BOOL:config[@"albumArtistButtonEnabled"]]];
|
|
887
|
+
}
|
|
888
|
+
if (config[@"upNextButtonTitle"]) {
|
|
889
|
+
[nowPlayingTemplate setUpNextTitle:[RCTConvert NSString:config[@"upNextButtonTitle"]]];
|
|
890
|
+
}
|
|
891
|
+
if (config[@"upNextButtonEnabled"]) {
|
|
892
|
+
[nowPlayingTemplate setUpNextButtonEnabled:[RCTConvert BOOL:config[@"upNextButtonEnabled"]]];
|
|
893
|
+
}
|
|
894
|
+
if (config[@"buttons"]) {
|
|
895
|
+
NSArray<NSDictionary*> *_buttons = [RCTConvert NSDictionaryArray:config[@"buttons"]];
|
|
896
|
+
|
|
897
|
+
// Collect all image sources for image-type buttons
|
|
898
|
+
NSMutableArray *imageSources = [NSMutableArray new];
|
|
899
|
+
NSMutableArray *imageIndices = [NSMutableArray new]; // Track which buttons need images
|
|
900
|
+
|
|
901
|
+
for (NSUInteger i = 0; i < _buttons.count; i++) {
|
|
902
|
+
NSDictionary *_button = _buttons[i];
|
|
903
|
+
NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
|
|
904
|
+
if ([buttonType isEqualToString:@"image"]) {
|
|
905
|
+
id imageSource = [_button objectForKey:@"image"];
|
|
906
|
+
[imageSources addObject:imageSource ?: [NSNull null]];
|
|
907
|
+
[imageIndices addObject:@(i)];
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Pre-load all images, then create buttons
|
|
912
|
+
[self loadImagesForNowPlayingButtons:imageSources completion:^(NSArray *loadedImages) {
|
|
913
|
+
NSMutableArray<CPNowPlayingButton *> *buttons = [NSMutableArray new];
|
|
914
|
+
|
|
915
|
+
NSDictionary *buttonTypeMapping = @{
|
|
916
|
+
@"shuffle": CPNowPlayingShuffleButton.class,
|
|
917
|
+
@"add-to-library": CPNowPlayingAddToLibraryButton.class,
|
|
918
|
+
@"more": CPNowPlayingMoreButton.class,
|
|
919
|
+
@"playback": CPNowPlayingPlaybackRateButton.class,
|
|
920
|
+
@"repeat": CPNowPlayingRepeatButton.class,
|
|
921
|
+
@"image": CPNowPlayingImageButton.class
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
NSUInteger imageIndex = 0;
|
|
925
|
+
for (NSUInteger i = 0; i < _buttons.count; i++) {
|
|
926
|
+
NSDictionary *_button = _buttons[i];
|
|
927
|
+
NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
|
|
928
|
+
NSDictionary *body = @{@"templateId":templateId, @"id": _button[@"id"] ?: @"" };
|
|
929
|
+
Class buttonClass = buttonTypeMapping[buttonType];
|
|
930
|
+
|
|
931
|
+
if (buttonClass) {
|
|
932
|
+
CPNowPlayingButton *button;
|
|
933
|
+
|
|
934
|
+
if ([buttonType isEqualToString:@"image"]) {
|
|
935
|
+
id imageObj = (imageIndex < loadedImages.count) ? loadedImages[imageIndex] : nil;
|
|
936
|
+
UIImage *image = ([imageObj isKindOfClass:[UIImage class]]) ? (UIImage *)imageObj : nil;
|
|
937
|
+
imageIndex++;
|
|
938
|
+
|
|
939
|
+
if (image) {
|
|
940
|
+
button = [[CPNowPlayingImageButton alloc] initWithImage:image handler:^(__kindof CPNowPlayingImageButton * _Nonnull btn) {
|
|
941
|
+
if (self->hasListeners) {
|
|
942
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
943
|
+
}
|
|
944
|
+
}];
|
|
945
|
+
} else {
|
|
946
|
+
// Fallback: create a minimal placeholder if image failed to load
|
|
947
|
+
UIImage *placeholder = [UIImage systemImageNamed:@"circle"];
|
|
948
|
+
button = [[CPNowPlayingImageButton alloc] initWithImage:placeholder handler:^(__kindof CPNowPlayingImageButton * _Nonnull btn) {
|
|
949
|
+
if (self->hasListeners) {
|
|
950
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
951
|
+
}
|
|
952
|
+
}];
|
|
953
|
+
}
|
|
954
|
+
} else {
|
|
955
|
+
button = [[buttonClass alloc] initWithHandler:^(__kindof CPNowPlayingButton * _Nonnull btn) {
|
|
956
|
+
if (self->hasListeners) {
|
|
957
|
+
[self sendEventWithName:@"buttonPressed" body:body];
|
|
958
|
+
}
|
|
959
|
+
}];
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
[buttons addObject:button];
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
967
|
+
[nowPlayingTemplate updateNowPlayingButtons:buttons];
|
|
968
|
+
});
|
|
969
|
+
}];
|
|
970
|
+
}
|
|
971
|
+
} else if ([template isKindOfClass:[CPMapTemplate class]]) {
|
|
972
|
+
CPMapTemplate *mapTemplate = (CPMapTemplate *)template;
|
|
973
|
+
[self applyConfigForMapTemplate:mapTemplate templateId:templateId config:config];
|
|
974
|
+
} else if ([template isKindOfClass:[CPInformationTemplate class]]) {
|
|
975
|
+
CPInformationTemplate *informationTemplate = (CPInformationTemplate *)template;
|
|
976
|
+
if (config[@"items"]) {
|
|
977
|
+
informationTemplate.items = [self parseInformationItems:[RCTConvert NSArray:config[@"items"]]];
|
|
978
|
+
}
|
|
979
|
+
if (config[@"actions"]) {
|
|
980
|
+
informationTemplate.actions = [self parseInformationActions:[RCTConvert NSArray:config[@"actions"]] templateId:templateId];
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
640
985
|
RCT_EXPORT_METHOD(updateListTemplate:(NSString*)templateId config:(NSDictionary*)config) {
|
|
641
986
|
RNCPStore *store = [RNCPStore sharedManager];
|
|
642
987
|
CPTemplate *template = [store findTemplateById:templateId];
|
|
@@ -702,14 +1047,21 @@ RCT_EXPORT_METHOD(updateListTemplateItem:(NSString *)templateId config:(NSDictio
|
|
|
702
1047
|
CPListItem *item = (CPListItem *)section.items[index];
|
|
703
1048
|
if (config[@"imgUrl"]) {
|
|
704
1049
|
NSString *imgUrlString = [RCTConvert NSString:config[@"imgUrl"]];
|
|
705
|
-
UIImage *placeholderImage;
|
|
706
|
-
|
|
707
|
-
|
|
1050
|
+
UIImage *placeholderImage = nil;
|
|
1051
|
+
id placeholderSource = config[@"placeholderImage"];
|
|
1052
|
+
if (placeholderSource != nil && ![self isImageSourceURL:placeholderSource]) {
|
|
1053
|
+
placeholderImage = [RCTConvert UIImage:placeholderSource];
|
|
708
1054
|
}
|
|
709
1055
|
[self updateItemImageWithURL:item imgUrl:imgUrlString placeholderImage:placeholderImage];
|
|
710
1056
|
}
|
|
711
1057
|
if (config[@"image"]) {
|
|
712
|
-
|
|
1058
|
+
id imageSource = config[@"image"];
|
|
1059
|
+
if ([self isImageSourceURL:imageSource]) {
|
|
1060
|
+
NSString *imgUrlString = [self getURLStringFromImageSource:imageSource];
|
|
1061
|
+
[self updateItemImageWithURL:item imgUrl:imgUrlString placeholderImage:nil];
|
|
1062
|
+
} else {
|
|
1063
|
+
[item setImage:[RCTConvert UIImage:imageSource]];
|
|
1064
|
+
}
|
|
713
1065
|
}
|
|
714
1066
|
if (config[@"text"]) {
|
|
715
1067
|
[item setText:[RCTConvert NSString:config[@"text"]]];
|
|
@@ -724,7 +1076,16 @@ RCT_EXPORT_METHOD(updateListTemplateItem:(NSString *)templateId config:(NSDictio
|
|
|
724
1076
|
[item setPlaybackProgress:[RCTConvert CGFloat:config[@"playbackProgress"]]];
|
|
725
1077
|
}
|
|
726
1078
|
if (@available(iOS 14.0, *) && config[@"accessoryImage"]) {
|
|
727
|
-
|
|
1079
|
+
id accessorySource = config[@"accessoryImage"];
|
|
1080
|
+
if ([self isImageSourceURL:accessorySource]) {
|
|
1081
|
+
[self loadImageAsync:accessorySource completion:^(UIImage *image) {
|
|
1082
|
+
if (image) {
|
|
1083
|
+
[item setAccessoryImage:image];
|
|
1084
|
+
}
|
|
1085
|
+
}];
|
|
1086
|
+
} else {
|
|
1087
|
+
[item setAccessoryImage:[RCTConvert UIImage:accessorySource]];
|
|
1088
|
+
}
|
|
728
1089
|
}
|
|
729
1090
|
} else {
|
|
730
1091
|
NSLog(@"Failed to find template %@", template);
|
|
@@ -1024,7 +1385,10 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1024
1385
|
NSArray<NSDictionary*> *tpls = [RCTConvert NSDictionaryArray:config[@"templates"]];
|
|
1025
1386
|
for (NSDictionary *tpl in tpls) {
|
|
1026
1387
|
CPTemplate *templ = [store findTemplateById:tpl[@"id"]];
|
|
1027
|
-
//
|
|
1388
|
+
// If template doesn't have tabTitle set, try to set it from config
|
|
1389
|
+
if (tpl[@"tabTitle"] && (!templ.tabTitle || [templ.tabTitle length] == 0)) {
|
|
1390
|
+
templ.tabTitle = [RCTConvert NSString:tpl[@"tabTitle"]];
|
|
1391
|
+
}
|
|
1028
1392
|
[templates addObject:templ];
|
|
1029
1393
|
}
|
|
1030
1394
|
return templates;
|
|
@@ -1086,8 +1450,18 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1086
1450
|
NSString *_title = [barButton objectForKey:@"title"];
|
|
1087
1451
|
[_barButton setTitle:_title];
|
|
1088
1452
|
} else if (_type == CPBarButtonTypeImage) {
|
|
1089
|
-
|
|
1090
|
-
[
|
|
1453
|
+
id imageSource = [barButton objectForKey:@"image"];
|
|
1454
|
+
if ([self isImageSourceURL:imageSource]) {
|
|
1455
|
+
// Load image asynchronously
|
|
1456
|
+
[self loadImageAsync:imageSource completion:^(UIImage *image) {
|
|
1457
|
+
if (image) {
|
|
1458
|
+
[_barButton setImage:image];
|
|
1459
|
+
}
|
|
1460
|
+
}];
|
|
1461
|
+
} else {
|
|
1462
|
+
UIImage *_image = [RCTConvert UIImage:imageSource];
|
|
1463
|
+
[_barButton setImage:_image];
|
|
1464
|
+
}
|
|
1091
1465
|
}
|
|
1092
1466
|
[result addObject:_barButton];
|
|
1093
1467
|
}
|
|
@@ -1110,6 +1484,27 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1110
1484
|
return result;
|
|
1111
1485
|
}
|
|
1112
1486
|
|
|
1487
|
+
// Helper to check if image source is a URL
|
|
1488
|
+
- (BOOL)isImageSourceURL:(id)imageSource {
|
|
1489
|
+
if ([imageSource isKindOfClass:[NSDictionary class]]) {
|
|
1490
|
+
NSDictionary *imageDict = (NSDictionary *)imageSource;
|
|
1491
|
+
NSString *uri = imageDict[@"uri"];
|
|
1492
|
+
if (uri && ([uri hasPrefix:@"http://"] || [uri hasPrefix:@"https://"])) {
|
|
1493
|
+
return YES;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return NO;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Helper to get URL string from image source
|
|
1500
|
+
- (NSString *)getURLStringFromImageSource:(id)imageSource {
|
|
1501
|
+
if ([imageSource isKindOfClass:[NSDictionary class]]) {
|
|
1502
|
+
NSDictionary *imageDict = (NSDictionary *)imageSource;
|
|
1503
|
+
return imageDict[@"uri"];
|
|
1504
|
+
}
|
|
1505
|
+
return nil;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1113
1508
|
- (NSArray<CPSelectableListItem>*)parseListItems:(NSArray*)items startIndex:(int)startIndex templateId:(NSString *)templateId {
|
|
1114
1509
|
NSMutableArray *_items = [NSMutableArray array];
|
|
1115
1510
|
int listIndex = startIndex;
|
|
@@ -1118,12 +1513,14 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1118
1513
|
NSString *_detailText = [item objectForKey:@"detailText"];
|
|
1119
1514
|
NSString *_text = [item objectForKey:@"text"];
|
|
1120
1515
|
NSObject *_imageObj = [item objectForKey:@"image"];
|
|
1121
|
-
|
|
1516
|
+
|
|
1122
1517
|
NSArray *_imageItems = [item objectForKey:@"images"];
|
|
1123
1518
|
NSArray *_imageUrls = [item objectForKey:@"imgUrls"];
|
|
1124
|
-
|
|
1519
|
+
|
|
1125
1520
|
if (_imageItems == nil && _imageUrls == nil) {
|
|
1126
|
-
|
|
1521
|
+
// Check if image is a URL - if so, use async loading
|
|
1522
|
+
BOOL isURL = [self isImageSourceURL:_imageObj];
|
|
1523
|
+
UIImage *_image = isURL ? nil : [RCTConvert UIImage:_imageObj];
|
|
1127
1524
|
CPListItem *_item;
|
|
1128
1525
|
if (@available(iOS 14.0, *)) {
|
|
1129
1526
|
CPListItemAccessoryType accessoryType = _showsDisclosureIndicator ? CPListItemAccessoryTypeDisclosureIndicator : CPListItemAccessoryTypeNone;
|
|
@@ -1136,11 +1533,16 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1136
1533
|
}
|
|
1137
1534
|
if (item[@"imgUrl"]) {
|
|
1138
1535
|
NSString *imgUrlString = [RCTConvert NSString:item[@"imgUrl"]];
|
|
1139
|
-
UIImage *placeholderImage;
|
|
1140
|
-
|
|
1141
|
-
|
|
1536
|
+
UIImage *placeholderImage = nil;
|
|
1537
|
+
id placeholderSource = [item objectForKey:@"placeholderImage"];
|
|
1538
|
+
if (placeholderSource != nil && ![self isImageSourceURL:placeholderSource]) {
|
|
1539
|
+
placeholderImage = [RCTConvert UIImage:placeholderSource];
|
|
1142
1540
|
}
|
|
1143
1541
|
[self updateItemImageWithURL:_item imgUrl:imgUrlString placeholderImage:placeholderImage];
|
|
1542
|
+
} else if (isURL) {
|
|
1543
|
+
// Load image asynchronously for URL-based images
|
|
1544
|
+
NSString *imgUrlString = [self getURLStringFromImageSource:_imageObj];
|
|
1545
|
+
[self updateItemImageWithURL:_item imgUrl:imgUrlString placeholderImage:nil];
|
|
1144
1546
|
}
|
|
1145
1547
|
[_item setUserInfo:@{ @"index": @(listIndex) }];
|
|
1146
1548
|
[_items addObject:_item];
|
|
@@ -1152,8 +1554,13 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1152
1554
|
NSArray* slicedArray = [_imageItems subarrayWithRange:NSMakeRange(0, MIN(CPMaximumNumberOfGridImages, _imageItems.count))];
|
|
1153
1555
|
|
|
1154
1556
|
for (NSObject *imageObj in slicedArray){
|
|
1155
|
-
|
|
1156
|
-
[
|
|
1557
|
+
// Skip URL-based images for initial creation (use placeholder)
|
|
1558
|
+
if ([self isImageSourceURL:imageObj]) {
|
|
1559
|
+
[_images addObject:[[UIImage alloc] init]];
|
|
1560
|
+
} else {
|
|
1561
|
+
UIImage *_image = [RCTConvert UIImage:imageObj];
|
|
1562
|
+
[_images addObject:_image];
|
|
1563
|
+
}
|
|
1157
1564
|
}
|
|
1158
1565
|
}
|
|
1159
1566
|
if (@available(iOS 14.0, *)) {
|
|
@@ -1181,7 +1588,8 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1181
1588
|
UIImage *placeholderImage = nil;
|
|
1182
1589
|
if (_placeholderImages != nil && _index < _placeholderImages.count) {
|
|
1183
1590
|
id placeholderImageObj = _placeholderImages[_index];
|
|
1184
|
-
|
|
1591
|
+
// Only load placeholder synchronously if it's not a URL
|
|
1592
|
+
if (placeholderImageObj != nil && placeholderImageObj != [NSNull null] && ![self isImageSourceURL:placeholderImageObj]) {
|
|
1185
1593
|
placeholderImage = [RCTConvert UIImage:placeholderImageObj];
|
|
1186
1594
|
}
|
|
1187
1595
|
}
|
|
@@ -1239,10 +1647,22 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1239
1647
|
for (NSDictionary *button in buttons) {
|
|
1240
1648
|
NSString *_id = [button objectForKey:@"id"];
|
|
1241
1649
|
NSArray<NSString*> *_titleVariants = [button objectForKey:@"titleVariants"];
|
|
1242
|
-
|
|
1650
|
+
id imageSource = [button objectForKey:@"image"];
|
|
1651
|
+
UIImage *_image;
|
|
1652
|
+
|
|
1653
|
+
if ([self isImageSourceURL:imageSource]) {
|
|
1654
|
+
// CPGridButton doesn't support updating image after creation
|
|
1655
|
+
// For URL-based images, we create with placeholder and load async
|
|
1656
|
+
_image = [[UIImage alloc] init];
|
|
1657
|
+
// Note: In production, images should be bundled locally to avoid this issue
|
|
1658
|
+
} else {
|
|
1659
|
+
_image = [RCTConvert UIImage:imageSource];
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
int currentIndex = index; // Capture for block
|
|
1243
1663
|
CPGridButton *_button = [[CPGridButton alloc] initWithTitleVariants:_titleVariants image:_image handler:^(CPGridButton * _Nonnull barButton) {
|
|
1244
1664
|
if (self->hasListeners) {
|
|
1245
|
-
[self sendEventWithName:@"gridButtonPressed" body:@{@"id": _id, @"templateId":templateId, @"index": @(
|
|
1665
|
+
[self sendEventWithName:@"gridButtonPressed" body:@{@"id": _id, @"templateId":templateId, @"index": @(currentIndex) }];
|
|
1246
1666
|
}
|
|
1247
1667
|
}];
|
|
1248
1668
|
BOOL _disabled = [button objectForKey:@"disabled"];
|
|
@@ -1281,8 +1701,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1281
1701
|
CPManeuver* maneuver = [[CPManeuver alloc] init];
|
|
1282
1702
|
|
|
1283
1703
|
if ([json objectForKey:@"junctionImage"]) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1704
|
+
id junctionSource = json[@"junctionImage"];
|
|
1705
|
+
if (![self isImageSourceURL:junctionSource]) {
|
|
1706
|
+
UIImage *junctionImage = [RCTConvert UIImage:junctionSource];
|
|
1707
|
+
[maneuver setJunctionImage:[self imageWithTint:junctionImage andTintColor:[UIColor whiteColor]]];
|
|
1708
|
+
}
|
|
1286
1709
|
}
|
|
1287
1710
|
|
|
1288
1711
|
if ([json objectForKey:@"initialTravelEstimates"]) {
|
|
@@ -1291,7 +1714,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1291
1714
|
}
|
|
1292
1715
|
|
|
1293
1716
|
if ([json objectForKey:@"symbolImage"]) {
|
|
1294
|
-
|
|
1717
|
+
id symbolSource = json[@"symbolImage"];
|
|
1718
|
+
if ([self isImageSourceURL:symbolSource]) {
|
|
1719
|
+
return maneuver; // Skip URL-based symbol images for now
|
|
1720
|
+
}
|
|
1721
|
+
UIImage *symbolImage = [RCTConvert UIImage:symbolSource];
|
|
1295
1722
|
|
|
1296
1723
|
if ([json objectForKey:@"symbolImageSize"]) {
|
|
1297
1724
|
NSDictionary *size = [RCTConvert NSDictionary:json[@"symbolImageSize"]];
|
|
@@ -1348,9 +1775,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1348
1775
|
}
|
|
1349
1776
|
|
|
1350
1777
|
- (CPNavigationAlert*)parseNavigationAlert:(NSDictionary*)json templateId:(NSString*)templateId {
|
|
1351
|
-
CPImageSet *imageSet;
|
|
1352
|
-
|
|
1353
|
-
|
|
1778
|
+
CPImageSet *imageSet = nil;
|
|
1779
|
+
id lightSource = json[@"lightImage"];
|
|
1780
|
+
id darkSource = json[@"darkImage"];
|
|
1781
|
+
if (lightSource && darkSource && ![self isImageSourceURL:lightSource] && ![self isImageSourceURL:darkSource]) {
|
|
1782
|
+
imageSet = [[CPImageSet alloc] initWithLightContentImage:[RCTConvert UIImage:lightSource] darkContentImage:[RCTConvert UIImage:darkSource]];
|
|
1354
1783
|
}
|
|
1355
1784
|
CPAlertAction *secondaryAction = [json objectForKey:@"secondaryAction"] ? [self parseAlertAction:json[@"secondaryAction"] body:@{ @"templateId": templateId, @"secondary": @(YES) }] : nil;
|
|
1356
1785
|
|
|
@@ -1374,7 +1803,12 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
|
|
|
1374
1803
|
}
|
|
1375
1804
|
|
|
1376
1805
|
- (CPVoiceControlState*)parseVoiceControlState:(NSDictionary*)json {
|
|
1377
|
-
|
|
1806
|
+
id imageSource = json[@"image"];
|
|
1807
|
+
UIImage *image = nil;
|
|
1808
|
+
if (imageSource && ![self isImageSourceURL:imageSource]) {
|
|
1809
|
+
image = [RCTConvert UIImage:imageSource];
|
|
1810
|
+
}
|
|
1811
|
+
return [[CPVoiceControlState alloc] initWithIdentifier:[RCTConvert NSString:json[@"identifier"]] titleVariants:[RCTConvert NSStringArray:json[@"titleVariants"]] image:image repeats:[RCTConvert BOOL:json[@"repeats"]]];
|
|
1378
1812
|
}
|
|
1379
1813
|
|
|
1380
1814
|
- (NSString*)panDirectionToString:(CPPanDirection)panDirection {
|