@momo-kits/native-kits 0.160.1-test.1-debug → 0.160.2-beta.1-debug

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 (29) hide show
  1. package/.claude/settings.local.json +3 -9
  2. package/compose/build.gradle.kts +9 -3
  3. package/compose/build.gradle.kts.backup +8 -2
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +7 -1
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +324 -117
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +5 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +5 -1
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +19 -14
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +7 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +15 -16
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +63 -55
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +62 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +9 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +12 -4
  19. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +33 -0
  20. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +7 -1
  21. package/gradle/libs.versions.toml +2 -0
  22. package/gradle.properties +1 -1
  23. package/ios/Input/Input.swift +33 -4
  24. package/ios/StatusBarTap/StatusBarTap.h +13 -0
  25. package/ios/StatusBarTap/StatusBarTap.m +75 -0
  26. package/ios/native-kits.podspec +2 -1
  27. package/local.properties +1 -1
  28. package/package.json +1 -1
  29. package/settings.gradle.kts +15 -3
@@ -221,6 +221,7 @@ fun Input(
221
221
  onBlur: () -> Unit = {},
222
222
  loading: Boolean = false,
223
223
  required: Boolean = false,
224
+ maxLength: Int? = null,
224
225
  fontWeight: InputFontWeight = InputFontWeight.REGULAR,
225
226
  keyboardType: KeyboardType = KeyboardType.Text,
226
227
  modifier: Modifier = Modifier,
@@ -319,7 +320,10 @@ fun Input(
319
320
  onBlur()
320
321
  }
321
322
  },
