@iternio/react-native-auto-play 0.5.14-beta.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +270 -36
  2. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +5 -0
  3. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplate.kt +9 -2
  4. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/NativeBackdrop.kt +40 -0
  5. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt +52 -0
  6. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/GridTemplate.kt +47 -13
  7. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +69 -15
  8. package/ios/Types.swift +3 -0
  9. package/ios/extensions/UIImage+Toggle.swift +5 -2
  10. package/ios/hybrid/HybridAutoPlay.swift +28 -16
  11. package/ios/hybrid/HybridMapTemplate.swift +22 -5
  12. package/ios/scenes/AutoPlayInterfaceController.swift +261 -72
  13. package/ios/templates/AutoPlayMapPanelDelegate.swift +190 -0
  14. package/ios/templates/AutoPlayTemplate.swift +24 -2
  15. package/ios/templates/GridTemplate.swift +57 -57
  16. package/ios/templates/InformationTemplate.swift +84 -9
  17. package/ios/templates/ListTemplate.swift +66 -11
  18. package/ios/templates/MapTemplate.swift +83 -68
  19. package/ios/templates/MessageTemplate.swift +72 -6
  20. package/ios/templates/Parser.swift +564 -69
  21. package/ios/templates/SearchTemplate.swift +1 -1
  22. package/ios/templates/TemplateStore.swift +5 -15
  23. package/ios/utils/SymbolFont.swift +13 -28
  24. package/jest.js +2 -0
  25. package/lib/Constants.d.ts +4 -0
  26. package/lib/Constants.js +6 -0
  27. package/lib/index.d.ts +1 -0
  28. package/lib/index.js +1 -0
  29. package/lib/index.web.d.ts +73 -0
  30. package/lib/index.web.js +72 -1
  31. package/lib/specs/MapTemplate.nitro.d.ts +3 -1
  32. package/lib/templates/GridTemplate.d.ts +23 -3
  33. package/lib/templates/GridTemplate.js +8 -0
  34. package/lib/templates/InformationTemplate.d.ts +38 -12
  35. package/lib/templates/ListTemplate.d.ts +16 -47
  36. package/lib/templates/MapTemplate.d.ts +26 -2
  37. package/lib/templates/MapTemplate.js +16 -2
  38. package/lib/templates/MessageTemplate.d.ts +52 -11
  39. package/lib/templates/MessageTemplate.js +1 -1
  40. package/lib/types/Image.d.ts +6 -1
  41. package/lib/utils/NitroColor.d.ts +1 -0
  42. package/lib/utils/NitroColor.js +8 -0
  43. package/lib/utils/NitroImage.js +1 -1
  44. package/lib/utils/NitroManeuver.js +1 -3
  45. package/lib/utils/NitroOptionsPanel.d.ts +103 -0
  46. package/lib/utils/NitroOptionsPanel.js +63 -0
  47. package/lib/utils/NitroSection.d.ts +89 -2
  48. package/lib/utils/NitroSection.js +49 -2
  49. package/lib-cjs/AutoPlayHeadlessJsTask.js +16 -0
  50. package/lib-cjs/Constants.js +9 -0
  51. package/lib-cjs/components/MapTemplateContext.js +10 -0
  52. package/lib-cjs/components/SafeAreaInsetsContext.js +21 -0
  53. package/lib-cjs/components/SafeAreaView.js +22 -0
  54. package/lib-cjs/components/WindowInformationWrapper.js +20 -0
  55. package/lib-cjs/hooks/useAndroidAutoTelemetry.js +115 -0
  56. package/lib-cjs/hooks/useFocusedEffect.js +35 -0
  57. package/lib-cjs/hooks/useMapTemplate.js +13 -0
  58. package/lib-cjs/hooks/useSafeAreaInsets.js +9 -0
  59. package/lib-cjs/hooks/useVoiceInput.js +42 -0
  60. package/lib-cjs/hybrid/HybridAndroidAutoTelemetry.js +4 -0
  61. package/lib-cjs/hybrid/HybridAndroidWindowInformation.js +4 -0
  62. package/lib-cjs/hybrid/HybridAutoPlay.js +5 -0
  63. package/lib-cjs/hybrid/HybridVoice.js +61 -0
  64. package/lib-cjs/index.js +76 -0
  65. package/lib-cjs/index.web.js +98 -0
  66. package/lib-cjs/scenes/AutoPlayCluster.js +140 -0
  67. package/lib-cjs/scenes/CarPlayDashboardScene.js +100 -0
  68. package/lib-cjs/specs/AndroidAutoTelemetry.nitro.js +2 -0
  69. package/lib-cjs/specs/AndroidAutomotive.nitro.js +16 -0
  70. package/lib-cjs/specs/AndroidWindowInformation.nitro.js +2 -0
  71. package/lib-cjs/specs/AutoPlay.nitro.js +2 -0
  72. package/lib-cjs/specs/CarPlayDashboard.nitro.js +2 -0
  73. package/lib-cjs/specs/Cluster.nitro.js +2 -0
  74. package/lib-cjs/specs/GridTemplate.nitro.js +2 -0
  75. package/lib-cjs/specs/InformationTemplate.nitro.js +2 -0
  76. package/lib-cjs/specs/ListTemplate.nitro.js +2 -0
  77. package/lib-cjs/specs/MapTemplate.nitro.js +2 -0
  78. package/lib-cjs/specs/MessageTemplate.nitro.js +2 -0
  79. package/lib-cjs/specs/SearchTemplate.nitro.js +2 -0
  80. package/lib-cjs/specs/SignInTemplate.nitro.js +2 -0
  81. package/lib-cjs/specs/Voice.nitro.js +2 -0
  82. package/lib-cjs/templates/GridTemplate.js +41 -0
  83. package/lib-cjs/templates/InformationTemplate.js +49 -0
  84. package/lib-cjs/templates/ListTemplate.js +33 -0
  85. package/lib-cjs/templates/MapTemplate.js +168 -0
  86. package/lib-cjs/templates/MessageTemplate.js +50 -0
  87. package/lib-cjs/templates/SearchTemplate.js +32 -0
  88. package/lib-cjs/templates/SignInTemplate.js +55 -0
  89. package/lib-cjs/templates/Template.js +40 -0
  90. package/lib-cjs/types/Button.js +12 -0
  91. package/lib-cjs/types/Event.js +2 -0
  92. package/lib-cjs/types/Image.js +2 -0
  93. package/lib-cjs/types/Maneuver.js +77 -0
  94. package/lib-cjs/types/RootComponent.js +2 -0
  95. package/lib-cjs/types/SignInMethod.js +22 -0
  96. package/lib-cjs/types/Telemetry.js +24 -0
  97. package/lib-cjs/types/Text.js +8 -0
  98. package/lib-cjs/types/Trip.js +2 -0
  99. package/lib-cjs/types/Voice.js +2 -0
  100. package/lib-cjs/utils/ErrorUtil.js +34 -0
  101. package/lib-cjs/utils/NitroAction.js +129 -0
  102. package/lib-cjs/utils/NitroAlert.js +19 -0
  103. package/lib-cjs/utils/NitroAttributedString.js +14 -0
  104. package/lib-cjs/utils/NitroColor.js +32 -0
  105. package/lib-cjs/utils/NitroGrid.js +12 -0
  106. package/lib-cjs/utils/NitroImage.js +97 -0
  107. package/lib-cjs/utils/NitroManeuver.js +79 -0
  108. package/lib-cjs/utils/NitroMapButton.js +33 -0
  109. package/lib-cjs/utils/NitroOptionsPanel.js +66 -0
  110. package/lib-cjs/utils/NitroSection.js +112 -0
  111. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -0
  112. package/nitrogen/generated/android/c++/JChargingConnector.hpp +79 -0
  113. package/nitrogen/generated/android/c++/JGridImageSize.hpp +64 -0
  114. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +7 -1
  115. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -0
  116. package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +12 -0
  117. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +12 -0
  118. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +78 -3
  119. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.hpp +2 -1
  120. package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +12 -0
  121. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +6 -0
  122. package/nitrogen/generated/android/c++/JListImageType.hpp +67 -0
  123. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +6 -0
  124. package/nitrogen/generated/android/c++/JNitroChargerLocation.hpp +115 -0
  125. package/nitrogen/generated/android/c++/JNitroChargerOutlet.hpp +82 -0
  126. package/nitrogen/generated/android/c++/JNitroColor.hpp +8 -4
  127. package/nitrogen/generated/android/c++/JNitroOptionsPanelChargerSection.hpp +112 -0
  128. package/nitrogen/generated/android/c++/JNitroOptionsPanelConfig.hpp +157 -0
  129. package/nitrogen/generated/android/c++/JNitroOptionsPanelGridSection.hpp +102 -0
  130. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.cpp +30 -0
  131. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.hpp +129 -0
  132. package/nitrogen/generated/android/c++/JNitroRow.hpp +33 -3
  133. package/nitrogen/generated/android/c++/JNitroSection.hpp +6 -0
  134. package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +6 -0
  135. package/nitrogen/generated/android/c++/JWaypointCoordinate.hpp +65 -0
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ChargingConnector.kt +30 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridImageSize.kt +25 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +9 -4
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplateSpec.kt +5 -1
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListImageType.kt +26 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerLocation.kt +90 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerOutlet.kt +70 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroColor.kt +9 -4
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelChargerSection.kt +61 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelConfig.kt +61 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelGridSection.kt +56 -0
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelSection.kt +75 -0
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +36 -6
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/WaypointCoordinate.kt +61 -0
  150. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +174 -0
  151. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +27 -0
  152. package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
  153. package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +9 -0
  154. package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +9 -0
  155. package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +47 -1
  156. package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +9 -0
  157. package/nitrogen/generated/ios/swift/ChargingConnector.swift +68 -0
  158. package/nitrogen/generated/ios/swift/GridImageSize.swift +48 -0
  159. package/nitrogen/generated/ios/swift/GridTemplateConfig.swift +12 -1
  160. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec.swift +2 -1
  161. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec_cxx.swift +31 -4
  162. package/nitrogen/generated/ios/swift/ListImageType.swift +52 -0
  163. package/nitrogen/generated/ios/swift/NitroChargerLocation.swift +161 -0
  164. package/nitrogen/generated/ios/swift/NitroChargerOutlet.swift +65 -0
  165. package/nitrogen/generated/ios/swift/NitroColor.swift +20 -2
  166. package/nitrogen/generated/ios/swift/NitroOptionsPanelChargerSection.swift +64 -0
  167. package/nitrogen/generated/ios/swift/NitroOptionsPanelConfig.swift +94 -0
  168. package/nitrogen/generated/ios/swift/NitroOptionsPanelGridSection.swift +53 -0
  169. package/nitrogen/generated/ios/swift/NitroOptionsPanelSection.swift +32 -0
  170. package/nitrogen/generated/ios/swift/NitroRow.swift +81 -1
  171. package/nitrogen/generated/ios/swift/WaypointCoordinate.swift +52 -0
  172. package/nitrogen/generated/shared/c++/ChargingConnector.hpp +104 -0
  173. package/nitrogen/generated/shared/c++/GridImageSize.hpp +84 -0
  174. package/nitrogen/generated/shared/c++/GridTemplateConfig.hpp +8 -1
  175. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.cpp +1 -0
  176. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.hpp +5 -1
  177. package/nitrogen/generated/shared/c++/ListImageType.hpp +88 -0
  178. package/nitrogen/generated/shared/c++/NitroChargerLocation.hpp +131 -0
  179. package/nitrogen/generated/shared/c++/NitroChargerOutlet.hpp +98 -0
  180. package/nitrogen/generated/shared/c++/NitroColor.hpp +7 -3
  181. package/nitrogen/generated/shared/c++/NitroOptionsPanelChargerSection.hpp +98 -0
  182. package/nitrogen/generated/shared/c++/NitroOptionsPanelConfig.hpp +107 -0
  183. package/nitrogen/generated/shared/c++/NitroOptionsPanelGridSection.hpp +91 -0
  184. package/nitrogen/generated/shared/c++/NitroRow.hpp +39 -2
  185. package/nitrogen/generated/shared/c++/WaypointCoordinate.hpp +91 -0
  186. package/package.json +7 -3
  187. package/src/Constants.ts +9 -0
  188. package/src/index.ts +1 -0
  189. package/src/index.web.ts +166 -0
  190. package/src/specs/MapTemplate.nitro.ts +3 -1
  191. package/src/templates/GridTemplate.ts +38 -3
  192. package/src/templates/InformationTemplate.ts +60 -29
  193. package/src/templates/ListTemplate.ts +27 -55
  194. package/src/templates/MapTemplate.ts +60 -2
  195. package/src/templates/MessageTemplate.ts +71 -24
  196. package/src/types/Image.ts +6 -1
  197. package/src/utils/NitroColor.ts +10 -1
  198. package/src/utils/NitroImage.ts +1 -5
  199. package/src/utils/NitroManeuver.ts +1 -4
  200. package/src/utils/NitroOptionsPanel.ts +218 -0
  201. package/src/utils/NitroSection.ts +160 -5
@@ -8,92 +8,92 @@
8
8
  import CarPlay
9
9
 
10
10
  class GridTemplate: AutoPlayHeaderProviding {
11
- let template: CPGridTemplate
11
+ /// `nil` when this template renders as a map panel instead (see `mapPanel`).
12
+ private(set) var template: CPGridTemplate?
12
13
  var config: GridTemplateConfig
13
14
 
14
15
  var buttons: [NitroGridButton]
15
16
 
17
+ /// Boxed as `Any` since `CPMapPanel` itself is only available on iOS 27+ while this class supports a much lower deployment target.
18
+ private var mapPanel: Any?
19
+
20
+ /// `CPMapPanel.delegate` is `weak`, so this keeps the shared delegate alive for as long as the panel exists. Boxed as `Any` for the same reason as `mapPanel`.
21
+ private var mapPanelDelegate: Any?
22
+
16
23
  override var autoDismissMs: Double? {
17
24
  return config.autoDismissMs
18
25
  }
19
26
 
20
- override func getTemplate() -> CPTemplate {
27
+ override func getTemplate() throws -> CPTemplate {
28
+ guard let template else {
29
+ throw AutoPlayError.templateIsMapPanel(config.id)
30
+ }
21
31
  return template
22
32
  }
23
33
 
34
+ override func getPanel() -> Any? {
35
+ return mapPanel
36
+ }
37
+
38
+ override func getPanelHeaderActions() -> [NitroAction]? {
39
+ return barButtons
40
+ }
41
+
42
+ override func getPanelMapButtons() -> [NitroMapButton]? {
43
+ return config.mapConfig?.mapButtons
44
+ }
45
+
24
46
  init(config: GridTemplateConfig) {
25
47
  self.config = config
26
48
  buttons = config.buttons
27
49
 
28
- template = CPGridTemplate(
29
- title: Parser.parseText(text: config.title),
30
- gridButtons: GridTemplate.parseButtons(buttons: buttons),
31
- id: config.id
32
- )
50
+ let title = Parser.parseText(text: config.title)
51
+
52
+ if #available(iOS 27.0, *), config.mapConfig != nil {
53
+ mapPanel = CPMapPanel(
54
+ title: title,
55
+ sections: [CPMapPanelSection(title: nil, items: [])],
56
+ buttonConfiguration: nil
57
+ )
58
+ }
59
+ else {
60
+ template = CPGridTemplate(
61
+ title: title,
62
+ gridButtons: GridTemplate.parseButtons(buttons: buttons),
63
+ id: config.id
64
+ )
65
+ }
33
66
 
34
67
  super.init()
35
68
 
36
69
  barButtons = config.headerActions
37
70
 
71
+ if #available(iOS 27.0, *), let mapPanel = mapPanel as? CPMapPanel {
72
+ let delegate = AutoPlayMapPanelDelegate(template: self, templateId: config.id)
73
+ mapPanelDelegate = delegate
74
+ mapPanel.delegate = delegate
75
+ }
38
76
  }
39
77
 
40
78
  static func parseButtons(buttons: [NitroGridButton]) -> [CPGridButton] {
41
- let gridButtonHeight: CGFloat
42
-
43
- if #available(iOS 26.0, *) {
44
- gridButtonHeight = CPGridTemplate.maximumGridButtonImageSize.height
45
- }
46
- else {
47
- gridButtonHeight = 44
48
- }
49
-
50
- guard let traitCollection = SceneStore.getRootTraitCollection() else {
51
- return []
52
- }
53
-
54
- return buttons.compactMap { button in
55
- var image: UIImage?
56
-
57
- if let glyphImage = button.image.glyphImage {
58
- image = SymbolFont.imageFromNitroImage(
59
- image: glyphImage,
60
- size: gridButtonHeight,
61
- traitCollection: traitCollection
62
- )
63
- }
64
-
65
- if let assetImage = button.image.assetImage {
66
- image = Parser.parseAssetImage(
67
- assetImage: assetImage,
68
- traitCollection: traitCollection
69
- )
70
- }
71
-
72
- if let remoteImage = button.image.remoteImage {
73
- image = Parser.parseRemoteImage(
74
- remoteImage: remoteImage,
75
- traitCollection: traitCollection
76
- )
77
- }
78
-
79
- guard let image = image else { return nil }
80
- guard let title = Parser.parseText(text: button.title) else { return nil }
81
-
82
- return CPGridButton(
83
- titleVariants: [title],
84
- image: image
85
- ) { _ in
86
- button.onPress()
87
- }
88
- }
79
+ guard let traitCollection = SceneStore.getRootTraitCollection() else { return [] }
80
+ return Parser.parseGridButtons(buttons: buttons, traitCollection: traitCollection)
89
81
  }
90
82
 
91
83
  @MainActor
92
84
  override func _invalidate() {
85
+ if #available(iOS 27.0, *), let mapPanel = mapPanel as? CPMapPanel {
86
+ let parsedButtons = GridTemplate.parseButtons(buttons: buttons)
87
+ mapPanel.sections.first?.items = [CPMapPanelItem(gridButtons: parsedButtons)]
88
+ reapplyPanelBarIfVisible(self, templateId: config.id)
89
+ return
90
+ }
91
+
92
+ guard let template else { return }
93
+
93
94
  setBarButtons(template: template, barButtons: barButtons)
94
95
 
95
- let buttons = GridTemplate.parseButtons(buttons: buttons)
96
- template.updateGridButtons(buttons)
96
+ template.updateGridButtons(GridTemplate.parseButtons(buttons: buttons))
97
97
  }
98
98
 
99
99
  override func onWillAppear(animated: Bool) {
@@ -8,31 +8,69 @@
8
8
  import CarPlay
9
9
 
10
10
  class InformationTemplate: AutoPlayHeaderProviding {
11
- let template: CPInformationTemplate
11
+ /// `nil` when this template renders as a map panel instead (see `mapPanel`).
12
+ private(set) var template: CPInformationTemplate?
12
13
  var config: InformationTemplateConfig
13
14
 
14
15
  var section: NitroSection
15
16
 
17
+ /// Whether this template renders as a `CPMapPanel` instead of a plain `CPInformationTemplate`.
18
+ private let usesMapPanel: Bool
19
+
20
+ /// Built lazily on the first `_invalidate()` call rather than in `init()`: `CPMapPanel.buttonConfiguration`
21
+ /// is readonly, so unlike sections it can't be patched in later, and building it needs
22
+ /// `traitCollection` for the optional symbol button image — only safe to fetch on the
23
+ /// main thread, which `init()` isn't guaranteed to run on. Boxed as `Any` since `CPMapPanel`
24
+ /// itself is only available on iOS 27+ while this class supports a much lower deployment target.
25
+ private var mapPanel: Any?
26
+
27
+ /// `CPMapPanel.delegate` is `weak`, so this keeps the shared delegate alive for as long as the panel exists. Boxed as `Any` for the same reason as `mapPanel`.
28
+ private var mapPanelDelegate: Any?
29
+
16
30
  override var autoDismissMs: Double? {
17
31
  return config.autoDismissMs
18
32
  }
19
33
 
20
- override func getTemplate() -> CPTemplate {
34
+ override func getTemplate() throws -> CPTemplate {
35
+ guard let template else {
36
+ throw AutoPlayError.templateIsMapPanel(config.id)
37
+ }
21
38
  return template
22
39
  }
23
40
 
41
+ override func getPanel() -> Any? {
42
+ return mapPanel
43
+ }
44
+
45
+ override func getPanelHeaderActions() -> [NitroAction]? {
46
+ return barButtons
47
+ }
48
+
49
+ override func getPanelMapButtons() -> [NitroMapButton]? {
50
+ return config.mapConfig?.mapButtons
51
+ }
52
+
24
53
  init(config: InformationTemplateConfig) {
25
54
  self.config = config
26
55
 
27
56
  section = config.section
28
57
 
29
- template = CPInformationTemplate(
30
- title: Parser.parseText(text: config.title)!,
31
- layout: .leading,
32
- items: Parser.parseInformationItems(section: section),
33
- actions: Parser.parseInformationActions(actions: config.actions),
34
- id: config.id
35
- )
58
+ if #available(iOS 27.0, *) {
59
+ usesMapPanel = config.mapConfig != nil
60
+ }
61
+ else {
62
+ usesMapPanel = false
63
+ }
64
+
65
+ if !usesMapPanel {
66
+ template = CPInformationTemplate(
67
+ title: Parser.parseText(text: config.title)!,
68
+ layout: .leading,
69
+ items: Parser.parseInformationItems(section: section),
70
+ actions: Parser.parseInformationActions(actions: config.actions),
71
+ id: config.id
72
+ )
73
+ }
36
74
 
37
75
  super.init()
38
76
 
@@ -41,6 +79,43 @@ class InformationTemplate: AutoPlayHeaderProviding {
41
79
 
42
80
  @MainActor
43
81
  override func _invalidate() {
82
+ if usesMapPanel, #available(iOS 27.0, *) {
83
+ guard let mapPanel = mapPanel as? CPMapPanel else {
84
+ guard let traitCollection = SceneStore.getRootTraitCollection() else { return }
85
+
86
+ // InformationTemplate's actions are only ever set once and never updated
87
+ // afterwards, so there's nothing to keep in sync with buttonConfiguration later.
88
+ let panel = CPMapPanel(
89
+ title: Parser.parseText(text: config.title),
90
+ sections: [
91
+ CPMapPanelSection(
92
+ title: nil,
93
+ items: Parser.parseInformationPanelItems(section: section)
94
+ )
95
+ ],
96
+ buttonConfiguration: Parser.parsePanelButtonConfiguration(
97
+ actions: config.actions,
98
+ traitCollection: traitCollection
99
+ )
100
+ )
101
+
102
+ mapPanel = panel
103
+
104
+ let delegate = AutoPlayMapPanelDelegate(template: self, templateId: config.id)
105
+ mapPanelDelegate = delegate
106
+ panel.delegate = delegate
107
+
108
+ reapplyPanelBarIfVisible(self, templateId: config.id)
109
+ return
110
+ }
111
+
112
+ mapPanel.sections.first?.items = Parser.parseInformationPanelItems(section: section)
113
+ reapplyPanelBarIfVisible(self, templateId: config.id)
114
+ return
115
+ }
116
+
117
+ guard let template else { return }
118
+
44
119
  setBarButtons(template: template, barButtons: barButtons)
45
120
  template.items = Parser.parseInformationItems(section: section)
46
121
  }
@@ -8,44 +8,99 @@
8
8
  import CarPlay
9
9
 
10
10
  class ListTemplate: AutoPlayHeaderProviding {
11
- let template: CPListTemplate
11
+ /// `nil` when this template renders as a map panel instead (see `mapPanel`).
12
+ private(set) var template: CPListTemplate?
12
13
  var config: ListTemplateConfig
13
14
 
14
15
  var sections: [NitroSection]?
15
16
 
17
+ /// Boxed as `Any` since `CPMapPanel` itself is only available on
18
+ /// iOS 27+ while this class supports a much lower deployment target.
19
+ private var mapPanel: Any?
20
+
21
+ /// `CPMapPanel.delegate` is `weak`, so this keeps the shared delegate alive for as long as
22
+ /// the panel exists. Boxed as `Any` for the same reason as `mapPanel`.
23
+ private var mapPanelDelegate: Any?
24
+
16
25
  override var autoDismissMs: Double? {
17
26
  return config.autoDismissMs
18
27
  }
19
28
 
20
- override func getTemplate() -> CPTemplate {
29
+ override func getTemplate() throws -> CPTemplate {
30
+ guard let template else {
31
+ throw AutoPlayError.templateIsMapPanel(config.id)
32
+ }
21
33
  return template
22
34
  }
23
35
 
36
+ override func getPanel() -> Any? {
37
+ return mapPanel
38
+ }
39
+
40
+ override func getPanelHeaderActions() -> [NitroAction]? {
41
+ return barButtons
42
+ }
43
+
44
+ override func getPanelMapButtons() -> [NitroMapButton]? {
45
+ return config.mapConfig?.mapButtons
46
+ }
47
+
24
48
  init(config: ListTemplateConfig) {
25
49
  self.config = config
26
50
 
27
51
  sections = config.sections
28
-
29
- template = CPListTemplate(
30
- title: Parser.parseText(text: config.title),
31
- sections: [],
32
- assistantCellConfiguration: nil,
33
- id: config.id
34
- )
52
+ let title = Parser.parseText(text: config.title)
53
+
54
+ if #available(iOS 27.0, *), config.mapConfig != nil {
55
+ mapPanel = CPMapPanel(
56
+ title: title,
57
+ sections: (config.sections ?? []).map { section in
58
+ CPMapPanelSection(title: section.title, items: [])
59
+ },
60
+ buttonConfiguration: nil
61
+ )
62
+ }
63
+ else {
64
+ template = CPListTemplate(
65
+ title: title,
66
+ sections: [],
67
+ assistantCellConfiguration: nil,
68
+ id: config.id
69
+ )
70
+ }
35
71
 
36
72
  super.init()
37
73
 
38
74
  barButtons = config.headerActions
75
+
76
+ if #available(iOS 27.0, *), let mapPanel = mapPanel as? CPMapPanel {
77
+ let delegate = AutoPlayMapPanelDelegate(template: self, templateId: config.id)
78
+ mapPanelDelegate = delegate
79
+ mapPanel.delegate = delegate
80
+ }
39
81
  }
40
82
 
41
83
  @MainActor
42
84
  override func _invalidate() {
43
- setBarButtons(template: template, barButtons: barButtons)
44
-
45
85
  guard let traitCollection = SceneStore.getRootTraitCollection() else {
46
86
  return
47
87
  }
48
88
 
89
+ if #available(iOS 27.0, *), let mapPanel = mapPanel as? CPMapPanel {
90
+ mapPanel.sections = Parser.parseMapPanelSections(
91
+ sections: sections,
92
+ updateSection: self.updateSection(section:sectionIndex:),
93
+ traitCollection: traitCollection
94
+ )
95
+
96
+ reapplyPanelBarIfVisible(self, templateId: config.id)
97
+ return
98
+ }
99
+
100
+ guard let template else { return }
101
+
102
+ setBarButtons(template: template, barButtons: barButtons)
103
+
49
104
  template.updateSections(
50
105
  Parser.parseSections(
51
106
  sections: sections,
@@ -22,6 +22,8 @@ class MapTemplate: AutoPlayHeaderProviding,
22
22
  var mapButtons: [NitroMapButton]?
23
23
  var visibleTravelEstimate: VisibleTravelEstimate?
24
24
 
25
+ private var optionsPanel: NitroOptionsPanelConfig?
26
+
25
27
  override var autoDismissMs: Double? {
26
28
  return config.autoDismissMs
27
29
  }
@@ -92,67 +94,6 @@ class MapTemplate: AutoPlayHeaderProviding,
92
94
  }
93
95
  }
94
96
 
95
- func parseMapButtons(mapButtons: [NitroMapButton]) -> [CPMapButton] {
96
- guard let traitCollection = SceneStore.getRootTraitCollection() else {
97
- return []
98
- }
99
-
100
- return mapButtons.map { button in
101
- if let glyphImage = button.image.glyphImage,
102
- let icon = SymbolFont.imageFromNitroImage(
103
- image: glyphImage,
104
- size: CPButtonMaximumImageSize.height,
105
- traitCollection: traitCollection
106
- )
107
- {
108
- return CPMapButton(image: icon) { _ in
109
- if button.type == .pan {
110
- self.onPanButtonPress()
111
- return
112
- }
113
- button.onPress?()
114
- }
115
- }
116
- if let assetImage = button.image.assetImage,
117
- let icon = Parser.parseAssetImage(
118
- assetImage: assetImage,
119
- traitCollection: traitCollection
120
- )
121
- {
122
- return CPMapButton(image: icon) { _ in
123
- if button.type == .pan {
124
- self.onPanButtonPress()
125
- return
126
- }
127
- button.onPress?()
128
- }
129
- }
130
- if let remoteImage = button.image.remoteImage,
131
- let icon = Parser.parseRemoteImage(
132
- remoteImage: remoteImage,
133
- traitCollection: traitCollection
134
- )
135
- {
136
- return CPMapButton(image: icon) { _ in
137
- if button.type == .pan {
138
- self.onPanButtonPress()
139
- return
140
- }
141
- button.onPress?()
142
- }
143
- }
144
-
145
- return CPMapButton { _ in
146
- if button.type == .pan {
147
- self.onPanButtonPress()
148
- return
149
- }
150
- button.onPress?()
151
- }
152
- }
153
-
154
- }
155
-
156
97
  @MainActor
157
98
  override func _invalidate() {
158
99
  if tripSelectorVisible {
@@ -176,15 +117,35 @@ class MapTemplate: AutoPlayHeaderProviding,
176
117
  button.type != .pan
177
118
  } ?? []
178
119
 
179
- template.mapButtons = parseMapButtons(mapButtons: mapButtons)
120
+ template.mapButtons = Parser.parseMapButtons(
121
+ mapButtons: mapButtons,
122
+ onPanButtonPress: onPanButtonPress
123
+ )
180
124
 
181
125
  return
182
126
  }
183
127
 
184
- setBarButtons(template: template, barButtons: barButtons)
128
+ // A panel owns the bar buttons while it exists; keep barButtons up to date but don't apply it.
129
+ let hasPanel: Bool = {
130
+ if #available(iOS 27.0, *) {
131
+ return SceneStore.getRootScene()?.interfaceController?.hasPanel ?? false
132
+ }
133
+ return false
134
+ }()
135
+
136
+ if !hasPanel {
137
+ // Restore CarPlay's default auto-hide (disabled while a panel is shown, see panelDidShow).
138
+ template.automaticallyHidesNavigationBar = true
139
+ template.hidesButtonsWithNavigationBar = true
140
+
141
+ setBarButtons(template: template, barButtons: barButtons)
185
142
 
186
- if let mapButtons = mapButtons {
187
- template.mapButtons = parseMapButtons(mapButtons: mapButtons)
143
+ if let mapButtons = mapButtons {
144
+ template.mapButtons = Parser.parseMapButtons(
145
+ mapButtons: mapButtons,
146
+ onPanButtonPress: onPanButtonPress
147
+ )
148
+ }
188
149
  }
189
150
  }
190
151
 
@@ -222,7 +183,7 @@ class MapTemplate: AutoPlayHeaderProviding,
222
183
 
223
184
  template.tripEstimateStyle = isDark ? .dark : .light
224
185
 
225
- invalidate()
186
+ super.traitCollectionDidChange()
226
187
  }
227
188
 
228
189
  // MARK: gestures
@@ -428,6 +389,10 @@ class MapTemplate: AutoPlayHeaderProviding,
428
389
  duration: alertConfig.durationMs / 1000
429
390
  )
430
391
 
392
+ if #available(iOS 27.0, *) {
393
+ alert.showsCloseButton = false
394
+ }
395
+
431
396
  func setNavigationAlert() {
432
397
  self.navigationAlert = .init(alert: alert, config: alertConfig)
433
398
  template.present(navigationAlert: alert, animated: true)
@@ -496,8 +461,9 @@ class MapTemplate: AutoPlayHeaderProviding,
496
461
 
497
462
  self.template.leadingNavigationBarButtons = []
498
463
  self.template.trailingNavigationBarButtons = []
499
- self.template.mapButtons = self.parseMapButtons(
500
- mapButtons: mapButtons
464
+ self.template.mapButtons = Parser.parseMapButtons(
465
+ mapButtons: mapButtons,
466
+ onPanButtonPress: self.onPanButtonPress
501
467
  )
502
468
  }
503
469
 
@@ -846,6 +812,7 @@ class MapTemplate: AutoPlayHeaderProviding,
846
812
  }
847
813
 
848
814
  self.navigationSession = template.startNavigationSession(for: trip)
815
+ applyOptionsPanel()
849
816
  }
850
817
 
851
818
  func stopNavigation() {
@@ -853,6 +820,54 @@ class MapTemplate: AutoPlayHeaderProviding,
853
820
  navigationSession = nil
854
821
  }
855
822
 
823
+ func updateOptionsPanel(config: NitroOptionsPanelConfig?) {
824
+ optionsPanel = config
825
+ applyOptionsPanel()
826
+ }
827
+
828
+ @available(iOS 27.0, *)
829
+ private func updateOptionsPanelSection(section: NitroSection, sectionIndex: Int) {
830
+ guard var sections = optionsPanel?.sections, sectionIndex < sections.count else { return }
831
+
832
+ // `.first` is the `NitroSection` (list) case of `NitroOptionsPanelSection` — matches
833
+ // whatever nitrogen assigned the last time `yarn specs` ran, see the ordering note on
834
+ // `Parser.parseOptionsPanelSections`.
835
+ sections[sectionIndex] = .first(section)
836
+ optionsPanel = NitroOptionsPanelConfig(
837
+ title: optionsPanel?.title,
838
+ sections: sections,
839
+ actions: optionsPanel?.actions
840
+ )
841
+ applyOptionsPanel()
842
+ }
843
+
844
+ private func applyOptionsPanel() {
845
+ guard #available(iOS 27.0, *) else { return }
846
+ guard let navigationSession else { return }
847
+ guard let optionsPanel else {
848
+ navigationSession.optionsPanel = nil
849
+ return
850
+ }
851
+ guard let traitCollection = SceneStore.getRootTraitCollection() else { return }
852
+
853
+ let sections = Parser.parseOptionsPanelSections(
854
+ sections: optionsPanel.sections,
855
+ updateSection: { [weak self] section, sectionIndex in
856
+ self?.updateOptionsPanelSection(section: section, sectionIndex: sectionIndex)
857
+ },
858
+ traitCollection: traitCollection
859
+ )
860
+
861
+ navigationSession.optionsPanel = CPMapPanel(
862
+ title: Parser.parseText(text: optionsPanel.title),
863
+ sections: sections,
864
+ buttonConfiguration: Parser.parsePanelButtonConfiguration(
865
+ actions: optionsPanel.actions,
866
+ traitCollection: traitCollection
867
+ )
868
+ )
869
+ }
870
+
856
871
  func setManeuverState(state: ManeuverState) {
857
872
  guard #available(iOS 17.4, *) else { return }
858
873
  guard let navigationSession = navigationSession else { return }
@@ -8,25 +8,91 @@
8
8
  import CarPlay
9
9
 
10
10
  class MessageTemplate: AutoPlayTemplate {
11
- let template: CPAlertTemplate
11
+ /// `nil` when this template renders as a map panel instead (see `mapPanel`).
12
+ private(set) var template: CPAlertTemplate?
12
13
  var config: MessageTemplateConfig
13
14
 
15
+ /// Whether this template renders as a `CPMapPanel` instead of a plain `CPAlertTemplate`.
16
+ private let usesMapPanel: Bool
17
+
18
+ /// Built lazily on the first `_invalidate()` call rather than in `init()`, same reasoning as `InformationTemplate`: `CPMapPanel.buttonConfiguration` is readonly and needs `traitCollection` for its optional symbol button image, which isn't safe to fetch off the main thread that `init()` isn't guaranteed to run on. Boxed as `Any` since `CPMapPanel` itself is only available on iOS 27+ while this class supports a much lower deployment target.
19
+ private var mapPanel: Any?
20
+
21
+ /// `CPMapPanel.delegate` is `weak`, so this keeps the shared delegate alive for as long as the panel exists. Boxed as `Any` for the same reason as `mapPanel`.
22
+ private var mapPanelDelegate: Any?
23
+
14
24
  override var autoDismissMs: Double? {
15
25
  return config.autoDismissMs
16
26
  }
17
27
 
18
- override func getTemplate() -> CPTemplate {
28
+ override func getTemplate() throws -> CPTemplate {
29
+ guard let template else {
30
+ throw AutoPlayError.templateIsMapPanel(config.id)
31
+ }
19
32
  return template
20
33
  }
21
34
 
35
+ override func getPanel() -> Any? {
36
+ return mapPanel
37
+ }
38
+
39
+ override func getPanelHeaderActions() -> [NitroAction]? {
40
+ return config.headerActions
41
+ }
42
+
43
+ override func getPanelMapButtons() -> [NitroMapButton]? {
44
+ return config.mapConfig?.mapButtons
45
+ }
46
+
22
47
  init(config: MessageTemplateConfig) {
23
48
  self.config = config
24
49
 
25
- template = CPAlertTemplate(
26
- titleVariants: [Parser.parseText(text: config.message)!],
27
- actions: Parser.parseAlertActions(alertActions: config.actions),
28
- id: config.id
50
+ if #available(iOS 27.0, *) {
51
+ usesMapPanel = config.mapConfig != nil
52
+ }
53
+ else {
54
+ usesMapPanel = false
55
+ }
56
+
57
+ if !usesMapPanel {
58
+ template = CPAlertTemplate(
59
+ titleVariants: [Parser.parseText(text: config.message)!],
60
+ actions: Parser.parseAlertActions(alertActions: config.actions),
61
+ id: config.id
62
+ )
63
+ }
64
+ }
65
+
66
+ @MainActor
67
+ override func _invalidate() {
68
+ guard usesMapPanel, #available(iOS 27.0, *), mapPanel == nil else { return }
69
+ guard let traitCollection = SceneStore.getRootTraitCollection() else { return }
70
+
71
+ // The message and its actions are only ever set once and never updated afterwards,
72
+ // so there's nothing to keep in sync with the panel's content later.
73
+ let panel = CPMapPanel(
74
+ title: Parser.parseText(text: config.title),
75
+ sections: [
76
+ CPMapPanelSection(
77
+ title: nil,
78
+ items: [
79
+ CPMapPanelItem(
80
+ listItem: CPListItem(text: Parser.parseText(text: config.message), detailText: nil)
81
+ )
82
+ ]
83
+ )
84
+ ],
85
+ buttonConfiguration: Parser.parsePanelButtonConfiguration(
86
+ actions: config.actions,
87
+ traitCollection: traitCollection
88
+ )
29
89
  )
90
+
91
+ mapPanel = panel
92
+
93
+ let delegate = AutoPlayMapPanelDelegate(template: self, templateId: config.id)
94
+ mapPanelDelegate = delegate
95
+ panel.delegate = delegate
30
96
  }
31
97
 
32
98
  override func onWillAppear(animated: Bool) {