@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
@@ -39,10 +39,10 @@ internal class CommandAnswer(
39
39
  fun resolve(value: Any?) {
40
40
  if (!canAnswer) return
41
41
  isAnswered = true
42
- if (commandName in STATE_COMMANDS) guarded("report the navigation state") { events.beforeAnswer() }
42
+ if (commandName in CommandContract.STATE) guarded("report the navigation state") { events.beforeAnswer() }
43
43
  promise.resolve(value)
44
44
  val commandId = commandId
45
- if (commandName in COMPLETED_COMMANDS && commandId != null) {
45
+ if (commandName in CommandContract.COMPLETED && commandId != null) {
46
46
  guarded("send ANN.CommandCompleted") { events.commandCompleted(commandName, commandId, clock()) }
47
47
  }
48
48
  }
@@ -65,28 +65,6 @@ internal class CommandAnswer(
65
65
  log("[ApexNavigation] $commandName: ${ErrorTexts.couldNot(what, ErrorTexts.reasonOf(e))}")
66
66
  }
67
67
  }
68
-
69
- companion object {
70
- // The commands that send `ANN.CommandCompleted` (EVT-CommandCompleted).
71
- val COMPLETED_COMMANDS = setOf(
72
- "setRoot",
73
- "push",
74
- "pop",
75
- "popTo",
76
- "popToRoot",
77
- "setStackRoot",
78
- "showInColumn",
79
- "showModal",
80
- "dismissModal",
81
- "dismissAllModals",
82
- "showOverlay",
83
- "dismissOverlay",
84
- "dismissAllOverlays",
85
- )
86
-
87
- // The commands whose answer comes after the navigation state: those and `setColumnShown`.
88
- val STATE_COMMANDS = COMPLETED_COMMANDS + "setColumnShown"
89
- }
90
68
  }
91
69
 
92
70
  /** What goes to JavaScript around the answer of a command. Main thread only. */
@@ -0,0 +1,59 @@
1
+ // Made by scripts/contract.mjs from src/interfaces/contract/commands.json: change that file and run the script again.
2
+
3
+ package dev.apexnative.navigation.react
4
+
5
+ /**
6
+ * What the commands with a promise answer, the same table as iOS. What each resolves with:
7
+ * - `setRoot`: the id of the root, once the root and the layout's modals and overlays show.
8
+ * - `push`: the id of the screen that shows; of a pair, the pair's id.
9
+ * - `preparePush`: the frame `{ width, height, padding }` of the prepared screen, or null; always null on iOS.
10
+ * - `pop`: the id of the screen that went.
11
+ * - `popTo`: the id of the screen it went back to.
12
+ * - `popToRoot`: the id of the first screen of the stack.
13
+ * - `setStackRoot`: the id of the new top screen.
14
+ * - `showModal`: the id of the modal's root.
15
+ * - `dismissModal`: the id of the modal's root.
16
+ * - `dismissAllModals`: an empty string.
17
+ * - `showOverlay`: the id of the overlay's root.
18
+ * - `dismissOverlay`: the id of the overlay's root.
19
+ * - `dismissAllOverlays`: an empty string.
20
+ * - `showInColumn`: the id of the screen that shows.
21
+ * - `setColumnShown`: nothing once the columns stopped moving (`undefined` on iOS, `null` on Android).
22
+ * - `getLaunchLink`: the link that launched the app, or null.
23
+ */
24
+ internal object CommandContract {
25
+ /** The commands whose answer comes after the navigation state: every one that may change what shows. */
26
+ val STATE: Set<String> = setOf(
27
+ "setRoot",
28
+ "push",
29
+ "pop",
30
+ "popTo",
31
+ "popToRoot",
32
+ "setStackRoot",
33
+ "showModal",
34
+ "dismissModal",
35
+ "dismissAllModals",
36
+ "showOverlay",
37
+ "dismissOverlay",
38
+ "dismissAllOverlays",
39
+ "showInColumn",
40
+ "setColumnShown",
41
+ )
42
+
43
+ /** The commands that send `ANN.CommandCompleted` after their answer: every one that shows or dismisses. */
44
+ val COMPLETED: Set<String> = setOf(
45
+ "setRoot",
46
+ "push",
47
+ "pop",
48
+ "popTo",
49
+ "popToRoot",
50
+ "setStackRoot",
51
+ "showModal",
52
+ "dismissModal",
53
+ "dismissAllModals",
54
+ "showOverlay",
55
+ "dismissOverlay",
56
+ "dismissAllOverlays",
57
+ "showInColumn",
58
+ )
59
+ }
@@ -19,13 +19,16 @@ import java.lang.ref.WeakReference
19
19
 