322
- onValueChange = onChangeText,
323
+ onValueChange = { newText ->
324
+ val limitedText = maxLength?.let { newText.take(it) } ?: newText
325
+ onChangeText(limitedText)
326
+ },
323
327
  decorationBox = { innerTextField ->
324
328
  // Floating label
325
329
  if (floatingValue.isNotEmpty() || floatingIcon.isNotEmpty()) {
@@ -8,6 +8,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
8
8
  import androidx.compose.animation.core.tween
9
9
  import androidx.compose.foundation.background
10
10
  import androidx.compose.foundation.border
11
+ import androidx.compose.ui.draw.alpha
11
12
  import androidx.compose.foundation.clickable
12
13
  import androidx.compose.foundation.layout.Arrangement
13
14
  import androidx.compose.foundation.layout.Box
@@ -129,8 +130,11 @@ fun InputOTP(
129
130
  if (!it.isFocused && isBlurred) onBlur()
130
131
  if (it.isFocused && !isBlurred) isBlurred = true
131
132
  },
132
- decorationBox = { _ ->
133
+ decorationBox = { innerTextField ->
133
134
  Box {
135
+ Box(
136
+ modifier = Modifier.fillMaxWidth().height(56.dp).alpha(0f)
137
+ ) { innerTextField() }
134
138
  if (floatingValue.isNotEmpty()) {
135
139
  Box(
136
140
  modifier = Modifier.wrapContentSize()
@@ -105,6 +105,8 @@ data class InputSearchProps(
105
105
  val iconModifier: Modifier = Modifier,
106
106
  val onClearPress: () -> Unit = {},
107
107
  val leftPosition: Dp? = null,
108
+ val placeholderCustomRender: (@Composable () -> Unit)? = null,
109
+ val searchIcon: (@Composable () -> Unit)? = null
108
110
  )
109
111
 
110
112
  @Composable
@@ -145,12 +147,13 @@ fun InputSearch(
145
147
  )
146
148
  }
147
149
 
148
- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
149
- .fillMaxWidth()
150
- .height(36.dp)
151
- .conditional(IsShowBaseLineDebug) {
152
- border(1.dp, Colors.blue_03)
153
- }
150
+ Row(
151
+ verticalAlignment = Alignment.CenterVertically, modifier = Modifier
152
+ .fillMaxWidth()
153
+ .height(36.dp)
154
+ .conditional(IsShowBaseLineDebug) {
155
+ border(1.dp, Colors.blue_03)
156
+ }
154
157
  ) {
155
158
  BasicTextField(
156
159
  enabled = !inputSearchProps.disabled,
@@ -184,13 +187,14 @@ fun InputSearch(
184
187
  horizontalArrangement = Arrangement.Start,
185
188
  verticalAlignment = Alignment.CenterVertically
186
189
  ) {
190
+
187
191
  Row(
188
192
  modifier = Modifier.padding(
189
193
  horizontal = Spacing.M
190
194
  ),
191
195
  verticalAlignment = Alignment.CenterVertically
192
196
  ) {
193
- Icon(
197
+ inputSearchProps.searchIcon?.invoke() ?: Icon(
194
198
  source = "navigation_search",
195
199
  modifier = Modifier.padding(end = Spacing.XS),
196
200
  size = 24.dp,
@@ -198,13 +202,14 @@ fun InputSearch(
198
202
  )
199
203
  Box(Modifier.weight(1f)) {
200
204
  if (inputSearchProps.text.value.isEmpty()) {
201
- Text(
202
- text = inputSearchProps.placeholder,
203
- style = placeHolderStyle,
204
- maxLines = 1,
205
- color = placeholderColor,
206
- overflow = TextOverflow.Ellipsis
207
- )
205
+ inputSearchProps.placeholderCustomRender?.invoke()
206
+ ?: Text(
207
+ text = inputSearchProps.placeholder,
208
+ style = placeHolderStyle,
209
+ maxLines = 1,
210
+ color = placeholderColor,
211
+ overflow = TextOverflow.Ellipsis
212
+ )
208
213
  }
209
214
  innerTextField()
210
215
  }
@@ -96,4 +96,5 @@ data class KeyboardOptions(
96
96
  data class ScrollData(
97
97
  val scrollable: Boolean = true,
98
98
  val scrollState: ScrollableState? = null,
99
+ val scrollToTopCallback: (() -> Unit)? = null,
99
100
  )
@@ -3,6 +3,7 @@ package vn.momo.kits.navigation
3
3
  import androidx.compose.animation.*
4
4
  import androidx.compose.animation.core.tween
5
5
  import androidx.compose.runtime.*
6
+ import androidx.compose.runtime.saveable.rememberSaveable
6
7
  import androidx.compose.ui.unit.Dp
7
8
  import androidx.navigation.compose.NavHost
8
9
  import androidx.navigation.compose.composable
@@ -49,7 +50,9 @@ fun NavigationContainer(
49
50
  }
50
51
  }
51
52
 
52
- val startDestination = remember { DynamicScreenRegistry.register(initialScreenName, initialScreen, options) }
53
+ val screenId = rememberSaveable { DynamicScreenRegistry.nextId() }
54
+ DynamicScreenRegistry.bind(screenId, initialScreenName, initialScreen, options)
55
+ val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
53
56
 
54
57
  ProvideNavigationEventDispatcherOwner {
55
58
  CompositionLocalProvider(
@@ -132,6 +135,9 @@ fun NavigationContainer(
132
135
  DisposableEffect(Unit) {
133
136
  onDispose {
134
137
  navigator.dispose()
138
+ // AI-GENERATED START: unregister initial screen on dispose to avoid leaking a stale registry entry
139
+ DynamicScreenRegistry.unregisterScreen(screenId)
140
+ // AI-GENERATED END: unregister initial screen on dispose to avoid leaking a stale registry entry
135
141
  }
136
142
  }
137
143
  }
@@ -28,13 +28,6 @@ class Navigator(
28
28
  ) {
29
29
  private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
30
30
 
31
- internal var currentScreenId: Int = -1
32
- private set
33
-
34
- internal fun setCurrentScreenId(id: Int) {
35
- currentScreenId = id
36
- }
37
-
38
31
  fun push(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
39
32
  val route = DynamicScreenRegistry.register(screenName, content, options)
40
33
  navController.navigate(DynamicScreenRoute(route.id))
@@ -129,9 +122,7 @@ class Navigator(
129
122
  barrierDismissible: Boolean = true,
130
123
  onDismiss: (() -> Unit)? = null
131
124
  ){
132
- // AI-GENERATED START: use per-Navigator currentScreenId; fall back to global registry if not yet set
133
- val id = currentScreenId.takeIf { it != -1 } ?: DynamicScreenRegistry.getLatestScreen()?.id ?: -1
134
- // AI-GENERATED END: use per-Navigator currentScreenId; fall back to global registry if not yet set
125
+ val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
135
126
  OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.MODAL, false, barrierDismissible, onDismiss)
136
127
  }
137
128
 
@@ -142,16 +133,12 @@ class Navigator(
142
133
  onDismiss: (() -> Unit)? = null,
143
134
  bottomSheetHeader: BottomHeader? = null
144
135
  ){
145
- // AI-GENERATED START: use per-Navigator currentScreenId; fall back to global registry if not yet set
146
- val id = currentScreenId.takeIf { it != -1 } ?: DynamicScreenRegistry.getLatestScreen()?.id ?: -1
147
- // AI-GENERATED END: use per-Navigator currentScreenId; fall back to global registry if not yet set
136
+ val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
148
137
  OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.BOTTOM_SHEET, isSurface, barrierDismissible, onDismiss, bottomSheetHeader)
149
138
  }
150
139
 
151
140
  fun showSnackBar(snackBar: SnackBar, onDismiss: (() -> Unit)? = null) {
152
- // AI-GENERATED START: use per-Navigator currentScreenId; fall back to global registry if not yet set
153
- val id = currentScreenId.takeIf { it != -1 } ?: DynamicScreenRegistry.getLatestScreen()?.id ?: -1
154
- // AI-GENERATED END: use per-Navigator currentScreenId; fall back to global registry if not yet set
141
+ val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
155
142
  scope.launch {
156
143
  OverplayComponentRegistry.registerOverplay(
157
144
  id = id,
@@ -209,6 +196,18 @@ object DynamicScreenRegistry {
209
196
  return DynamicScreenRoute(id)
210
197
  }
211
198
 
199
+ fun nextId(): Int = idCounter++
200
+
201
+ fun bind(id: Int, screenName: String, content: @Composable () -> Unit, options: NavigationOptions?) {
202
+ screens[id] = DynamicScreen(
203
+ id = id,
204
+ name = screenName,
205
+ content = content,
206
+ options = options
207
+ )
208
+ if (id >= idCounter) idCounter = id + 1 // keep counter ahead after process-death restore
209
+ }
210
+
212
211
  fun unregisterScreen(id: Int) {
213
212
  screens.remove(id)
214
213
  }
@@ -0,0 +1,8 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+
5
+ @Composable
6
+ internal expect fun RegisterScrollToTop(callback: (() -> Unit)?)
7
+
8
+
@@ -90,12 +90,6 @@ internal fun StackScreen(
90
90
  val density = LocalDensity.current
91
91
  val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions) }
92
92
 
93
- if (bottomTabIndex == -1) {
94
- LaunchedEffect(id) {
95
- navigator.setCurrentScreenId(id)
96
- }
97
- }
98
-
99
93
  val options by navigation.currentOptions
100
94
 
101
95
  // Auto tracking state
@@ -156,6 +150,10 @@ internal fun StackScreen(
156
150
  val footerHeightPx = remember { mutableIntStateOf(0) }
157
151
  val headerRightWidthPx = remember { mutableIntStateOf(0) }
158
152
 
153
+ if (navigation.options.scrollData.scrollToTopCallback != null) {
154
+ RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
155
+ }
156
+
159
157
  BackHandler(true) { navigator.onBackSafe() }
160
158
 
161
159
  CompositionLocalProvider(
@@ -166,15 +164,16 @@ internal fun StackScreen(
166
164
  LocalFooterHeightPx provides footerHeightPx,
167
165
  LocalHeaderRightWidthPx provides headerRightWidthPx
168
166
  ) {
169
- Box(Modifier
170
- .fillMaxSize()
171
- .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
172
- .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
173
- hideKeyboardOnTap()
174
- }
175
- .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
176
- imePadding()
177
- }
167
+ Box(
168
+ Modifier
169
+ .fillMaxSize()
170
+ .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
171
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
172
+ hideKeyboardOnTap()
173
+ }
174
+ .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
175
+ imePadding()
176
+ }
178
177
  ) {
179
178
  Box(Modifier.zIndex(1f)) {
180
179
  HeaderBackground()
@@ -184,7 +183,7 @@ internal fun StackScreen(
184
183
  Header(onBackHandler)
185
184
  }
186
185
 
187
- Column (Modifier.zIndex(6f)) {
186
+ Column(Modifier.zIndex(6f)) {
188
187
  SearchAnimated(isScrollInProgress = scrollInProcess)
189
188
  }
190
189
 
@@ -196,7 +195,7 @@ internal fun StackScreen(
196
195
  FooterContent()
197
196
  }
198
197
 
199
- Box(Modifier.zIndex(7f)){
198
+ Box(Modifier.zIndex(7f)) {
200
199
  FloatingContent()
201
200
  }
202
201
 
@@ -206,15 +205,15 @@ internal fun StackScreen(
206
205
  }
207
206
 
208
207
  @Composable
209
- fun FloatingContent(){
208
+ fun FloatingContent() {
210
209
  val options = LocalOptions.current
211
210
  val density = LocalDensity.current
212
211
  val footerHeightPx = LocalFooterHeightPx.current
213
212
  val scrollState = LocalScrollState.current
214
213
 
215
214
  val fabProps = options.floatingButtonProps ?: return
216
- val bottomPadding = fabProps.bottom ?:
217
- (Spacing.M + if (options.footerComponent != null) with(density){ footerHeightPx.intValue.toDp() } else 0.dp)
215
+ val bottomPadding = fabProps.bottom
216
+ ?: (Spacing.M + if (options.footerComponent != null) with(density) { footerHeightPx.intValue.toDp() } else 0.dp)
218
217
 
219
218
  FloatingButton(
220
219
  scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
@@ -230,17 +229,19 @@ fun FloatingContent(){
230
229
  }
231
230
 
232
231
  @Composable
233
- fun ColumnScope.MainContent(content: @Composable ()-> Unit){
232
+ fun ColumnScope.MainContent(content: @Composable () -> Unit) {
234
233
  val options = LocalOptions.current
235
234
  val inputSearchType = getInputSearchType(options)
236
235
  val density = LocalDensity.current
237
236
  val scrollState = LocalScrollState.current
238
237
 
239
- Spacer(Modifier.height(
240
- if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
241
- AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp)
238
+ Spacer(
239
+ Modifier.height(
240
+ if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
241
+ AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp
242
+ )
242
243
  )
243
- if (inputSearchType == InputSearchType.Animated){
244
+ if (inputSearchType == InputSearchType.Animated) {
244
245
  val scrollDp = with(density) { scrollState.value.toDp() }
245
246
 
246
247
  val animatedTopPadding by animateDpAsState(
@@ -249,35 +250,36 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
249
250
  )
250
251
  Spacer(Modifier.height(animatedTopPadding))
251
252
  }
252
- Column (Modifier
253
- .fillMaxWidth()
254
- .weight(1f)
255
- .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
256
- verticalScroll(scrollState)
257
- }
253
+ Column(
254
+ Modifier
255
+ .fillMaxWidth()
256
+ .weight(1f)
257
+ .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
258
+ verticalScroll(scrollState)
259
+ }
258
260
  ) {
259
261
  ScreenContent(content = content)
260
262
  }
261
263
 
262
- if (options.footerComponent != null){
264
+ if (options.footerComponent != null) {
263
265
  val footerHeight = LocalFooterHeightPx.current
264
- val footerHeightDp = with(density) { footerHeight.value.toDp() }
266
+ val footerHeightDp = with(density) { footerHeight.intValue.toDp() }
265
267
  Spacer(Modifier.height(footerHeightDp))
266
268
  }
267
269
  }
268
270
 
269
271
  @Composable
270
- fun ScreenContent(content: @Composable () -> Unit){
272
+ fun ScreenContent(content: @Composable () -> Unit) {
271
273
  val scrollState = LocalScrollState.current
272
274
  val options = LocalOptions.current
273
275
 
274
- if (options.headerType is HeaderType.Animated){
276
+ if (options.headerType is HeaderType.Animated) {
275
277
  val animatedHeader = options.headerType
276
278
  Box {
277
- Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)){
279
+ Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)) {
278
280
  animatedHeader.composable.invoke(scrollState.value)
279
281
  }
280
- Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)){
282
+ Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
281
283
  content()
282
284
  }
283
285
  }
@@ -287,9 +289,9 @@ fun ScreenContent(content: @Composable () -> Unit){
287
289
  }
288
290
 
289
291
  @Composable
290
- fun FooterContent(){
292
+ fun FooterContent() {
291
293
  val options = LocalOptions.current
292
- if (options.footerComponent != null){
294
+ if (options.footerComponent != null) {
293
295
  val ime = WindowInsets.ime
294
296
  val density = LocalDensity.current
295
297
  val imeBottom = ime.getBottom(density)
@@ -301,11 +303,11 @@ fun FooterContent(){
301
303
  }
302
304
 
303
305
  @Composable
304
- fun OverplayView(bottomTabIndex: Int, id: Int){
306
+ fun OverplayView(bottomTabIndex: Int, id: Int) {
305
307
  val overplayType = OverplayComponentRegistry.getOverplayType()
306
308
 
307
309
  if (overplayType != null) {
308
- Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()){
310
+ Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()) {
309
311
  if (bottomTabIndex != -1) return@Box
310
312
  if (OverplayComponentRegistry.currentRootId() != id) return@Box
311
313
  OverplayComponentRegistry.OverlayComponent()
@@ -328,22 +330,24 @@ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
328
330
  Box(Modifier.onGloballyPositioned {
329
331
  if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
330
332
  }) {
331
- Box(Modifier
332
- .fillMaxWidth()
333
- .background(AppTheme.current.colors.background.surface)
334
- .conditional(IsShowBaseLineDebug) {
335
- border(1.dp, Colors.blue_03)
336
- }
337
- .padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
338
- ){
333
+ Box(
334
+ Modifier
335
+ .fillMaxWidth()
336
+ .background(AppTheme.current.colors.background.surface)
337
+ .conditional(IsShowBaseLineDebug) {
338
+ border(1.dp, Colors.blue_03)
339
+ }
340
+ .padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
341
+ ) {
339
342
  footerComponent.invoke()
340
343
  }
341
344
 
342
- Box(modifier = Modifier
343
- .fillMaxWidth()
344
- .height(6.dp)
345
- .offset(x = 0.dp, y = (-6).dp)
346
- .background(shadowBrush)
345
+ Box(
346
+ modifier = Modifier
347
+ .fillMaxWidth()
348
+ .height(6.dp)
349
+ .offset(x = 0.dp, y = (-6).dp)
350
+ .background(shadowBrush)
347
351
  )
348
352
  }
349
353
  }
@@ -353,6 +357,7 @@ data class InputSearchLayoutParams(
353
357
  val startPadding: Dp,
354
358
  val endPadding: Dp
355
359
  )
360
+
356
361
  @Composable
357
362
  fun SearchAnimated(
358
363
  isScrollInProgress: Boolean
@@ -467,6 +472,7 @@ internal fun isKeyboardVisible(): Boolean {
467
472
  val density = LocalDensity.current
468
473
  return ime.getBottom(density) > 0
469
474
  }
475
+
470
476
  private fun quantize(value: Int, stepPx: Int): Int {
471
477
  if (stepPx <= 1) return value
472
478
  return (value / stepPx) * stepPx
@@ -546,13 +552,15 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
546
552
 
547
553
  val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
548
554
 
549
- internal fun getInputSearchType(options: NavigationOptions): InputSearchType{
555
+ internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
550
556
  return when (val headerType = options.headerType) {
551
557
  is HeaderType.DefaultOrExtended -> when {
552
558
  headerType.inputSearchProps == null -> InputSearchType.None
553
559
  headerType.useAnimated -> InputSearchType.Animated
554
560
  else -> InputSearchType.Header
555
561
  }
562
+
556
563
  else -> InputSearchType.None
557
564
  }
558
565
  }
566
+
@@ -0,0 +1,62 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.foundation.Image
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.fillMaxSize
7
+ import androidx.compose.runtime.Composable
8
+ import androidx.compose.runtime.getValue
9
+ import androidx.compose.ui.Modifier
10
+ import androidx.compose.ui.graphics.Color
11
+ import io.github.alexzhirkevich.compottie.Compottie
12
+ import io.github.alexzhirkevich.compottie.ExperimentalCompottieApi
13
+ import io.github.alexzhirkevich.compottie.LottieCompositionSpec
14
+ import io.github.alexzhirkevich.compottie.dynamic.rememberLottieDynamicProperties
15
+ import io.github.alexzhirkevich.compottie.rememberLottieComposition
16
+ import io.github.alexzhirkevich.compottie.rememberLottiePainter
17
+ import vn.momo.kits.utils.readJson
18
+
19
+ @OptIn(ExperimentalCompottieApi::class)
20
+ @Composable
21
+ internal fun ComposeLottieAnimation(
22
+ path: String,
23
+ tintColor: Color?,
24
+ bgColor: Color?,
25
+ modifier: Modifier,
26
+ ) {
27
+ val json = readJson(path)
28
+
29
+ if (json.isEmpty()) {
30
+ Box(modifier.background(bgColor ?: Color.Transparent))
31
+ return
32
+ }
33
+
34
+ val composition by rememberLottieComposition(json) {
35
+ LottieCompositionSpec.JsonString(json)
36
+ }
37
+
38
+ // Recolor only the fill/stroke color slots (matching the native lottie-ios per-keypath tint)
39
+ // instead of a global ColorFilter.tint, which would flatten a multicolor animation to a silhouette.
40
+ val dynamicProperties = if (tintColor != null) {
41
+ rememberLottieDynamicProperties(tintColor) {
42
+ shapeLayer("**") {
43
+ fill("**") { color { tintColor } }
44
+ stroke("**") { color { tintColor } }
45
+ }
46
+ }
47
+ } else {
48
+ null
49
+ }
50
+
51
+ Box(modifier.background(bgColor ?: Color.Transparent)) {
52
+ Image(
53
+ painter = rememberLottiePainter(
54
+ composition = composition,
55
+ iterations = Compottie.IterateForever,
56
+ dynamicProperties = dynamicProperties,
57
+ ),
58
+ contentDescription = null,
59
+ modifier = Modifier.fillMaxSize(),
60
+ )
61
+ }
62
+ }
@@ -44,13 +44,21 @@ fun supportsImePadding(): Boolean = when (val v = getOSVersion()) {
44
44
  is OSVersion.IOS -> true
45
45
  }
46
46
 
47
+ /**
48
+ * @param useCompose when `true`, renders via the pure-Compose Compottie engine instead of the
49
+ * platform-native engine (airbnb-lottie on Android, lottie-ios on iOS). Defaults to `false` for
50
+ * backward compatibility. Note: when `useCompose = true`, `tintColor` recolors only the fill/stroke
51
+ * color slots (matching the native lottie-ios per-keypath tint), so gradient and text-layer colors
52
+ * are left untinted; `placedAsOverlay` is a no-op.
53
+ */
47
54
  @Composable
48
55
  expect fun LottieAnimation(
49
56
  path: String,
50
57
  tintColor: Color? = null,
51
58
  bgColor: Color? = null,
52
59
  placedAsOverlay: Boolean = false,
53
- modifier: Modifier = Modifier
60
+ modifier: Modifier = Modifier,
61
+ useCompose: Boolean = false
54
62
  )
55
63
 
56
64
  expect fun NativePaint.setColor(
@@ -24,13 +24,21 @@ fun getResource(name: String): DrawableResource {
24
24
  @Composable
25
25
  fun readJson(name: String): String {
26
26
  val path = name.plus(".json")
27
+ val candidatePaths = listOf(
28
+ path,
29
+ "composeResources/vn.momo.compose.resources/$path",
30
+ )
27
31
 
28
32
  val jsonContent by rememberState(path, { "" }) {
29
33
  val cached = resourceCache.getOrPut(path) {
30
- val content = try {
31
- readResourceBytes(path).decodeToString()
32
- } catch (_: Exception) {
33
- ""
34
+ var content = ""
35
+ for (candidate in candidatePaths) {
36
+ try {
37
+ content = readResourceBytes(candidate).decodeToString()
38
+ break
39
+ } catch (_: Exception) {
40
+ // try next candidate
41
+ }
34
42
  }
35
43
  ResourceCache.JSON(content)
36
44
  } as ResourceCache.JSON
@@ -0,0 +1,33 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import platform.Foundation.NSNotification
6
+ import platform.Foundation.NSNotificationCenter
7
+ import platform.Foundation.NSOperationQueue
8
+ import platform.darwin.NSObjectProtocol
9
+
10
+ private const val STATUS_BAR_TAPPED_NOTIFICATION = "statusBarSelected"
11
+
12
+ @Composable
13
+ internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
14
+ val maxApi = LocalMaxApi.current
15
+ DisposableEffect(callback) {
16
+ val cb = callback
17
+ val token: NSObjectProtocol? = if (cb != null) {
18
+ NSNotificationCenter.defaultCenter.addObserverForName(
19
+ name = STATUS_BAR_TAPPED_NOTIFICATION,
20
+ `object` = null,
21
+ queue = NSOperationQueue.mainQueue,
22
+ ) { _: NSNotification? ->
23
+ runCatching { cb() }.onFailure {
24
+ maxApi?.logFile("[ScrollToTop] callback threw: ${it.message}") {}
25
+
26
+ }
27
+ }
28
+ } else null
29
+ onDispose {
30
+ token?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
31
+ }
32
+ }
33
+ }
@@ -98,8 +98,14 @@ actual fun LottieAnimation(
98
98
  tintColor: Color?,
99
99
  bgColor: Color?,
100
100
  placedAsOverlay: Boolean,
101
- modifier: Modifier
101
+ modifier: Modifier,
102
+ useCompose: Boolean
102
103
  ) {
104
+ if (useCompose) {
105
+ ComposeLottieAnimation(path, tintColor, bgColor, modifier)
106
+ return
107
+ }
108
+
103
109
  var animation by remember { mutableStateOf<CompatibleAnimation?>(null) }
104
110
 
105
111
  LaunchedEffect(Unit) {
@@ -13,6 +13,7 @@ androidGradlePlugin = "8.13.2"
13
13
  r8 = "8.9.35"
14
14
  kotlinx-datetime = "0.7.1"
15
15
  airbnb-lottie = "5.2.0"
16
+ compottie = "2.2.1"
16
17
  androidx-activity = "1.9.1"
17
18
  androidx-appcompat = "1.7.0"
18
19
  material = "1.10.0"
@@ -38,6 +39,7 @@ coil-multiplatform-compose = { module = "io.coil-kt.coil3:coil-compose", version
38
39
  coil-multiplatform-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3-multiplatform" }
39
40
  r8 = { module = "com.android.tools:r8", version.ref = "r8" }
40
41
  airbnb-lottie = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "airbnb-lottie" }
42
+ compottie = { module = "io.github.alexzhirkevich:compottie", version.ref = "compottie" }
41
43
  native-max-api = { group = "vn.momo.maxapi", name = "NativeMaxApi", version.ref = "nativemaxapi" }
42
44
  kits = { module = "vn.momo.kits:kits", version.ref = "kits" }
43
45
  androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.160.1-test.1
21
+ version=0.160.2-beta.1
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven