@apexnative/navigation 1.0.0-alpha.2 → 1.0.0-alpha.4

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 (226) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/android/CLAUDE.md +38 -9
  3. package/android/CORE.md +11 -4
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
  6. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  8. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  9. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  12. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/StatusBarOptions.kt +7 -3
  14. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  15. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
  16. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  19. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  20. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  23. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  24. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  25. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  26. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  27. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  28. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/SecondaryScreenPresenter.kt +13 -5
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +92 -19
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt +1 -1
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +79 -13
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +29 -12
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +22 -18
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +45 -6
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +35 -9
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +10 -8
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +5 -1
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +72 -16
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
  53. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  54. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +164 -13
  55. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +26 -1
  56. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +136 -40
  57. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
  58. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +107 -4
  59. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  60. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
  61. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  62. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +131 -9
  63. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +75 -21
  64. package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
  65. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  66. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
  67. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
  68. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  69. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
  70. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
  71. package/cli/doctor/checks/app.js +61 -13
  72. package/cli/migrate/native.js +18 -0
  73. package/cli/setup/source.js +2 -1
  74. package/cli/setup/steps/mainApplication.js +134 -19
  75. package/docs-md/architecture/transitions.md +12 -7
  76. package/docs-md/containers/modals.md +4 -2
  77. package/docs-md/containers/overlays.md +3 -1
  78. package/docs-md/containers/side-menu.md +4 -2
  79. package/docs-md/containers/tabs.md +2 -0
  80. package/docs-md/flows/forms-in-a-modal.md +1 -1
  81. package/docs-md/help/glossary.md +2 -2
  82. package/docs-md/help/troubleshooting.md +20 -3
  83. package/docs-md/index.json +12 -0
  84. package/docs-md/migration/from-react-native-navigation.md +8 -2
  85. package/docs-md/screen-ui/back-and-gestures.md +8 -2
  86. package/docs-md/screen-ui/system-ui.md +1 -1
  87. package/docs-md/start/installation.md +3 -1
  88. package/ios/CLAUDE.md +17 -5
  89. package/ios/CORE.md +13 -8
  90. package/ios/Contract/ANNCommandAnswer.mm +5 -20
  91. package/ios/Contract/ANNCommandContract.h +15 -0
  92. package/ios/Contract/ANNCommandContract.mm +78 -0
  93. package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
  94. package/ios/Core/ANNErrorHandler.h +1 -1
  95. package/ios/Core/ANNErrorHandler.mm +3 -1
  96. package/ios/Core/ANNEventEmitter.h +1 -1
  97. package/ios/Core/ANNEventEmitter.mm +2 -2
  98. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  99. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  100. package/ios/Core/ANNNavigationState.h +7 -3
  101. package/ios/Core/ANNNavigationState.mm +10 -5
  102. package/ios/Core/ANNNodeHelper.h +32 -12
  103. package/ios/Core/ANNNodeHelper.mm +100 -3
  104. package/ios/Core/ANNRenderWait.h +15 -1
  105. package/ios/Core/ANNRenderWait.mm +119 -17
  106. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  107. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  108. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  109. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  110. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  111. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  112. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  113. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  114. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  115. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  116. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  117. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  118. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  119. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  120. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  121. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  122. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  123. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  124. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  125. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  126. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  127. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  128. package/ios/Options/ANNOptionsCache.h +7 -6
  129. package/ios/Options/ANNSearchBarOptions.h +4 -3
  130. package/ios/Options/ANNUserChoice.h +57 -0
  131. package/ios/Options/ANNUserChoice.mm +44 -0
  132. package/ios/Options/Params/ANNImageSource.h +61 -0
  133. package/ios/Options/Params/ANNImageSource.mm +101 -0
  134. package/ios/Options/Params/ANNOptionImages.h +37 -0
  135. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  136. package/ios/Options/Params/Image.h +11 -2
  137. package/ios/Options/Params/Image.mm +15 -2
  138. package/ios/Options/Params/ImageParser.h +5 -5
  139. package/ios/Options/Params/ImageParser.mm +28 -46
  140. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  141. package/ios/RECIPES.md +11 -5
  142. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  143. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  144. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  145. package/ios/TopBar/ANNBarButtons.h +9 -5
  146. package/ios/TopBar/ANNBarButtons.mm +211 -63
  147. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  148. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  149. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  150. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  151. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  152. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  153. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  154. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  155. package/lib/module/ARCHITECTURE.md +18 -18
  156. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  157. package/lib/module/api/hooks/useNavigation.js +8 -3
  158. package/lib/module/api/hooks/useNavigation.js.map +1 -1
  159. package/lib/module/api/navigation.js.map +1 -1
  160. package/lib/module/engine/commands.js +9 -7
  161. package/lib/module/engine/commands.js.map +1 -1
  162. package/lib/module/engine/layout.js +13 -9
  163. package/lib/module/engine/layout.js.map +1 -1
  164. package/lib/module/engine/options.js +44 -11
  165. package/lib/module/engine/options.js.map +1 -1
  166. package/lib/module/engine/places.js +44 -7
  167. package/lib/module/engine/places.js.map +1 -1
  168. package/lib/module/engine/props.js +30 -1
  169. package/lib/module/engine/props.js.map +1 -1
  170. package/lib/module/engine/rememberedOptions.js +1 -1
  171. package/lib/module/engine/rememberedOptions.js.map +1 -1
  172. package/lib/module/engine/screens.js +22 -12
  173. package/lib/module/engine/screens.js.map +1 -1
  174. package/lib/module/interfaces/contract/commands.json +101 -0
  175. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  176. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  177. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  178. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  179. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  180. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  181. package/lib/typescript/api/navigation.d.ts +4 -1
  182. package/lib/typescript/api/navigation.d.ts.map +1 -1
  183. package/lib/typescript/engine/commands.d.ts.map +1 -1
  184. package/lib/typescript/engine/options.d.ts +5 -2
  185. package/lib/typescript/engine/options.d.ts.map +1 -1
  186. package/lib/typescript/engine/places.d.ts +5 -2
  187. package/lib/typescript/engine/places.d.ts.map +1 -1
  188. package/lib/typescript/engine/props.d.ts +8 -1
  189. package/lib/typescript/engine/props.d.ts.map +1 -1
  190. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  191. package/lib/typescript/engine/screens.d.ts.map +1 -1
  192. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  193. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  194. package/lib/typescript/interfaces/Events.d.ts +19 -10
  195. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  196. package/lib/typescript/interfaces/options/animations.d.ts +15 -7
  197. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  198. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  199. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  200. package/lib/typescript/interfaces/options/modals.d.ts +18 -13
  201. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  202. package/lib/typescript/interfaces/options/system.d.ts +6 -3
  203. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  204. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  205. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  206. package/package.json +1 -1
  207. package/src/ARCHITECTURE.md +18 -18
  208. package/src/adapters/NativeANNTurboModule.ts +5 -2
  209. package/src/api/events/navigationEvents.ts +1 -1
  210. package/src/api/hooks/useNavigation.ts +9 -4
  211. package/src/api/navigation.ts +4 -1
  212. package/src/engine/commands.ts +19 -7
  213. package/src/engine/layout.ts +16 -9
  214. package/src/engine/options.ts +39 -11
  215. package/src/engine/places.ts +56 -7
  216. package/src/engine/props.ts +30 -1
  217. package/src/engine/rememberedOptions.ts +4 -1
  218. package/src/engine/screens.tsx +23 -13
  219. package/src/interfaces/ComponentEvents.ts +2 -2
  220. package/src/interfaces/Events.ts +19 -10
  221. package/src/interfaces/contract/commands.json +101 -0
  222. package/src/interfaces/options/animations.ts +15 -7
  223. package/src/interfaces/options/buttons.ts +13 -5
  224. package/src/interfaces/options/modals.ts +18 -13
  225. package/src/interfaces/options/system.ts +6 -3
  226. package/src/interfaces/options/topBar.ts +22 -9
@@ -228,13 +228,15 @@ class BottomTabs(context: Context) : BottomNavigationView(MaterialTheme.context(
228
228
  applyLongPressesAgain()
229
229
  }
230
230
 
231
- fun setIcon(index: Int, icon: Drawable) {
231
+ /** Null: the item has no icon. */
232
+ fun setIcon(index: Int, icon: Drawable?) {
232
233
  val item = items.getOrNull(index) ?: return
233
234
  if (item.icon == icon) return
234
235
  updateIcon(index, item.copy(icon = icon))
235
236
  }
236
237
 
237
- fun setSelectedIcon(index: Int, icon: Drawable) {
238
+ /** Null: the item shows its icon when selected too. */
239
+ fun setSelectedIcon(index: Int, icon: Drawable?) {
238
240
  val item = items.getOrNull(index) ?: return
239
241
  if (item.selectedIcon == icon) return
240
242
  updateIcon(index, item.copy(selectedIcon = icon))
@@ -0,0 +1,15 @@
1
+ package dev.apexnative.navigation.views.stack
2
+
3
+ import android.graphics.drawable.Drawable
4
+ import android.view.View
5
+
6
+ /**
7
+ * What fills the room beside a moving screen in a transition (`StackLayout.extendEdge`), where the system's
8
+ * transitions between activities stretch a window's edge (`<extend>` of `activity_open_*` and `activity_close_*`): the
9
+ * screen's own background, which is what its edge shows unless its content covers it. Taken without drawing the
10
+ * screen, at the start of every push and pop. Main thread only.
11
+ */
12
+ internal object ScreenEdge {
13
+ /** A copy of [view]'s background, stretched across the room; none without one. */
14
+ fun pictureOf(view: View): Drawable? = view.background?.constantState?.newDrawable(view.resources)?.mutate()
15
+ }
@@ -2,6 +2,8 @@ package dev.apexnative.navigation.views.stack
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
+ import android.graphics.Canvas
6
+ import android.graphics.drawable.Drawable
5
7
  import android.view.Gravity
6
8
  import android.view.View
7
9
  import android.view.ViewGroup
@@ -18,7 +20,8 @@ import java.util.WeakHashMap
18
20
  * moves between its places without leaving it, so it doesn't leave and join the window again; a covered screen that
19
21
  * comes back gets the last window insets the stack got before it joins, and nothing asks the window to send them again
20
22
  * (CoordinatorLayout asks for insets only as it joins the window itself and only with `fitsSystemWindows`, which this
21
- * view doesn't set). Main thread only.
23
+ * view doesn't set). In a transition of the stack's own the bar is drawn with the screen it goes with
24
+ * ([barTravels]). Main thread only.
22
25
  */
23
26
  @SuppressLint("ViewConstructor")
24
27
  class StackLayout(context: Context) : CoordinatorLayout(context) {
@@ -134,4 +137,137 @@ class StackLayout(context: Context) : CoordinatorLayout(context) {
134
137
  requestLayout()
135
138
  invalidate()
136
139
  }
140
+
141
+ // The edges of the screens in a transition: drawn beside each, under it, where it moves and as transparent as it.
142
+ private val edges = HashMap<View, Edge>()
143
+
144
+ private class Edge(val picture: Drawable, val width: Int, val atStart: Boolean)
145
+
146
+ /**
147
+ * [view] extends its start edge ([atStart]) or its end edge by [width] pixels while it moves, as the system's
148
+ * transitions between activities extend a window: its background fills the room it moves away from, so the screen
149
+ * under it never shows at its edge. [dropEdge] ends it. Each frame of the move calls [edgesMoved].
150
+ */
151
+ fun extendEdge(view: View, atStart: Boolean, width: Int) {
152
+ val left = atStart != (layoutDirection == LAYOUT_DIRECTION_RTL)
153
+ val picture = ScreenEdge.pictureOf(view) ?: return
154
+ edges[view] = Edge(picture, width, left)
155
+ invalidate()
156
+ }
157
+
158
+ /** [view] moves no more: its edge goes. */
159
+ fun dropEdge(view: View) {
160
+ if (edges.remove(view) != null) invalidate()
161
+ }
162
+
163
+ /** A screen with an extended edge moved: the edges are drawn again where the screens are now. */
164
+ fun edgesMoved() {
165
+ if (edges.isNotEmpty()) invalidate()
166
+ }
167
+
168
+ // The bar's travel in a transition of the stack's own: the screen the bar goes with, the screen that leaves with the
169
+ // picture of the bar as it was, and whether that one lies over the bar (a pop).
170
+ private var barScreen: View? = null
171
+ private var barLeaving: View? = null
172
+ private var barPicture: Drawable? = null
173
+ private var leavingOver = false
174
+
175
+ /**
176
+ * The bar goes with [screen] in a transition of the stack's own: it is drawn as [screen] moves (its translation,
177
+ * scale and rotation around its pivot, its alpha). [picture], laid where the bar is, goes the same way with
178
+ * [leaving]: it is drawn right over that screen, outside its bounds too (a screen laid out under the bar). With
179
+ * [over] the leaving screen and its picture lie over the bar (a pop), else under it. Only the drawing moves: the
180
+ * bar's views and touches stay in place. [endBarTravel] ends it.
181
+ */
182
+ fun barTravels(screen: View, leaving: View?, picture: Drawable?, over: Boolean) {
183
+ barScreen = screen
184
+ barLeaving = leaving
185
+ barPicture = picture
186
+ leavingOver = over && leaving != null
187
+ invalidate()
188
+ }
189
+
190
+ /** The bar is drawn in its place again, over the screens; the picture goes. */
191
+ fun endBarTravel() {
192
+ if (barScreen == null) return
193
+ barScreen = null
194
+ barLeaving = null
195
+ barPicture = null
196
+ leavingOver = false
197
+ invalidate()
198
+ }
199
+
200
+ override fun dispatchDraw(canvas: Canvas) {
201
+ super.dispatchDraw(canvas)
202
+ // The screens' animators change only their own properties, which don't draw this view again: each frame of
203
+ // the bar's travel asks for the next.
204
+ if (barScreen != null) postInvalidateOnAnimation()
205
+ }
206
+
207
+ override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
208
+ edges[child]?.let { drawEdge(canvas, child, it) }
209
+ val screen = barScreen ?: return super.drawChild(canvas, child, drawingTime)
210
+ // The leaving screen, while it is still here, and its picture; on a pop the bar right under them.
211
+ val leaving = barLeaving?.takeIf(::holds)
212
+ val under = leaving.takeIf { leavingOver }
213
+ if (child === topBar) {
214
+ if (under != null) return false
215
+ return drawWith(canvas, screen, topBar.left.toFloat(), topBar.top + topBar.translationY,
216
+ topBar.right.toFloat(), topBar.bottom + topBar.translationY) {
217
+ super.drawChild(canvas, topBar, drawingTime)
218
+ }
219
+ }
220
+ if (child === under && topBar.visibility == VISIBLE) {
221
+ drawWith(canvas, screen, topBar.left.toFloat(), topBar.top + topBar.translationY,
222
+ topBar.right.toFloat(), topBar.bottom + topBar.translationY) {
223
+ super.drawChild(canvas, topBar, drawingTime)
224
+ }
225
+ }
226
+ val drawn = super.drawChild(canvas, child, drawingTime)
227
+ val picture = barPicture
228
+ if (child === leaving && picture != null) {
229
+ val bounds = picture.bounds
230
+ drawWith(canvas, child, bounds.left.toFloat(), bounds.top.toFloat(), bounds.right.toFloat(),
231
+ bounds.bottom.toFloat()) {
232
+ picture.draw(canvas)
233
+ false
234
+ }
235
+ }
236
+ return drawn
237
+ }
238
+
239
+ // Draws with [screen]'s transform and alpha what [draw] draws within the bounds given, in this view's coordinates.
240
+ private fun drawWith(
241
+ canvas: Canvas,
242
+ screen: View,
243
+ left: Float,
244
+ top: Float,
245
+ right: Float,
246
+ bottom: Float,
247
+ draw: () -> Boolean,
248
+ ): Boolean {
249
+ val alpha = if (screen.visibility == VISIBLE) screen.alpha else 0f
250
+ if (alpha <= 0f) return false
251
+ val saved = canvas.save()
252
+ canvas.translate(screen.left.toFloat(), screen.top.toFloat())
253
+ canvas.concat(screen.matrix)
254
+ canvas.translate(-screen.left.toFloat(), -screen.top.toFloat())
255
+ if (alpha < 1f) canvas.saveLayerAlpha(left, top, right, bottom, (alpha * 255).toInt())
256
+ val drawn = draw()
257
+ canvas.restoreToCount(saved)
258
+ return drawn
259
+ }
260
+
261
+ // At the child's place to the fraction of a pixel, as its own translation draws it, and one pixel under it: no seam
262
+ // of the screen below shows between them.
263
+ private fun drawEdge(canvas: Canvas, child: View, edge: Edge) {
264
+ if (child.visibility != VISIBLE || child.alpha <= 0f) return
265
+ val x = if (edge.atStart) child.left + child.translationX - edge.width else child.right + child.translationX - 1f
266
+ val saved = canvas.save()
267
+ canvas.translate(x, child.top + child.translationY)
268
+ edge.picture.setBounds(0, 0, edge.width + 1, child.height)
269
+ edge.picture.alpha = (child.alpha * 255).toInt()
270
+ edge.picture.draw(canvas)
271
+ canvas.restoreToCount(saved)
272
+ }
137
273
  }
@@ -4,12 +4,11 @@ import android.content.Context
4
4
  import android.content.res.ColorStateList
5
5
  import android.content.res.Configuration
6
6
  import android.graphics.Color
7
- import android.graphics.drawable.Drawable
8
7
  import com.google.android.material.floatingactionbutton.FloatingActionButton
9
8
  import dev.apexnative.navigation.options.params.ThemeColour
10
- import dev.apexnative.navigation.utils.ImageLoader
11
- import dev.apexnative.navigation.utils.ImageLoadingListenerAdapter
9
+ import dev.apexnative.navigation.utils.ImageRequests
12
10
  import dev.apexnative.navigation.utils.MaterialTheme
11
+ import dev.apexnative.navigation.utils.OptionImages
13
12
  import dev.apexnative.navigation.utils.UiUtils
14
13
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ScrollEventListener
15
14
  import androidx.appcompat.R as AppCompatR
@@ -29,6 +28,7 @@ class Fab(context: Context, val fabId: String) : FloatingActionButton(MaterialTh
29
28
  // The icon's color of the options: a container color merged later kept the theme's icon color instead.
30
29
  private var iconColor: Int? = null
31
30
  private var optionRippleColor: Int? = null
31
+ private val icons = ImageRequests<Unit>(OptionImages.source(this.context))
32
32
 
33
33
  init {
34
34
  // Material's theme overlay of the button is a copy of the theme, made once: it follows a light/dark switch too.
@@ -36,14 +36,18 @@ class Fab(context: Context, val fabId: String) : FloatingActionButton(MaterialTh
36
36
  MaterialTheme.followOverlay(this.context)
37
37
  }
38
38
 
39
- fun applyIcon(icon: String, color: ThemeColour) {
39
+ /**
40
+ * The icon of the options, with its color. [mayWait]: the button can show it when it is ready (it shows an icon
41
+ * already, it is hidden with `visible: false`, or it is an action of a closed menu), so an icon the cache doesn't
42
+ * have loads off the main thread; the last icon asked for wins. Otherwise it loads at once. An icon that doesn't
43
+ * load leaves the button without an icon.
44
+ */
45
+ fun applyIcon(icon: String, color: ThemeColour, mayWait: Boolean) {
40
46
  if (color.hasValue()) iconColor = color.get()
41
- ImageLoader().loadIcon(context, icon, object : ImageLoadingListenerAdapter() {
42
- override fun onComplete(drawable: Drawable) {
43
- setImageDrawable(drawable)
44
- iconColor?.let { imageTintList = ColorStateList.valueOf(it) }
45
- }
46
- })
47
+ icons.request(Unit, icon, mayWait) { drawable ->
48
+ setImageDrawable(drawable)
49
+ iconColor?.let { imageTintList = ColorStateList.valueOf(it) }
50
+ }
47
51
  }
48
52
 
49
53
  /** iconColor alone, without a new icon: it did nothing in a merge. */
@@ -118,6 +118,8 @@ class TitleBar(context: Context) : ViewGroup(context) {
118
118
  val height = MeasureSpec.getSize(heightMeasureSpec)
119
119
  val inner = max(width - sideInsetsBox[0] - sideInsetsBox[1], 0)
120
120
  val exactHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
121
+ alignButtons(leftButtonBar, height)
122
+ alignButtons(rightButtonBar, height)
121
123
  leftButtonBar.measure(MeasureSpec.makeMeasureSpec(inner, MeasureSpec.AT_MOST), exactHeight)
122
124
  val rightRoom = max(inner - leftButtonBar.measuredWidth, 0)
123
125
  rightButtonBar.measure(MeasureSpec.makeMeasureSpec(rightRoom, MeasureSpec.AT_MOST), exactHeight)
@@ -192,6 +194,12 @@ class TitleBar(context: Context) : ViewGroup(context) {
192
194
  return min(max(middle, startBar), max(inner - endBar - width, startBar))
193
195
  }
194
196
 
197
+ // A toolbar stands its back button and its menu at the top of its minimum height, centered in it (`buttonGravity`
198
+ // is top): with the row's height as that minimum, they stand in the middle of the row, as the title does.
199
+ private fun alignButtons(toolbar: MaterialToolbar, height: Int) {
200
+ if (toolbar.minimumHeight != height) toolbar.minimumHeight = height
201
+ }
202
+
195
203
  // A toolbar that holds only buttons: no insets of its own around them (the row places them).
196
204
  private fun plain(toolbar: MaterialToolbar) {
197
205
  // Before the toolbar makes its menu, which takes the popup theme once: "⋮" in the current mode.
@@ -200,7 +208,6 @@ class TitleBar(context: Context) : ViewGroup(context) {
200
208
  toolbar.contentInsetStartWithNavigation = 0
201
209
  toolbar.contentInsetEndWithActions = 0
202
210
  toolbar.setPadding(0, 0, 0, 0)
203
- toolbar.minimumHeight = 0
204
211
  }
205
212
 
206
213
  private companion object {
@@ -55,6 +55,8 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
55
55
  private var heightChanged = false
56
56
  // How far the large title is collapsed, in pixels.
57
57
  private var largeTitleShift = 0
58
+ // `topBar.elevation`, in pixels: the bar's elevation unless it is flat.
59
+ private var shadow = 0f
58
60
 
59
61
  /** The toolbar of the right buttons: its menu holds them, the overflow items and the search's magnifier. */
60
62
  val rightButtonBar: MaterialToolbar
@@ -187,14 +189,27 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
187
189
  * The shadow of `topBar.elevation`, in pixels: its colors are black (Android 9 and later read them), none at 0.
188
190
  */
189
191
  fun setShadow(elevation: Float) {
190
- if (this.elevation == elevation) return
191
- this.elevation = elevation
192
+ if (shadow == elevation) return
193
+ shadow = elevation
194
+ if (!isFlat) this.elevation = elevation
192
195
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
193
196
  outlineAmbientShadowColor = Color.BLACK
194
197
  outlineSpotShadowColor = Color.BLACK
195
198
  }
196
199
  }
197
200
 
201
+ /**
202
+ * The bar draws without its shadow, at the level of the screens, in a transition that draws it with a screen: a
203
+ * screen drawn after it covers it, and an alpha layer around it fades it (with an elevation the renderer would draw
204
+ * the bar over every screen and out of the layer).
205
+ */
206
+ var isFlat = false
207
+ set(value) {
208
+ if (field == value) return
209
+ field = value
210
+ elevation = if (value) 0f else shadow
211
+ }
212
+
198
213
  /** The line under the bar: [height] pixels of [color]. */
199
214
  fun setBorder(color: Int, height: Int) {
200
215
  border.setBackgroundColor(color)
@@ -335,8 +350,15 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
335
350
  * A picture of the bar under its row — the background, the large title, the search, the line — and where it starts
336
351
  * in the bar, for a push to a screen without a large title; none when the bar can't be drawn in software.
337
352
  */
338
- fun pictureUnderRow(): Pair<Bitmap, Int>? {
339
- val top = content.top + rowArea.bottom
353
+ fun pictureUnderRow(): Pair<Bitmap, Int>? = pictureFrom(content.top + rowArea.bottom)
354
+
355
+ /**
356
+ * A picture of the whole bar, for the screen it leaves with in a transition of the stack's own; none when the bar
357
+ * can't be drawn in software.
358
+ */
359
+ fun picture(): Bitmap? = pictureFrom(0)?.first
360
+
361
+ private fun pictureFrom(top: Int): Pair<Bitmap, Int>? {
340
362
  val width = width
341
363
  val height = height - top
342
364
  if (width <= 0 || height <= 0) return null
@@ -11,8 +11,10 @@ const {
11
11
  reactNativeVersion,
12
12
  usesExpo,
13
13
  } = require('../../app');
14
+ const { mainActivity } = require('../../links/native');
14
15
  const { findProject } = require('../../project');
15
16
  const { codeOf } = require('../../setup/source');
17
+ const { loadsReactNativeItself } = require('../../setup/steps/mainApplication');
16
18
  const { planCodegen } = require('../../codegen');
17
19
  const { readConfig } = require('../../config');
18
20
  const { planProject } = require('../../setup');
@@ -27,6 +29,16 @@ const { splashChecks } = require('./splash');
27
29
  const SETUP = { label: 'npx apex-navigation setup', run: /** @type {const} */ ('setup') };
28
30
  const GENERATED = ['ApexNavigation.generated.swift', 'ApexNavigationGenerated.kt'];
29
31
  const CODEGEN = { label: 'npx apex-navigation codegen', run: /** @type {const} */ ('codegen') };
32
+ // The changes the main activity must handle itself; the template's value has them all.
33
+ const CONFIG_CHANGES = [
34
+ 'orientation',
35
+ 'screenSize',
36
+ 'smallestScreenSize',
37
+ 'screenLayout',
38
+ 'uiMode',
39
+ ];
40
+ const TEMPLATE_CONFIG_CHANGES =
41
+ 'keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode';
30
42
 
31
43
  /**
32
44
  * React Native, the navigation, and the files setup sets up.
@@ -122,28 +134,64 @@ function appChecks({ root, run }) {
122
134
  });
123
135
  }
124
136
 
137
+ const configChanges = configChangesCheck(project.manifest);
138
+ if (configChanges) checks.push(configChanges);
139
+
125
140
  checks.push(...splashChecks(root, project), ...iconChecks(root, project));
126
141
 
127
- // A second load of React Native crashes the app on start: the most common slip of a setup by hand.
142
+ // NavigationApplication leaves the load of React Native to the template's call: without it the app closes on start,
143
+ // and so it does with a second load of the app's own next to it.
128
144
  const application = project.mainApplication && fs.readFileSync(project.mainApplication, 'utf8');
129
- if (
130
- application &&
131
- /:\s*NavigationApplication\(\)/.test(application) &&
132
- /\bloadReactNative\s*\(/.test(codeOf(application))
133
- ) {
134
- checks.push({
135
- group: 'app',
136
- title: 'React Native loads twice',
137
- status: 'error',
138
- detail: 'MainApplication.kt calls loadReactNative(this), NavigationApplication loads it too',
139
- fix: { label: 'Remove loadReactNative(this) from MainApplication.kt' },
140
- });
145
+ if (application && /:\s*NavigationApplication\(/.test(application)) {
146
+ const loads = /\bloadReactNative\s*\(/.test(codeOf(application));
147
+ if (!loads) {
148
+ checks.push({
149
+ group: 'app',
150
+ title: 'React Native not loaded',
151
+ status: 'error',
152
+ detail: "MainApplication.kt doesn't call loadReactNative(this) in onCreate",
153
+ fix: SETUP,
154
+ });
155
+ } else if (loadsReactNativeItself(application)) {
156
+ checks.push({
157
+ group: 'app',
158
+ title: 'React Native loads twice',
159
+ status: 'error',
160
+ detail:
161
+ 'MainApplication.kt loads React Native itself (SoLoader.init or the New Architecture entry point) next to loadReactNative(this)',
162
+ fix: SETUP,
163
+ });
164
+ }
141
165
  }
142
166
  const generated = generatedCheck(root, run);
143
167
  if (generated) checks.push(generated);
144
168
  return checks;
145
169
  }
146
170
 
171
+ /**
172
+ * Whether the main activity handles the changes of its window itself, as the React Native template has it. Without
173
+ * one of them Android recreates the activity on a rotation, a resize, a fold or a light/dark switch: every screen goes
174
+ * with it and the navigation starts again from its root. Nothing without a manifest or a main activity.
175
+ * @param {string | undefined} manifest
176
+ * @returns {Check | undefined}
177
+ */
178
+ function configChangesCheck(manifest) {
179
+ if (!manifest || !fs.existsSync(manifest)) return undefined;
180
+ const activity = mainActivity(fs.readFileSync(manifest, 'utf8'));
181
+ if (!activity) return undefined;
182
+ const value = /android:configChanges\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(activity.text);
183
+ const declared = (value?.[1] ?? value?.[2] ?? '').split('|').map((change) => change.trim());
184
+ const missing = CONFIG_CHANGES.filter((change) => !declared.includes(change));
185
+ if (missing.length === 0) return undefined;
186
+ return {
187
+ group: 'app',
188
+ title: 'Config changes',
189
+ status: 'warn',
190
+ detail: `the main activity doesn't handle ${missing.join(', ')}: Android recreates it, and every screen with it`,
191
+ fix: { label: `Set android:configChanges="${TEMPLATE_CONFIG_CHANGES}" on the main activity` },
192
+ };
193
+ }
194
+
147
195
  /**
148
196
  * One line for a few files: set up, or which need setup or a change by hand.
149
197
  * @param {string} title
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
+ const { withReactNativeLoaded } = require('../setup/steps/mainApplication');
6
7
 
7
8
  /** @typedef {import('../changes').FileChange} FileChange */
8
9
  /** @typedef {import('./report').ReportItem} ReportItem */
@@ -158,6 +159,23 @@ function planNative(project) {
158
159
  const before = fs.readFileSync(file, 'utf8');
159
160
  let next = before;
160
161
  for (const [pattern, replacement] of rewrite.rules) next = next.replace(pattern, replacement);
162
+ // react-native-navigation loaded React Native itself; the navigation's application leaves it to the template's
163
+ // loadReactNative(this), which such an app doesn't have.
164
+ if (file === project.mainApplication && /:\s*NavigationApplication\(/.test(next)) {
165
+ const loaded = withReactNativeLoaded(next);
166
+ if (loaded !== undefined) next = loaded;
167
+ else {
168
+ report.push({
169
+ file: path.relative(root, file),
170
+ line: 1,
171
+ step: 'native',
172
+ before: 'onCreate',
173
+ after: 'super.onCreate() then loadReactNative(this), and no load of its own',
174
+ what: 'React Native loads through loadReactNative(this) in onCreate: add it by hand',
175
+ docs: `${DOCS}#native`,
176
+ });
177
+ }
178
+ }
161
179
  if (next !== before) {
162
180
  changes.push({
163
181
  name: path.basename(file),
@@ -35,7 +35,8 @@ function removeUnusedImports(text, names) {
35
35
  for (const name of names) {
36
36
  const line = new RegExp(`^import\\s+${escape(name)}[ \\t]*\\r?\\n`, 'm');
37
37
  const simple = name.slice(name.lastIndexOf('.') + 1);
38
- const code = result.replace(/^import\s.*$/gm, '');
38
+ // A name in a comment is no use of the import.
39
+ const code = codeOf(result).replace(/^import\s.*$/gm, '');
39
40
  if (line.test(result) && !new RegExp(`\\b${escape(simple)}\\b`).test(code))
40
41
  result = result.replace(line, '');
41
42
  }
@@ -5,24 +5,51 @@ const { addImport, codeOf, removeUnusedImports } = require('../source');
5
5
 
6
6
  /** @typedef {import('./types').StepResult} StepResult */
7
7
 
8
- const HOW_TO = `Extend NavigationApplication. It loads React Native itself, so loadReactNative(this) goes:
9
- calling it again crashes the app on start.
8
+ const HOW_TO = `Extend NavigationApplication and keep the template's loadReactNative(this) in onCreate: it loads
9
+ React Native as the app's Gradle files say, and the navigation makes its React host after it.
10
10
 
11
+ import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
11
12
  import dev.apexnative.navigation.NavigationApplication
12
13
 
13
14
  class MainApplication : NavigationApplication() {
14
15
  override val reactHost: ReactHost by lazy { ... } // as it is
16
+
17
+ override fun onCreate() {
18
+ super.onCreate()
19
+ loadReactNative(this)
20
+ }
15
21
  }`;
16
22
 
23
+ const LOAD_IMPORT = 'import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative';
24
+
17
25
  // Imports of the template that the change leaves unused.
18
- const TEMPLATE_IMPORTS = [
19
- 'android.app.Application',
20
- 'com.facebook.react.ReactApplication',
21
- 'com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative',
26
+ const TEMPLATE_IMPORTS = ['android.app.Application', 'com.facebook.react.ReactApplication'];
27
+
28
+ // A load of React Native of the app's own, next to loadReactNative(this) a second one that crashes the app at start:
29
+ // the New Architecture's entry point (react-native-navigation's apps call it in onCreate) and SoLoader.
30
+ // A bare `load()` is the entry point's only where the file imports it. A form these don't take out (`load` with
31
+ // arguments, an `if` without braces) is left to the app's author.
32
+ const OWN_LOAD = [
33
+ /^[ \t]*if\s*\(\s*BuildConfig\.IS_NEW_ARCHITECTURE_ENABLED\s*\)\s*\{(?:\s*\/\/[^\n]*)*\s*DefaultNewArchitectureEntryPoint\.load\(\)\s*\}[ \t]*\r?\n/gm,
34
+ /^[ \t]*DefaultNewArchitectureEntryPoint\.load\(\)[ \t]*\r?\n/gm,
35
+ /^[ \t]*SoLoader\.init\([^\n]*\)[ \t]*\r?\n/gm,
36
+ ];
37
+ const BARE_LOAD = [
38
+ /^[ \t]*if\s*\(\s*BuildConfig\.IS_NEW_ARCHITECTURE_ENABLED\s*\)\s*\{(?:\s*\/\/[^\n]*)*\s*load\(\)\s*\}[ \t]*\r?\n/gm,
39
+ /^[ \t]*load\(\)[ \t]*\r?\n/gm,
40
+ ];
41
+ const BARE_LOAD_IMPORT =
42
+ /^import\s+com\.facebook\.react\.defaults\.DefaultNewArchitectureEntryPoint\.load\b/m;
43
+ const OWN_LOAD_IMPORTS = [
44
+ 'com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load',
45
+ 'com.facebook.react.defaults.DefaultNewArchitectureEntryPoint',
46
+ 'com.facebook.soloader.SoLoader',
47
+ 'com.facebook.react.soloader.OpenSourceMergedSoMapping',
22
48
  ];
23
49
 
24
50
  /**
25
- * MainApplication.kt: the application becomes a NavigationApplication.
51
+ * MainApplication.kt: the application becomes a NavigationApplication, and loads React Native in its onCreate as the
52
+ * template does.
26
53
  * @param {string} text
27
54
  * @returns {StepResult}
28
55
  */
@@ -32,21 +59,20 @@ function planMainApplication(text) {
32
59
  note: 'changed by hand, see below',
33
60
  howTo: HOW_TO,
34
61
  });
35
- if (/:\s*NavigationApplication\(\)/.test(text)) {
36
- return /\bloadReactNative\s*\(/.test(codeOf(text))
62
+ if (/:\s*NavigationApplication\(/.test(text)) {
63
+ if (loadsReactNative(text) && !loadsReactNativeItself(text)) {
64
+ return { status: 'done', note: 'already set up' };
65
+ }
66
+ const next = withReactNativeLoaded(text);
67
+ return next === undefined
37
68
  ? manual
38
- : { status: 'done', note: 'already set up' };
69
+ : { status: 'change', next, note: 'loadReactNative(this) in onCreate' };
39
70
  }
40
71
  const header = /class\s+(\w+)\s*:\s*Application\(\)\s*,\s*ReactApplication\b/;
41
72
  if (!header.test(text)) return manual;
42
- let next = text.replace(header, 'class $1 : NavigationApplication()');
43
- // The template's onCreate only loads React Native, so it goes; another one keeps the rest of its code.
44
- next = next.replace(
45
- /\n\n?[ \t]*override\s+fun\s+onCreate\(\)\s*\{\s*super\.onCreate\(\)\s*loadReactNative\(this\)\s*\}/,
46
- ''
47
- );
48
- next = next.replace(/^[ \t]*loadReactNative\(this\)[ \t]*\r?\n/m, '');
49
- if (/\bloadReactNative\s*\(/.test(codeOf(next))) return manual;
73
+ // The template's onCreate stays: its loadReactNative(this) loads React Native for the navigation too.
74
+ let next = withReactNativeLoaded(text.replace(header, 'class $1 : NavigationApplication()'));
75
+ if (next === undefined) return manual;
50
76
  // Added first: with the template's imports gone the file would have none to sort it among.
51
77
  next = addImport(next, 'import dev.apexnative.navigation.NavigationApplication', {
52
78
  sorted: true,
@@ -55,4 +81,93 @@ function planMainApplication(text) {
55
81
  return { status: 'change', next, note: 'NavigationApplication' };
56
82
  }
57
83
 
58
- module.exports = { planMainApplication };
84
+ /**
85
+ * Whether the code of the file, not a comment, calls loadReactNative.
86
+ * @param {string} text
87
+ */
88
+ function loadsReactNative(text) {
89
+ return /\bloadReactNative\s*\(/.test(codeOf(text));
90
+ }
91
+
92
+ /**
93
+ * Whether the code of the file loads React Native on its own as well: the New Architecture's entry point or SoLoader.
94
+ * @param {string} text
95
+ */
96
+ function loadsReactNativeItself(text) {
97
+ const code = codeOf(text);
98
+ return (
99
+ /\bSoLoader\.init\s*\(/.test(code) ||
100
+ /\bDefaultNewArchitectureEntryPoint\.load\s*\(/.test(code) ||
101
+ (BARE_LOAD_IMPORT.test(text) && /(?<![\w.])load\s*\(/.test(code))
102
+ );
103
+ }
104
+
105
+ /**
106
+ * The application with `loadReactNative(this)` in its onCreate, after `super.onCreate()`: as it is when it has the
107
+ * call, with an onCreate of the template's when it has none. Undefined for an onCreate that doesn't call
108
+ * `super.onCreate()`, whose order only its author knows.
109
+ * @param {string} text
110
+ * @returns {string | undefined}
111
+ */
112
+ function withReactNativeLoaded(text) {
113
+ let without = text;
114
+ const patterns = BARE_LOAD_IMPORT.test(text) ? [...OWN_LOAD, ...BARE_LOAD] : OWN_LOAD;
115
+ for (const pattern of patterns) without = without.replace(pattern, '');
116
+ without = removeUnusedImports(without, OWN_LOAD_IMPORTS);
117
+ if (loadsReactNativeItself(without)) return undefined;
118
+ if (loadsReactNative(without)) return without;
119
+ return withLoadCall(without);
120
+ }
121
+
122
+ /**
123
+ * @param {string} text an application without a load of React Native
124
+ * @returns {string | undefined}
125
+ */
126
+ function withLoadCall(text) {
127
+ let next;
128
+ const superCall = /^([ \t]*)super\.onCreate\(\)[ \t]*\r?\n/m;
129
+ if (/\boverride\s+fun\s+onCreate\s*\(/.test(text)) {
130
+ if (!superCall.test(text)) return undefined;
131
+ next = text.replace(superCall, (line, indent) => `${line}${indent}loadReactNative(this)\n`);
132
+ } else {
133
+ const close = classEnd(text);
134
+ if (close === undefined) return undefined;
135
+ const indent = /^([ \t]+)override\b/m.exec(text)?.[1] ?? ' ';
136
+ const body = text.slice(0, close).replace(/\s*$/, '');
137
+ const onCreate = [
138
+ '',
139
+ `${indent}override fun onCreate() {`,
140
+ `${indent}${indent}super.onCreate()`,
141
+ `${indent}${indent}loadReactNative(this)`,
142
+ `${indent}}`,
143
+ ].join('\n');
144
+ next = `${body}\n${onCreate}\n${text.slice(close)}`;
145
+ }
146
+ return addImport(next, LOAD_IMPORT, { sorted: true });
147
+ }
148
+
149
+ /**
150
+ * The index of the closing brace of the file's application class: its braces counted from the class's own, past any
151
+ * declaration or comment after it. Undefined when the braces don't close.
152
+ * @param {string} text
153
+ * @returns {number | undefined}
154
+ */
155
+ function classEnd(text) {
156
+ // The comments and the string and character literals blanked out, so that an index in it is one in the text and a
157
+ // brace in a literal doesn't count.
158
+ const blank = (/** @type {string} */ part) => part.replace(/[^\n]/g, ' ');
159
+ const code = text
160
+ .replace(/\/\*[\s\S]*?\*\//g, blank)
161
+ .replace(/\/\/.*$/gm, blank)
162
+ .replace(/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g, blank);
163
+ const header = /\bclass\s+\w+\s*:[^{]*\{/.exec(code);
164
+ if (!header) return undefined;
165
+ let depth = 0;
166
+ for (let index = header.index + header[0].length - 1; index < code.length; index++) {
167
+ if (code[index] === '{') depth++;
168
+ else if (code[index] === '}' && --depth === 0) return index;
169
+ }
170
+ return undefined;
171
+ }
172
+
173
+ module.exports = { planMainApplication, withReactNativeLoaded, loadsReactNativeItself };