20
20
  /**
21
21
  * The device's hinge (`useHinge`): its state from the fold in the activity's window, its angle from the hinge angle
22
- * sensor (Android 11+). Android has no public state of a closed device: closed is a window without a fold and an
23
- * angle under 90°. No sensor and no fold: no hinge, `{}`. It runs while JavaScript has a hook mounted
24
- * (`setHingeUpdates`), the sensor only while the activity is resumed too; the same value is not sent twice. Main
25
- * thread only, but [get].
22
+ * sensor (Android 11+), in degrees to a tenth as on iOS. Android has no public state of a closed device: closed is a
23
+ * window without a fold, or an angle under 90° ([valueOf]). No sensor and no fold: no hinge, `{}`. It runs while
24
+ * JavaScript has a hook mounted (`setHingeUpdates`), the sensor only while the activity is resumed too; the same value
25
+ * is not sent twice. Main thread only, but [get].
26
26
  */
27
27
  internal object Hinge {
28
- data class Value(val state: String, val angle: Float?)
28
+ /** A value of the hinge: its state, and its angle in degrees to a tenth; none while the state is unknown. */
29
+ data class Value(val state: String, val angle: Double?)
30
+
31
+ private const val UNKNOWN = "unknown"
29
32
 
30
33
  private const val CLOSED_BELOW = 90f
31
34
 
@@ -128,28 +131,33 @@ internal object Hinge {
128
131
  if (!enabled) return
129
132
  val folds = folds ?: return
130
133
  if (!folds.hasInfo || (listening && angle == null)) return
131
- val fold = folds.folds.firstOrNull()
132
- val angle = angle
133
- val value = if (fold == null && sensor == null) {
134
- null
135
- } else {
136
- val state = when {
137
- fold?.halfOpened == true -> "partiallyOpen"
138
- fold != null -> "fullyOpen"
139
- angle != null && angle < CLOSED_BELOW -> "closed"
140
- else -> "unknown"
141
- }
142
- Value(state, angle)
143
- }
134
+ val value = valueOf(folds.folds.firstOrNull(), sensor != null, angle)
144
135
  if (value == last) return
145
136
  last = value
146
137
  val reactContext = (activity?.get()?.application as? ReactApplication)?.reactHost?.currentReactContext ?: return
147
138
  EventEmitter(reactContext).emitHingeChanged(toMap(value))
148
139
  }
149
140
 
141
+ /**
142
+ * The value of the window's first [fold] and the sensor's [angle]: none without a fold and without a sensor
143
+ * ([hasSensor]). `closed` without a fold or under 90°, else `partiallyOpen` for a half-opened fold and `fullyOpen`
144
+ * for a flat one. The angle goes to a tenth, as iOS gives it: the sensor's noise doesn't make a new value, and it
145
+ * is left out while the state is unknown.
146
+ */
147
+ fun valueOf(fold: WindowFolds.Fold?, hasSensor: Boolean, angle: Float?): Value? {
148
+ if (fold == null && !hasSensor) return null
149
+ val state = when {
150
+ fold == null || (angle != null && angle < CLOSED_BELOW) -> "closed"
151
+ fold.halfOpened -> "partiallyOpen"
152
+ else -> "fullyOpen"
153
+ }
154
+ val tenths = angle?.let { Math.round(it * 10.0) / 10.0 }
155
+ return Value(state, tenths.takeIf { state != UNKNOWN })
156
+ }
157
+
150
158
  private fun toMap(value: Value?): WritableMap = Arguments.createMap().apply {
151
159
  if (value == null) return@apply
152
160
  putString(EventContract.HingeChanged.STATE, value.state)
153
- value.angle?.let { putDouble(EventContract.HingeChanged.ANGLE, it.toDouble()) }
161
+ if (value.state != UNKNOWN) value.angle?.let { putDouble(EventContract.HingeChanged.ANGLE, it) }
154
162
  }
155
163
  }
@@ -211,7 +211,7 @@ class NavigationTurboModule(reactContext: ReactApplicationContext) : NativeANNTu
211
211
  layout: ReadableMap,
212
212
  promise: Promise,
213
213
  ) {
214
- // Android has one column to show screens in: the secondary pane (`showInColumn`).
214
+ // Android's split view has the primary and the secondary pane; another column rejects (`showInColumn`).
215
215
  val what = "show the screen '${LayoutReader.idOf(layout)}' in the '$column' column"
216
216
  runner.run(
217
217
  name = "showInColumn",
@@ -219,7 +219,7 @@ class NavigationTurboModule(reactContext: ReactApplicationContext) : NativeANNTu
219
219
  what = what,
220
220
  promise = promise,
221
221
  check = { LayoutReader.node(layout, context = appContext()) },
222
- ) { node, answer -> navigator().stackCommands.showInColumn(componentId, node, answer, what) }
222
+ ) { node, answer -> navigator().stackCommands.showInColumn(componentId, column, node, answer, what) }
223
223
  }
224
224
 
225
225
  override fun setColumnShown(
@@ -295,9 +295,10 @@ class NavigationTurboModule(reactContext: ReactApplicationContext) : NativeANNTu
295
295
  })
296
296
  }
297
297
 
298
- // The navigation of the activity the command runs in; the runner checked there is one.
298
+ // The navigation of the activity the command runs in; the runner checked there is an activity. One without its
299
+ // navigation yet (or any more) has nowhere to show screens either: the command rejects with the runner's own text.
299
300
  private fun navigator(): Navigator =
300
- requireNotNull(Navigators.current()?.navigator) { "the activity has no navigation" }
301
+ Navigators.current()?.navigator ?: throw LayoutException(ErrorTexts.nowhereToShow())
301
302
 
302
303
  // The app's context, which the colors of the options resolve platform colors with.
303
304
  private fun appContext(): Context = reactApplicationContext.applicationContext
@@ -0,0 +1,48 @@
1
+ package dev.apexnative.navigation.react
2
+
3
+ import dev.apexnative.navigation.NavigationApplication
4
+ import dev.apexnative.navigation.utils.UiThread
5
+
6
+ /**
7
+ * The calls JavaScript made to the module that may not have reached the main thread yet: [after] runs a task on the
8
+ * main thread behind every one of them. A screen's first mount reaches the main thread from Fabric while a
9
+ * `mergeOptions` of the same React commit (its layout effects: `useScreenOptions`) may still be on its way: an
10
+ * asynchronous method goes from the JavaScript thread to the module's thread (`BridgelessNativeMethodCallInvoker`),
11
+ * which reads its options there and posts its body to the main thread ([CommandRunner]). The task goes the same way: a
12
+ * turn of the JavaScript thread, which comes after the task of the commit with its layout effects; then the module's
13
+ * thread, behind the calls that task made; then the main thread, behind their bodies. Each of the three runs its queue
14
+ * in order, so every call JavaScript made before the mount has applied when the task runs.
15
+ *
16
+ * Without JavaScript (none yet, a reload, a queue that ended) the task is posted to the main thread at once: nothing is
17
+ * on its way. A queue that ends while the task is on it drops it, and a wait with no limit of its own (a push with
18
+ * `waitForRender: true` whose content has mounted: the task is its only end) would not end; a queue ends only as the
19
+ * JavaScript goes (a reload, the host destroyed), and then the layouts go with it (`ReloadCoordinator`), the stack
20
+ * and its waiting push too. Main thread.
21
+ */
22
+ internal object PendingCalls {
23
+ /** A queue the task goes through: it runs what it takes in order, and says false when it ended and took nothing. */
24
+ fun interface Queue {
25
+ fun run(task: Runnable): Boolean
26
+ }
27
+
28
+ /** Runs [task] on the main thread, on a later turn, after every call JavaScript made so far has applied there. */
29
+ fun after(task: () -> Unit) {
30
+ val context = NavigationApplication.instanceOrNull?.reactGatewayOrNull?.currentReactContext
31
+ ?.takeIf { it.hasActiveReactInstance() }
32
+ val js = context?.jsMessageQueueThread?.let { Queue(it::runOnQueue) }
33
+ val module = context?.nativeModulesMessageQueueThread?.let { Queue(it::runOnQueue) }
34
+ after(js, module, UiThread::post, task)
35
+ }
36
+
37
+ /**
38
+ * The way of [after] over the queues given: [js], then [module], then [main]; straight to [main] without one of
39
+ * them, or from where a queue refused it. The unit tests run it over their own queues.
40
+ */
41
+ fun after(js: Queue?, module: Queue?, main: (Runnable) -> Unit, task: () -> Unit) {
42
+ val onMain = Runnable(task)
43
+ if (js == null || module == null) return main(onMain)
44
+ val toMain = Runnable { main(onMain) }
45
+ val toModule = Runnable { if (!module.run(toMain)) main(onMain) }
46
+ if (!js.run(toModule)) main(onMain)
47
+ }
48
+ }
@@ -391,12 +391,14 @@ internal object AppearanceDispatcher {
391
391
 
392
392
  /**
393
393
  * [modal] went with [count] modals (EVT-ModalDismissed): `ANN.ModalDismissed` with the id and the name of the
394
- * screen the modal shows, down its current children.
394
+ * screen the modal shows, down its current children ([modalScreen]); a modal that shows no screen gives its own id
395
+ * and no name.
395
396
  */
396
397
  fun modalDismissed(modal: ViewController<*>, count: Int) {
397
398
  if (!ReloadCoordinator.isCurrent(modal.jsGeneration)) return
398
- val screen = visibleNode(modal)
399
- sender.modalDismissed(screen.id, screen.currentComponentName.orEmpty(), count)
399
+ val named = modalScreen(modal)
400
+ val name = if (named is ParentController<*>) "" else named.currentComponentName.orEmpty()
401
+ sender.modalDismissed(named.id, name, count)
400
402
  }
401
403
 
402
404
  /**
@@ -449,7 +451,7 @@ internal object AppearanceDispatcher {
449
451
  * modal shows. Nothing for a JavaScript that went.
450
452
  */
451
453
  fun modalAttemptedToDismiss(modal: ViewController<*>) {
452
- val screen = visibleNode(modal)
454
+ val screen = modalScreen(modal)
453
455
  if (ReloadCoordinator.isCurrent(screen.jsGeneration)) sender.modalAttemptedToDismiss(screen.id)
454
456
  }
455
457
 
@@ -458,18 +460,26 @@ internal object AppearanceDispatcher {
458
460
  * Nothing for a JavaScript that went.
459
461
  */
460
462
  fun sheetDetentChanged(modal: ViewController<*>, detent: Any) {
461
- val screen = visibleNode(modal)
463
+ val screen = modalScreen(modal)
462
464
  if (ReloadCoordinator.isCurrent(screen.jsGeneration)) sender.sheetDetentChanged(screen.id, detent)
463
465
  }
464
466
 
465
467
  /**
466
468
  * The screen [node] shows: the node at the end of its current children (the one "visible screen" of the core): what
467
- * `ANN.ModalDismissed`, `ANN.ModalAttemptedToDismiss`, `ANN.SheetDetentChanged` and back's
468
- * `ANN.NavigationButtonPressed` of a modal name. A layout without a child answers itself.
469
+ * back's `ANN.NavigationButtonPressed` of a modal names, and, through [modalScreen], its `ANN.ModalDismissed`,
470
+ * `ANN.ModalAttemptedToDismiss` and `ANN.SheetDetentChanged`. A layout without a child answers itself.
469
471
  */
470
472
  fun visibleNode(node: ViewController<*>): ViewController<*> {
471
473
  var current = node
472
474
  while (current is ParentController<*>) current = current.currentChild ?: break
473
475
  return current
474
476
  }
477
+
478
+ /**
479
+ * The node the events of the modal [modal] (its root) name: its [visibleNode] when that is a screen; the root
480
+ * itself when the chain ends in a layout without a child, as on iOS. Asked again of what it gave, it gives the
481
+ * same.
482
+ */
483
+ fun modalScreen(modal: ViewController<*>): ViewController<*> =
484
+ visibleNode(modal).takeUnless { it is ParentController<*> } ?: modal
475
485
  }
@@ -9,14 +9,15 @@ interface SlotContainer {
9
9
  }
10
10
 
11
11
  /**
12
- * The native containers the slots of the app's React tree show their children in, by slot id. A destroyed container
13
- * drops out on its own.
12
+ * The native containers the slots of the app's React tree show their children in, by slot id, and the slot views by
13
+ * the same id: a change of a container goes straight to the views of its id. A destroyed container and a collected
14
+ * view drop out on their own.
14
15
  */
15
16
  object SlotContainers {
16
17
  private class Entry(val view: WeakReference<ViewGroup>, val passThrough: Boolean)
17
18
 
18
19
  private val containers = HashMap<String, Entry>()
19
- private val slots = LinkedHashSet<SlotView>()
20
+ private val slots = SlotViews<SlotView>()
20
21
 
21
22
  /** `passThrough`: touches outside the slot's content go to the screens below, as in an overlay for a toast. */
22
23
  fun put(slotId: String, container: ViewGroup, passThrough: Boolean = false) {
@@ -33,12 +34,14 @@ object SlotContainers {
33
34
  changed(slotId)
34
35
  }
35
36
 
36
- /** The container of a slot appeared or changed its size. */
37
+ /** The container of a slot appeared or changed its size: the slot views under its id, and only they, hear it. */
37
38
  fun changed(slotId: String) {
38
- slots.toList().forEach { it.containerChanged(slotId) }
39
+ slots.views(slotId).forEach { it.containerChanged(slotId) }
39
40
  }
40
41
 
41
- internal fun listen(slot: SlotView) = slots.add(slot)
42
+ /** The slot view goes under its slot id, and out from under the id it had. Main thread only. */
43
+ internal fun register(slotId: String, slot: SlotView) = slots.register(slotId, slot)
42
44
 
43
- internal fun stopListening(slot: SlotView) = slots.remove(slot)
45
+ /** The slot view unmounted or lost its slot id: no container change reaches it. Main thread only. */
46
+ internal fun unregister(slot: SlotView) = slots.unregister(slot)
44
47
  }
@@ -86,7 +86,6 @@ class SlotView(reactContext: ThemedReactContext) : ReactViewGroup(reactContext)
86
86
  init {
87
87
  content.visibility = INVISIBLE
88
88
  addView(content)
89
- SlotContainers.listen(this)
90
89
  }
91
90
 
92
91
  override fun setId(id: Int) {
@@ -100,7 +99,9 @@ class SlotView(reactContext: ThemedReactContext) : ReactViewGroup(reactContext)
100
99
  }
101
100
 
102
101
  fun attachContent() {
103
- val id = slotId ?: return
102
+ // The view hears the changes of the container under the id it has now, and no other.
103
+ val id = slotId ?: return SlotContainers.unregister(this)
104
+ SlotContainers.register(id, this)
104
105
  val container = SlotContainers.get(id)
105
106
  val target: ViewGroup = container ?: this
106
107
  if (content.parent !== target) {
@@ -135,7 +136,7 @@ class SlotView(reactContext: ThemedReactContext) : ReactViewGroup(reactContext)
135
136
  }
136
137
 
137
138
  fun drop() {
138
- SlotContainers.stopListening(this)
139
+ SlotContainers.unregister(this)
139
140
  (content.parent as? ViewGroup)?.removeView(content)
140
141
  }
141
142
  }
@@ -0,0 +1,40 @@
1
+ package dev.apexnative.navigation.slots
2
+
3
+ import java.lang.ref.WeakReference
4
+ import java.util.WeakHashMap
5
+
6
+ /**
7
+ * The slot views by their slot id, held weakly, so a change of a container reaches only the views of its id. A view is
8
+ * under one id at a time: registering it under a new one (its `nativeID` changed, or Fabric reused it) takes it out
9
+ * from under the old one. Two live views under one id both stay, in the order they came, and both hear the change, as
10
+ * every view with that id did when the change went to all of them. Main thread only.
11
+ */
12
+ internal class SlotViews<T : Any> {
13
+ private val byId = HashMap<String, MutableList<WeakReference<T>>>()
14
+ private val idOf = WeakHashMap<T, String>()
15
+
16
+ fun register(slotId: String, view: T) {
17
+ if (idOf[view] == slotId) return
18
+ unregister(view)
19
+ idOf[view] = slotId
20
+ byId.getOrPut(slotId) { ArrayList(1) }.add(WeakReference(view))
21
+ }
22
+
23
+ fun unregister(view: T) {
24
+ val slotId = idOf.remove(view) ?: return
25
+ prune(slotId) { it === view }
26
+ }
27
+
28
+ /** The live views under [slotId], a copy: a view may register or leave while it hears the change. */
29
+ fun views(slotId: String): List<T> {
30
+ prune(slotId) { false }
31
+ return byId[slotId]?.mapNotNull { it.get() }.orEmpty()
32
+ }
33
+
34
+ // Drops the collected views under the id and the ones `drop` picks; an id with no views left goes.
35
+ private fun prune(slotId: String, drop: (T) -> Boolean) {
36
+ val views = byId[slotId] ?: return
37
+ views.removeAll { reference -> reference.get()?.let(drop) ?: true }
38
+ if (views.isEmpty()) byId.remove(slotId)
39
+ }
40
+ }
@@ -0,0 +1,66 @@
1
+ package dev.apexnative.navigation.utils
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import android.view.View
6
+ import java.io.File
7
+ import java.net.URI
8
+ import java.net.URISyntaxException
9
+
10
+ /**
11
+ * What an image of the options is cached by: its [uri] and what changes the decoded result for the same uri, the
12
+ * screen's density (a drawable of the app picks its density's file, a decoded file is scaled to it), light or dark
13
+ * (`drawable-night`) and the layout direction (`drawable-ldrtl`, auto-mirrored drawables). For a `file:` uri also the
14
+ * file's [modified] time and [size]: a file the app writes again at the same path is another image. The loader bakes
15
+ * no tint and no size into an image, so neither is part of it.
16
+ */
17
+ internal data class ImageKey(
18
+ val uri: String,
19
+ val densityDpi: Int,
20
+ val night: Boolean,
21
+ val rightToLeft: Boolean,
22
+ /** When the file of a `file:` uri last changed, in milliseconds; 0 for any other uri or a file that isn't there. */
23
+ val modified: Long = 0,
24
+ /** The bytes of the file of a `file:` uri; 0 for any other uri or a file that isn't there. */
25
+ val size: Long = 0,
26
+ ) {
27
+ companion object {
28
+ private const val FILE_SCHEME = "file:"
29
+ private const val FILE_PREFIX = "file://"
30
+
31
+ /** The key of [uri] in the current configuration of [context]. Any thread. */
32
+ fun of(context: Context, uri: String): ImageKey {
33
+ val configuration = context.resources.configuration
34
+ return of(uri, configuration.densityDpi, configuration.uiMode, configuration.layoutDirection)
35
+ }
36
+
37
+ /**
38
+ * The key of [uri] for a configuration's density, `uiMode` (only its night bits count) and layout direction;
39
+ * a `file:` uri reads its file's modification time and size. Any thread.
40
+ */
41
+ fun of(uri: String, densityDpi: Int, uiMode: Int, layoutDirection: Int): ImageKey {
42
+ val file = filePath(uri)?.let(::File)
43
+ return ImageKey(
44
+ uri = uri,
45
+ densityDpi = densityDpi,
46
+ night = uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES,
47
+ rightToLeft = layoutDirection == View.LAYOUT_DIRECTION_RTL,
48
+ // Both 0 when the file isn't there or can't be read.
49
+ modified = file?.lastModified() ?: 0,
50
+ size = file?.length() ?: 0,
51
+ )
52
+ }
53
+
54
+ // The path of a `file:` uri, decoded as the loader's `Uri.getPath` decodes it; none for any other uri. A uri
55
+ // that `URI` can't parse (an unescaped space) is taken as it is after its scheme.
56
+ private fun filePath(uri: String): String? {
57
+ if (!uri.startsWith(FILE_SCHEME)) return null
58
+ val path = try {
59
+ URI(uri).path
60
+ } catch (e: URISyntaxException) {
61
+ null
62
+ }
63
+ return path ?: uri.removePrefix(FILE_PREFIX).removePrefix(FILE_SCHEME).ifEmpty { null }
64
+ }
65
+ }
66
+ }
@@ -1,19 +1,8 @@
1
1
  package dev.apexnative.navigation.utils
2
2
 
3
3
  import android.content.Context
4
- import android.content.res.Resources
5
- import android.graphics.BitmapFactory
6
- import android.graphics.drawable.BitmapDrawable
7
4
  import android.graphics.drawable.Drawable
8
- import android.net.Uri
9
- import android.os.StrictMode
10
- import androidx.core.content.ContextCompat
11
- import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
12
- import java.io.FileInputStream
13
- import java.io.FileNotFoundException
14
5
  import java.io.IOException
15
- import java.io.InputStream
16
- import java.net.URL
17
6
 
18
7
  /** What an image loads into: [onComplete] with the image, or [onError] when it can't load. Main thread. */
19
8
  open class ImageLoadingListenerAdapter {
@@ -23,90 +12,19 @@ open class ImageLoadingListenerAdapter {
23
12
  }
24
13
 
25
14
  /**
26
- * Loads the images and icons of the options when they apply: synchronously on the main thread, without a cache, each
27
- * image its own mutable copy. A `file:` address is a file; anything else first a drawable of the app by name (as React
28
- * Native's images by resource name: release builds' assets, `{ android }` icons). Not found while the app is in
29
- * development: the address is read as a stream, Metro's asset over the network (network on the main thread allowed for
30
- * that read only) or through the content resolver. A remote address that is not Metro's asset is never loaded. A
31
- * failure never ends the app: the listener's [ImageLoadingListenerAdapter.onError], the log, and React Native's red
32
- * screen in development.
15
+ * Loads an image or an icon of the options at once, through the shared cache of [OptionImages]: each image its own
16
+ * mutable copy. For the places whose first frame needs the image (the top bar's buttons, the back button, menus, search
17
+ * suggestions); a place that can show it when ready asks through [ImageRequests]. A failure never ends the app: the
18
+ * listener's [ImageLoadingListenerAdapter.onError], and one warning per address.
33
19
  */
34
20
  class ImageLoader {
35
21
  /** Loads [uri] and gives it to [listener] before returning. Main thread. */
36
22
  fun loadIcon(context: Context, uri: String, listener: ImageLoadingListenerAdapter) {
37
- val drawable = try {
38
- load(context, uri)
39
- } catch (e: Exception) {
40
- val reason = if (e is LoadFailure) "" else ": ${e.message ?: e.javaClass.simpleName}"
41
- DevelopmentLog.error("Could not load image $uri$reason${(e as? LoadFailure)?.why.orEmpty()}", e)
42
- listener.onError(e)
23
+ val drawable = OptionImages.load(context, uri)
24
+ if (drawable == null) {
25
+ listener.onError(IOException("Could not load image $uri"))
43
26
  return
44
27
  }
45
28
  listener.onComplete(drawable)
46
29
  }
47
-
48
- private fun load(context: Context, uri: String): Drawable {
49
- if (uri.startsWith(FILE_SCHEME)) return decode(context) { FileInputStream(Uri.parse(uri).path) }
50
- resourceDrawable(context, uri)?.let { return it }
51
- if (!DevelopmentLog.isDevelopment) throw LoadFailure("")
52
- val address = Uri.parse(uri)
53
- return when (address.scheme) {
54
- "http", "https" -> {
55
- if (!isMetroAsset(address)) throw LoadFailure(": remote images are not supported in options")
56
- withNetworkOnMainThread { decode(context) { URL(uri).openStream() } }
57
- }
58
- else -> decode(context) {
59
- context.contentResolver.openInputStream(address) ?: throw FileNotFoundException(uri)
60
- }
61
- }
62
- }
63
-
64
- // A drawable of the app by name, its own copy; none when the app has none by that name.
65
- private fun resourceDrawable(context: Context, name: String): Drawable? {
66
- val id = ResourceDrawableIdHelper.getResourceDrawableId(context, name)
67
- if (id <= 0) return null
68
- return try {
69
- ContextCompat.getDrawable(context, id)?.mutate()
70
- } catch (e: Resources.NotFoundException) {
71
- null
72
- }
73
- }
74
-
75
- // The image of the stream [open] gives, closed after the read, also on an error; one it can't decode fails, and so
76
- // does one too large for the memory: that is a load error too, never the end of the app.
77
- private fun decode(context: Context, open: () -> InputStream): Drawable {
78
- val decoded = try {
79
- open().use { BitmapFactory.decodeStream(it) }
80
- } catch (e: OutOfMemoryError) {
81
- throw LoadFailure(": it is too large to decode")
82
- }
83
- val bitmap = decoded ?: throw LoadFailure(": it can't be decoded")
84
- return BitmapDrawable(context.resources, bitmap)
85
- }
86
-
87
- // A failure the loader names itself: `Could not load image <source>`, then [why].
88
- private class LoadFailure(val why: String) : IOException("Could not load image$why")
89
-
90
- // Metro's asset: an address of the server the JavaScript bundle came from, under `/assets/`.
91
- private fun isMetroAsset(address: Uri): Boolean {
92
- val bundle = DevelopmentLog.bundleUrl?.let(Uri::parse) ?: return false
93
- return address.host == bundle.host && address.port == bundle.port &&
94
- address.path.orEmpty().startsWith(ASSETS_PATH)
95
- }
96
-
97
- // Reading from the network on the main thread for [read] only; the thread's policy comes back, also on an error.
98
- private fun <T> withNetworkOnMainThread(read: () -> T): T {
99
- val policy = StrictMode.getThreadPolicy()
100
- StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder(policy).permitNetwork().build())
101
- try {
102
- return read()
103
- } finally {
104
- StrictMode.setThreadPolicy(policy)
105
- }
106
- }
107
-
108
- private companion object {
109
- const val FILE_SCHEME = "file:"
110
- const val ASSETS_PATH = "/assets/"
111
- }
112
30
  }