@momo-kits/native-kits 0.161.2-beta.9-debug → 0.161.2-nativeimage.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 (21) hide show
  1. package/compose/build.gradle.kts +9 -11
  2. package/compose/build.gradle.kts.backup +8 -10
  3. package/compose/compose.podspec +2 -1
  4. package/compose/src/androidMain/kotlin/vn/momo/kits/components/NativeImage.android.kt +71 -0
  5. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +24 -30
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +13 -9
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/NativeImage.kt +74 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +5 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -4
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +9 -45
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -4
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +11 -1
  13. package/compose/src/iosMain/kotlin/vn/momo/kits/components/NativeImage.ios.kt +142 -0
  14. package/example/ios/Example.xcodeproj/xcuserdata/thanhle.xcuserdatad/xcschemes/xcschememanagement.plist +5 -0
  15. package/example/ios/Example.xcworkspace/xcuserdata/thanhle.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  16. package/example/ios/Example.xcworkspace/xcuserdata/thanhle.xcuserdatad/xcschemes/xcschememanagement.plist +5 -0
  17. package/gradle/libs.versions.toml +14 -2
  18. package/gradle.properties +1 -1
  19. package/local.properties +2 -2
  20. package/package.json +1 -1
  21. package/.claude/settings.local.json +0 -32
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.161.2-beta.9-debug"
43
+ version = "0.161.2-nativeimage.1-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -55,6 +55,10 @@ kotlin {
55
55
  version = "4.4.3"
56
56
  extraOpts += listOf("-compiler-option", "-fmodules")
57
57
  }
58
+
59
+ pod("SDWebImage") {
60
+ version = "~> 5.14.3"
61
+ }
58
62
  }
59
63
 
60
64
  sourceSets {
@@ -73,19 +77,13 @@ kotlin {
73
77
  implementation(libs.jetbrains.serialization.json)
74
78
  implementation(libs.kotlinx.datetime)
75
79
  api(libs.native.max.api)
76
- // AI-GENERATED START
77
- // compottie-core depends on keight-core directly, and keight (also a dep of compottie)
78
- // also depends on keight-core transitively. Kotlin Native links both static archives
79
- // into the framework causing duplicate symbols. Exclude keight-core so it is
80
- // pulled only once through keight. String notation required for exclude lambda in KMP sourceSets.
81
- implementation("io.github.alexzhirkevich:compottie:${libs.versions.compottie.get()}") {
82
- exclude(group = "io.github.alexzhirkevich", module = "keight-core")
83
- }
84
- // AI-GENERATED END
80
+ implementation(libs.compottie)
85
81
  }
86
82
  androidMain.dependencies {
87
83
  implementation(libs.ktor.client.okhttp)
88
84
  implementation(libs.airbnb.lottie)
85
+ implementation(libs.coil.gif)
86
+ implementation(libs.coil.http)
89
87
  }
90
88
  iosMain.dependencies {
91
89
  implementation(libs.ktor.client.darwin)
@@ -120,7 +118,7 @@ repositories {
120
118
  maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
121
119
  maven {
122
120
  url = uri("https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven")
123
- credentials {username = "download_packages"; password = "gldt-bjDqLpU_sPcHDuXau2ws" }
121
+ credentials { username = "download_packages"; password = "gldt-bjDqLpU_sPcHDuXau2ws" }
124
122
  }
125
123
  maven {
126
124
  url = uri("https://artifacts.mservice.com.vn/repository/maven-momo-app/")
@@ -55,6 +55,10 @@ kotlin {
55
55
  version = "4.4.3"
56
56
  extraOpts += listOf("-compiler-option", "-fmodules")
57
57
  }
58
+
59
+ pod("SDWebImage") {
60
+ version = "~> 5.14.3"
61
+ }
58
62
  }
59
63
 
60
64
  sourceSets {
@@ -73,19 +77,13 @@ kotlin {
73
77
  implementation(libs.jetbrains.serialization.json)
74
78
  implementation(libs.kotlinx.datetime)
75
79
  api(libs.native.max.api)
76
- // AI-GENERATED START
77
- // compottie-core depends on keight-core directly, and keight (also a dep of compottie)
78
- // also depends on keight-core transitively. Kotlin Native links both static archives
79
- // into the framework causing duplicate symbols. Exclude keight-core so it is
80
- // pulled only once through keight. String notation required for exclude lambda in KMP sourceSets.
81
- implementation("io.github.alexzhirkevich:compottie:${libs.versions.compottie.get()}") {
82
- exclude(group = "io.github.alexzhirkevich", module = "keight-core")
83
- }
84
- // AI-GENERATED END
80
+ implementation(libs.compottie)
85
81
  }
86
82
  androidMain.dependencies {
87
83
  implementation(libs.ktor.client.okhttp)
88
84
  implementation(libs.airbnb.lottie)
85
+ implementation(libs.coil.gif)
86
+ implementation(libs.coil.http)
89
87
  }
90
88
  iosMain.dependencies {
91
89
  implementation(libs.ktor.client.darwin)
@@ -120,7 +118,7 @@ repositories {
120
118
  maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
121
119
  maven {
122
120
  url = uri("https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven")
123
- credentials {username = "download_packages"; password = "gldt-bjDqLpU_sPcHDuXau2ws" }
121
+ credentials { username = "download_packages"; password = "gldt-bjDqLpU_sPcHDuXau2ws" }
124
122
  }
125
123
  maven {
126
124
  url = uri("https://artifacts.mservice.com.vn/repository/maven-momo-app/")
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.161.2-beta.9'
3
+ spec.version = '0.161.2-beta.15'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -9,6 +9,7 @@ Pod::Spec.new do |spec|
9
9
  spec.vendored_frameworks = 'build/cocoapods/framework/kits.framework'
10
10
  spec.libraries = 'c++'
11
11
  spec.ios.deployment_target = '15.0'
12
+ spec.dependency 'SDWebImage', '~> 5.14.3'
12
13
  spec.dependency 'lottie-ios', '4.4.3'
13
14
  if !Dir.exist?('build/cocoapods/framework/kits.framework') || Dir.empty?('build/cocoapods/framework/kits.framework')
14
15
  raise "
@@ -0,0 +1,71 @@
1
+ package vn.momo.kits.components
2
+
3
+ import android.os.Build.VERSION.SDK_INT
4
+ import androidx.compose.foundation.layout.BoxScope
5
+ import androidx.compose.runtime.*
6
+ import androidx.compose.ui.Alignment
7
+ import androidx.compose.ui.Modifier
8
+ import androidx.compose.ui.platform.LocalContext
9
+ import androidx.compose.ui.unit.dp
10
+ import coil3.SingletonImageLoader
11
+ import coil3.compose.AsyncImage
12
+ import coil3.gif.AnimatedImageDecoder
13
+ import coil3.gif.GifDecoder
14
+ import vn.momo.kits.const.AppTheme
15
+
16
+
17
+ @Composable
18
+ actual fun BoxScope.NativeImageWrapper(
19
+ urlToLoad: Any,
20
+ modifier: Modifier,
21
+ options: Options?,
22
+ loading: Boolean
23
+ ) {
24
+ val imageOptions = remember(options) {
25
+ options ?: Options()
26
+ }
27
+ var imageState by remember { mutableStateOf(ImageState.Loading) }
28
+ val context = LocalContext.current
29
+ val gifEnabledLoader = remember(context) {
30
+ if (isGifImage(urlToLoad)) {
31
+ SingletonImageLoader.get(context).newBuilder()
32
+ .components {
33
+ if (SDK_INT >= 28) {
34
+ add(AnimatedImageDecoder.Factory())
35
+ } else {
36
+ add(GifDecoder.Factory())
37
+ }
38
+ }
39
+ .build()
40
+ } else {
41
+ SingletonImageLoader.get(context)
42
+ }
43
+ }
44
+ AsyncImage(
45
+ modifier = Modifier.matchParentSize(),
46
+ model = urlToLoad,
47
+ imageLoader = gifEnabledLoader,
48
+ contentDescription = null,
49
+ contentScale = imageOptions.contentScale,
50
+ alignment = imageOptions.alignment,
51
+ colorFilter = imageOptions.effectiveColorFilter,
52
+ alpha = imageOptions.alpha,
53
+ onLoading = { imageState = ImageState.Loading },
54
+ onSuccess = { imageState = ImageState.Success },
55
+ onError = { imageState = ImageState.Error },
56
+ )
57
+
58
+ when (imageState) {
59
+ ImageState.Loading -> if (loading) Skeleton(cornerRadius = options?.skeletonCornerRadius ?: 0.dp)
60
+ ImageState.Error -> {
61
+ val theme = AppTheme.current
62
+ Icon(
63
+ source = "media_fail",
64
+ color = theme.colors.text.disable,
65
+ modifier = Modifier.align(Alignment.Center)
66
+ )
67
+ }
68
+
69
+ ImageState.Success -> {}
70
+ }
71
+ }
@@ -38,6 +38,7 @@ import androidx.compose.runtime.produceState
38
38
  import androidx.compose.runtime.remember
39
39
  import androidx.compose.runtime.rememberUpdatedState
40
40
  import androidx.compose.runtime.snapshotFlow
41
+ import androidx.compose.runtime.staticCompositionLocalOf
41
42
  import androidx.compose.ui.Alignment
42
43
  import androidx.compose.ui.Modifier
43
44
  import androidx.compose.ui.composed
@@ -48,6 +49,7 @@ import androidx.compose.ui.geometry.Offset
48
49
  import androidx.compose.ui.geometry.Rect
49
50
  import androidx.compose.ui.graphics.Brush
50
51
  import androidx.compose.ui.graphics.Color
52
+ import androidx.compose.ui.graphics.SolidColor
51
53
  import androidx.compose.ui.graphics.graphicsLayer
52
54
  import androidx.compose.ui.input.pointer.pointerInput
53
55
  import androidx.compose.ui.layout.Layout
@@ -60,19 +62,15 @@ import androidx.compose.ui.layout.layoutId
60
62
  import androidx.compose.ui.platform.LocalDensity
61
63
  import androidx.compose.ui.platform.LocalFocusManager
62
64
  import androidx.compose.ui.platform.LocalSoftwareKeyboardController
63
- import androidx.compose.ui.text.style.TextOverflow
64
65
  import androidx.compose.ui.text.TextStyle
66
+ import androidx.compose.ui.text.style.TextOverflow
65
67
  import androidx.compose.ui.unit.Constraints
66
68
  import androidx.compose.ui.unit.Dp
67
69
  import androidx.compose.ui.unit.IntOffset
68
70
  import androidx.compose.ui.unit.LayoutDirection
69
71
  import androidx.compose.ui.unit.dp
70
- import androidx.compose.ui.unit.sp
71
- import androidx.compose.runtime.staticCompositionLocalOf
72
- import androidx.compose.ui.graphics.Color.Companion
73
- import androidx.compose.ui.graphics.SolidColor
74
72
  import androidx.compose.ui.unit.lerp
75
- import kotlinx.coroutines.flow.StateFlow
73
+ import androidx.compose.ui.unit.sp
76
74
  import kotlinx.coroutines.flow.collectLatest
77
75
  import kotlinx.coroutines.flow.mapNotNull
78
76
  import vn.momo.kits.components.Icon
@@ -81,11 +79,11 @@ import vn.momo.kits.const.AppTheme
81
79
  import vn.momo.kits.const.Colors
82
80
  import vn.momo.kits.const.Spacing
83
81
  import vn.momo.kits.const.Typography
84
- import vn.momo.kits.modifier.conditional
85
82
  import vn.momo.kits.modifier.kitsAutomationId
86
83
  import vn.momo.kits.modifier.noFeedbackClickable
87
84
  import vn.momo.kits.modifier.setAutomationId
88
85
  import vn.momo.kits.modifier.shadow
86
+ import vn.momo.kits.navigation.component.HEADER_HEIGHT
89
87
  import vn.momo.kits.utils.getAppStatusBarHeight
90
88
  import kotlin.math.max
91
89
  import kotlin.math.roundToInt
@@ -441,6 +439,7 @@ private class LiteScreenLayoutPolicy(
441
439
  val spacing12 = Spacing.M.roundToPx()
442
440
  val spaceBetween = headerSpaceBetween?.roundToPx() ?: spacing12
443
441
  val statusBarPx = statusBarHeight.roundToPx()
442
+ val headerRowDefault = HEADER_HEIGHT.dp.roundToPx()
444
443
  val scrollPercent = scrollPercentage.value
445
444
 
446
445
  val contentMeasurable = measurables.find { it.layoutId == HeaderId.CONTENT_ID }
@@ -484,26 +483,20 @@ private class LiteScreenLayoutPolicy(
484
483
  )
485
484
  )
486
485
 
487
- val firstRowMaxHeight = buildList {
488
- add(backIconPlaceable.safeHeight)
489
- add(headerRightPlaceable.safeHeight)
490
- if (!isHeaderExtend) {
491
- add(inputSearchPlaceable.safeHeight)
492
- }
493
- add(HEADER_HEIGHT.dp.roundToPx())
494
- if (isHeaderExtend) {
495
- add(titlePlaceable.safeHeight)
496
- }
497
- }.max()
498
-
499
- var defaultHeight = statusBarPx + spacing12 + firstRowMaxHeight + spacing12
486
+ /**
487
+ * replicate logic from [vn.momo.kits.navigation.component.HeaderBackground]
488
+ * */
489
+ var defaultHeight = statusBarPx + headerRowDefault
500
490
  if (isHeaderExtend) {
501
- defaultHeight += inputSearchPlaceable.safeHeight + spacing12
491
+ defaultHeight = 154.dp.roundToPx()
502
492
  }
503
493
  val headerHeight = when {
504
494
  isHeaderNone -> statusBarPx
505
495
  !useAnimationSearch && !isHeaderExtend -> defaultHeight
506
- else -> (defaultHeight - scrollPercent * (defaultHeight - statusBarPx - HEADER_HEIGHT.dp.roundToPx())).toInt()
496
+ else -> {
497
+ val collapsableSpace = defaultHeight - statusBarPx - headerRowDefault
498
+ (defaultHeight - scrollPercent * collapsableSpace).toInt()
499
+ }
507
500
  }
508
501
 
509
502
  val layoutWidth = constraints.maxWidth
@@ -566,14 +559,14 @@ private class LiteScreenLayoutPolicy(
566
559
  if (backIconPlaceable != null) {
567
560
  backIconPlaceable.place(
568
561
  x = startX,
569
- y = startY + backIconPlaceable.verticalCenterOffset(firstRowMaxHeight),
562
+ y = startY + backIconPlaceable.verticalCenterOffset(headerRowDefault),
570
563
  )
571
564
  curX += backIconPlaceable.safeWidth + spaceBetween
572
565
  }
573
566
 
574
567
  headerRightPlaceable?.place(
575
568
  x = layoutWidth - spacing12 - headerRightPlaceable.safeWidth,
576
- y = startY + headerRightPlaceable.verticalCenterOffset(firstRowMaxHeight),
569
+ y = startY + headerRightPlaceable.verticalCenterOffset(headerRowDefault),
577
570
  )
578
571
 
579
572
  val titleOffset = IntOffset(
@@ -582,19 +575,19 @@ private class LiteScreenLayoutPolicy(
582
575
  space = layoutWidth,
583
576
  layoutDirection = layoutDirection,
584
577
  ),
585
- y = startY + titlePlaceable.verticalCenterOffset(firstRowMaxHeight),
578
+ y = startY + titlePlaceable.verticalCenterOffset(headerRowDefault),
586
579
  )
587
580
 
588
581
  titlePlaceable?.place(titleOffset)
589
582
 
590
583
  if (backIconPlaceable != null || headerRightPlaceable != null || titlePlaceable != null) {
591
- curY += firstRowMaxHeight
584
+ curY += headerRowDefault
592
585
  }
593
586
 
594
587
  val inputSearchOffset = if (isHeaderExtend) {
595
- val baseY = curY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight)
588
+ val baseY = curY + inputSearchPlaceable.verticalCenterOffset(headerRowDefault)
596
589
  val y = (baseY * (1 - scrollPercent)).toInt().coerceAtLeast(
597
- startY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight)
590
+ startY + inputSearchPlaceable.verticalCenterOffset(headerRowDefault)
598
591
  )
599
592
  IntOffset(
600
593
  x = startX + ((backIconPlaceable.safeWidth + spaceBetween) * (scrollPercent * 2f).coerceIn(
@@ -605,7 +598,7 @@ private class LiteScreenLayoutPolicy(
605
598
  } else {
606
599
  IntOffset(
607
600
  x = curX,
608
- y = startY + inputSearchPlaceable.verticalCenterOffset(firstRowMaxHeight),
601
+ y = startY + inputSearchPlaceable.verticalCenterOffset(headerRowDefault),
609
602
  )
610
603
  }
611
604
  val finalPosition = lerp(
@@ -823,7 +816,8 @@ private fun LiteInputSearch(
823
816
  if (!placeHolder.isNullOrEmpty()) {
824
817
  inputSearchProps.customPlaceHolder?.invoke() ?: Text(
825
818
  text = placeHolder ?: "",
826
- style = inputSearchProps.customTextStyle ?: Typography.bodyDefaultRegular,
819
+ style = inputSearchProps.customTextStyle
820
+ ?: Typography.bodyDefaultRegular,
827
821
  maxLines = 1,
828
822
  color = theme.colors.text.hint,
829
823
  overflow = TextOverflow.Ellipsis
@@ -2,20 +2,18 @@ package vn.momo.kits.components
2
2
 
3
3
  import androidx.compose.foundation.border
4
4
  import androidx.compose.foundation.layout.BoxWithConstraints
5
- import androidx.compose.runtime.Composable
6
- import androidx.compose.runtime.getValue
7
- import androidx.compose.runtime.mutableStateOf
8
- import androidx.compose.runtime.remember
9
- import androidx.compose.runtime.setValue
5
+ import androidx.compose.runtime.*
10
6
  import androidx.compose.ui.Alignment
11
7
  import androidx.compose.ui.Modifier
8
+ import androidx.compose.ui.graphics.Color
12
9
  import androidx.compose.ui.graphics.ColorFilter
13
10
  import androidx.compose.ui.graphics.DefaultAlpha
14
11
  import androidx.compose.ui.graphics.painter.Painter
15
12
  import androidx.compose.ui.layout.ContentScale
13
+ import androidx.compose.ui.platform.LocalDensity
16
14
  import androidx.compose.ui.semantics.contentDescription
17
15
  import androidx.compose.ui.semantics.semantics
18
- import androidx.compose.ui.platform.LocalDensity
16
+ import androidx.compose.ui.unit.Dp
19
17
  import androidx.compose.ui.unit.dp
20
18
  import coil3.compose.AsyncImage
21
19
  import vn.momo.kits.application.IsShowBaseLineDebug
@@ -28,7 +26,13 @@ data class Options(
28
26
  val contentScale: ContentScale = ContentScale.Crop,
29
27
  val colorFilter: ColorFilter? = null,
30
28
  val alpha: Float = DefaultAlpha,
31
- )
29
+ val skeletonCornerRadius: Dp = 0.dp,
30
+ val tint: Color? = null,
31
+ ) {
32
+
33
+ val effectiveColorFilter: ColorFilter?
34
+ get() = tint?.let { ColorFilter.tint(it) } ?: colorFilter
35
+ }
32
36
 
33
37
  enum class ImageState {
34
38
  Loading, Success, Error
@@ -131,7 +135,7 @@ fun Image(
131
135
  contentDescription = null,
132
136
  contentScale = imageOptions.contentScale,
133
137
  alignment = imageOptions.alignment,
134
- colorFilter = imageOptions.colorFilter,
138
+ colorFilter = imageOptions.effectiveColorFilter,
135
139
  alpha = imageOptions.alpha,
136
140
  onLoading = { imageState = ImageState.Loading },
137
141
  onSuccess = { imageState = ImageState.Success },
@@ -154,7 +158,7 @@ fun Image(
154
158
  }
155
159
 
156
160
  // Extracted URL processing logic for better performance
157
- private fun processImageUrl(url: String, maxWidth: androidx.compose.ui.unit.Dp, density: androidx.compose.ui.unit.Density): String {
161
+ internal fun processImageUrl(url: String, maxWidth: androidx.compose.ui.unit.Dp, density: androidx.compose.ui.unit.Density): String {
158
162
  val host = getHostFromUrl(url)
159
163
  return if (host != null && supportedDomains.contains(host)) {
160
164
  val widthPx = with(density) { maxWidth.roundToPx() }
@@ -0,0 +1,74 @@
1
+ package vn.momo.kits.components
2
+
3
+
4
+ import androidx.compose.foundation.border
5
+ import androidx.compose.foundation.layout.BoxScope
6
+ import androidx.compose.foundation.layout.BoxWithConstraints
7
+ import androidx.compose.runtime.Composable
8
+ import androidx.compose.runtime.remember
9
+ import androidx.compose.ui.Modifier
10
+ import androidx.compose.ui.platform.LocalDensity
11
+ import androidx.compose.ui.semantics.contentDescription
12
+ import androidx.compose.ui.semantics.semantics
13
+ import androidx.compose.ui.unit.dp
14
+ import vn.momo.kits.application.IsShowBaseLineDebug
15
+ import vn.momo.kits.const.Colors
16
+ import vn.momo.kits.modifier.conditional
17
+
18
+
19
+ fun isGifImage(source: Any): Boolean {
20
+ if (source is String) {
21
+ return source.contains(".gif") ||
22
+ source.contains(".webp") ||
23
+ source.contains(".apng") ||
24
+ source.contains(".avif")
25
+ }
26
+ return false
27
+ }
28
+
29
+ private val urlCache = mutableMapOf<String, String>()
30
+
31
+ @Composable
32
+ fun NativeImage(
33
+ source: Any,
34
+ modifier: Modifier = Modifier,
35
+ options: Options? = null,
36
+ loading: Boolean = true
37
+ ) {
38
+ BoxWithConstraints(
39
+ modifier = modifier
40
+ .conditional(IsShowBaseLineDebug) {
41
+ border(1.dp, Colors.blue_03)
42
+ }
43
+ .semantics { contentDescription = "img|$source" },
44
+ ) {
45
+ val density = LocalDensity.current
46
+ val urlToLoad = remember(source, maxWidth, maxHeight, density) {
47
+ when (source) {
48
+ is String -> {
49
+ // Check cache first
50
+ val cacheKey = "$source|${maxWidth}|${density.density}"
51
+ urlCache[cacheKey] ?: run {
52
+ val processedUrl = processImageUrl(source, maxWidth, density)
53
+ urlCache[cacheKey] = processedUrl
54
+ processedUrl
55
+ }
56
+ }
57
+
58
+ else -> source
59
+ }
60
+ }
61
+
62
+ NativeImageWrapper(urlToLoad, modifier, options, loading)
63
+
64
+ }
65
+ }
66
+
67
+
68
+ @Composable
69
+ expect fun BoxScope.NativeImageWrapper(
70
+ urlToLoad: Any,
71
+ modifier: Modifier = Modifier,
72
+ options: Options? = null,
73
+ loading: Boolean = true,
74
+ )
@@ -11,12 +11,15 @@ import androidx.compose.foundation.border
11
11
  import androidx.compose.foundation.layout.Box
12
12
  import androidx.compose.foundation.layout.BoxWithConstraints
13
13
  import androidx.compose.foundation.layout.fillMaxSize
14
+ import androidx.compose.foundation.shape.RoundedCornerShape
14
15
  import androidx.compose.runtime.Composable
15
16
  import androidx.compose.runtime.remember
16
17
  import androidx.compose.ui.Modifier
18
+ import androidx.compose.ui.draw.clip
17
19
  import androidx.compose.ui.draw.drawBehind
18
20
  import androidx.compose.ui.geometry.Offset
19
21
  import androidx.compose.ui.graphics.Brush
22
+ import androidx.compose.ui.unit.Dp
20
23
  import androidx.compose.ui.unit.dp
21
24
  import vn.momo.kits.application.IsShowBaseLineDebug
22
25
  import vn.momo.kits.const.Colors
@@ -34,7 +37,7 @@ private const val SHADOW_BRUSH_PERCENTAGE = 0.6f
34
37
  private const val DEFAULT_DURATION_MILLIS = 1000
35
38
 
36
39
  @Composable
37
- fun Skeleton() {
40
+ fun Skeleton(cornerRadius: Dp = 0.dp) {
38
41
  BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
39
42
  // Memoize width calculation
40
43
  val maxWidthValue = remember(maxWidth) { maxWidth.value.toInt() }
@@ -42,6 +45,7 @@ fun Skeleton() {
42
45
  Box(
43
46
  modifier = Modifier
44
47
  .fillMaxSize()
48
+ .clip(RoundedCornerShape(cornerRadius))
45
49
  .background(Colors.black_05)
46
50
  .conditional(IsShowBaseLineDebug) {
47
51
  border(1.dp, Colors.blue_03)
@@ -13,7 +13,6 @@ import vn.momo.kits.navigation.component.HeaderBackProps
13
13
  import vn.momo.kits.navigation.component.HeaderRight
14
14
  import vn.momo.kits.navigation.component.HeaderTitle
15
15
  import vn.momo.kits.navigation.component.HeaderType
16
- import vn.momo.kits.navigation.component.TitlePosition
17
16
 
18
17
  class Navigation(
19
18
  val id: Int = -1,
@@ -29,7 +28,6 @@ class Navigation(
29
28
  hiddenBack: Boolean? = null,
30
29
  headerBackProps: HeaderBackProps? = null,
31
30
  headerTitle: HeaderTitle? = null,
32
- titlePosition: TitlePosition? = null,
33
31
  headerRight: HeaderRight? = null,
34
32
  headerType: HeaderType? = null,
35
33
  scrollData: ScrollData? = null,
@@ -45,7 +43,6 @@ class Navigation(
45
43
  hiddenBack = hiddenBack ?: options.hiddenBack,
46
44
  headerBackProps = headerBackProps ?: options.headerBackProps,
47
45
  headerTitle = headerTitle ?: options.headerTitle,
48
- titlePosition = titlePosition ?: options.titlePosition,
49
46
  headerRight = headerRight ?: options.headerRight,
50
47
  headerType = headerType ?: options.headerType,
51
48
  scrollData = scrollData ?: options.scrollData,
@@ -80,7 +77,6 @@ data class NavigationOptions(
80
77
  val hiddenBack: Boolean = false,
81
78
  val headerBackProps: HeaderBackProps = HeaderBackProps(),
82
79
  val headerTitle: HeaderTitle = HeaderTitle.Default("Stack"),
83
- val titlePosition: TitlePosition = TitlePosition.LEFT,
84
80
  val headerRight: HeaderRight = HeaderRight.Toolkit(),
85
81
  val headerType: HeaderType = HeaderType.Default(),
86
82
  val scrollData: ScrollData = ScrollData(),
@@ -6,6 +6,7 @@ import androidx.compose.foundation.border
6
6
  import androidx.compose.foundation.layout.Arrangement
7
7
  import androidx.compose.foundation.layout.Box
8
8
  import androidx.compose.foundation.layout.Row
9
+ import androidx.compose.foundation.layout.RowScope
9
10
  import androidx.compose.foundation.layout.Spacer
10
11
  import androidx.compose.foundation.layout.fillMaxWidth
11
12
  import androidx.compose.foundation.layout.height
@@ -22,7 +23,6 @@ import androidx.compose.ui.graphics.Brush
22
23
  import androidx.compose.ui.graphics.Color
23
24
  import androidx.compose.ui.layout.onGloballyPositioned
24
25
  import androidx.compose.ui.platform.LocalDensity
25
- import androidx.compose.ui.text.style.TextAlign
26
26
  import androidx.compose.ui.unit.Dp
27
27
  import androidx.compose.ui.unit.dp
28
28
  import vn.momo.kits.components.Icon
@@ -44,7 +44,6 @@ import vn.momo.kits.navigation.getInputSearchType
44
44
 
45
45
  const val HEADER_HEIGHT = 52
46
46
  enum class InputSearchType { None, Header, Animated }
47
- enum class TitlePosition { LEFT, CENTER }
48
47
 
49
48
  @Composable
50
49
  fun Header(onBackHandler: (() -> Unit)? = null) {
@@ -77,13 +76,6 @@ fun Header(onBackHandler: (() -> Unit)? = null) {
77
76
  AppTheme.current.colors.background.surface
78
77
 
79
78
  if (options.headerType == HeaderType.None) return
80
-
81
- val titlePosition = options.titlePosition
82
- val titleColor = headerColor.tintIconColor
83
- .copy(alpha = if (inputSearchType == InputSearchType.Animated) 1f - animatedAlpha else 1f)
84
- // Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
85
- val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
86
-
87
79
  Box(
88
80
  Modifier.height(AppStatusBar.current + HEADER_HEIGHT.dp)
89
81
  .fillMaxWidth()
@@ -112,37 +104,17 @@ fun Header(onBackHandler: (() -> Unit)? = null) {
112
104
  Spacer(Modifier.width(Spacing.M))
113
105
  }
114
106
 
115
- if (centerTitle) {
116
- Spacer(Modifier.weight(1f))
117
- } else {
118
- HeaderContent(
119
- headerTitle = options.headerTitle,
120
- tintIconColor = titleColor,
121
- titlePosition = titlePosition,
122
- modifier = Modifier.weight(1f)
123
- )
124
- }
107
+ HeaderContent(
108
+ options.headerTitle,
109
+ headerColor.tintIconColor
110
+ .copy(alpha = if (inputSearchType == InputSearchType.Animated) 1f - animatedAlpha else 1f)
111
+ )
125
112
  Box(Modifier.onGloballyPositioned {
126
113
  if(headerRightWidthPx.intValue != it.size.width) headerRightWidthPx.intValue = it.size.width
127
114
  }){
128
115
  HeaderRight(options.headerRight, options.tintColor, headerColor)
129
116
  }
130
117
  }
131
- if (centerTitle) {
132
- Box(
133
- modifier = Modifier.height(HEADER_HEIGHT.dp)
134
- .fillMaxWidth()
135
- .padding(horizontal = Spacing.M),
136
- contentAlignment = Alignment.Center
137
- ) {
138
- HeaderContent(
139
- headerTitle = options.headerTitle,
140
- tintIconColor = titleColor,
141
- titlePosition = titlePosition,
142
- modifier = Modifier.fillMaxWidth()
143
- )
144
- }
145
- }
146
118
  VerticalShadow(opacity)
147
119
  }
148
120
  }
@@ -168,23 +140,15 @@ private fun BackButton(borderColor: Color, backgroundButton: Color, tintIconColo
168
140
  }
169
141
 
170
142
  @Composable
171
- fun HeaderContent(
172
- headerTitle: HeaderTitle,
173
- tintIconColor: Color,
174
- titlePosition: TitlePosition = TitlePosition.LEFT,
175
- modifier: Modifier = Modifier,
176
- ){
143
+ fun RowScope.HeaderContent(headerTitle: HeaderTitle, tintIconColor: Color){
177
144
  Box(
178
- modifier = modifier,
179
- contentAlignment = if (titlePosition == TitlePosition.CENTER) Alignment.Center else Alignment.CenterStart
145
+ Modifier.weight(1f)
180
146
  ) {
181
147
  when (headerTitle){
182
148
  is HeaderTitle.Default -> {
183
149
  HeaderTitle(
184
150
  title = headerTitle.title,
185
- color = tintIconColor,
186
- modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) 0.5f else 1f),
187
- textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
151
+ color = tintIconColor
188
152
  )
189
153
  }
190
154
  is HeaderTitle.Journey -> {}
@@ -16,13 +16,11 @@ import vn.momo.kits.modifier.setAutomationId
16
16
  fun HeaderTitle(
17
17
  title: String = "",
18
18
  color: Color? = null,
19
- modifier: Modifier = Modifier.fillMaxWidth(),
20
- textAlign: TextAlign = TextAlign.Start,
21
19
  ) {
22
20
  Text(
23
- modifier = modifier.zIndex(1f).setAutomationId("title_navigation_header"),
21
+ modifier = Modifier.fillMaxWidth().zIndex(1f).setAutomationId("title_navigation_header"),
24
22
  text = title,
25
- textAlign = textAlign,
23
+ textAlign = TextAlign.Start,
26
24
  style = Typography.actionSBold.copy(
27
25
  fontSize = 15.sp,
28
26
  lineHeight = 22.sp,
@@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.Color
11
11
  import io.github.alexzhirkevich.compottie.Compottie
12
12
  import io.github.alexzhirkevich.compottie.ExperimentalCompottieApi
13
13
  import io.github.alexzhirkevich.compottie.LottieCompositionSpec
14
+ import io.github.alexzhirkevich.compottie.animateLottieCompositionAsState
14
15
  import io.github.alexzhirkevich.compottie.dynamic.rememberLottieDynamicProperties
15
16
  import io.github.alexzhirkevich.compottie.rememberLottieComposition
16
17
  import io.github.alexzhirkevich.compottie.rememberLottiePainter
@@ -48,12 +49,21 @@ internal fun ComposeLottieAnimation(
48
49
  null
49
50
  }
50
51
 
52
+ val lottieState = animateLottieCompositionAsState(
53
+ composition = composition,
54
+ iterations = Compottie.IterateForever,
55
+ )
56
+
51
57
  Box(modifier.background(bgColor ?: Color.Transparent)) {
52
58
  Image(
53
59
  painter = rememberLottiePainter(
54
60
  composition = composition,
55
- iterations = Compottie.IterateForever,
61
+ progress = lottieState::value,
56
62
  dynamicProperties = dynamicProperties,
63
+ enableExpressions = false,
64
+ expressionEngineFactory = { _, _ ->
65
+ error("Lottie JS expressions are disabled in MoMo kits")
66
+ },
57
67
  ),
58
68
  contentDescription = null,
59
69
  modifier = Modifier.fillMaxSize(),
@@ -0,0 +1,142 @@
1
+ package vn.momo.kits.components
2
+
3
+ import androidx.compose.foundation.layout.BoxScope
4
+ import androidx.compose.runtime.*
5
+ import androidx.compose.ui.Alignment
6
+ import androidx.compose.ui.Modifier
7
+ import androidx.compose.ui.graphics.Color
8
+ import androidx.compose.ui.layout.ContentScale
9
+ import androidx.compose.ui.unit.dp
10
+ import androidx.compose.ui.viewinterop.UIKitInteropProperties
11
+ import androidx.compose.ui.viewinterop.UIKitView
12
+ import cocoapods.SDWebImage.*
13
+ import kotlinx.cinterop.ExperimentalForeignApi
14
+ import platform.Foundation.NSURL
15
+ import platform.UIKit.UIImageView
16
+ import platform.UIKit.UIViewContentMode
17
+ import vn.momo.kits.const.AppTheme
18
+ import vn.momo.kits.platform.toUIColor
19
+
20
+
21
+ @OptIn(ExperimentalForeignApi::class)
22
+ private val sdImageOptions = SDWebImageRetryFailed or
23
+ SDWebImageContinueInBackground or
24
+ SDWebImageScaleDownLargeImages or
25
+ SDWebImageHighPriority
26
+
27
+ @OptIn(ExperimentalForeignApi::class)
28
+ @Composable
29
+ actual fun BoxScope.NativeImageWrapper(
30
+ urlToLoad: Any,
31
+ modifier: Modifier,
32
+ options: Options?,
33
+ loading: Boolean
34
+ ) {
35
+ val imageOptions = remember(options) {
36
+ options ?: Options()
37
+ }
38
+
39
+ var imageState by remember { mutableStateOf(ImageState.Loading) }
40
+ (urlToLoad as? String)?.let { urlString ->
41
+ val contentModeValue = remember(imageOptions.contentScale) {
42
+ imageOptions.contentScale.toUIViewContentMode()
43
+ }
44
+ key(urlString) {
45
+ val isGif = isGifImage(urlString)
46
+ val imageView: UIImageView = remember {
47
+ if (isGif) {
48
+ SDAnimatedImageView().apply {
49
+ shouldCustomLoopCount = false
50
+ shouldIncrementalLoad = true
51
+ }
52
+ } else {
53
+ UIImageView()
54
+ }
55
+ }
56
+
57
+
58
+ val loadImage: (Color?) -> Unit = { tint ->
59
+ imageState = ImageState.Loading
60
+ val context = tint?.let {
61
+ mapOf<Any?, Any?>(
62
+ SDWebImageContextImageTransformer to
63
+ SDImageTintTransformer.transformerWithColor(it.toUIColor()),
64
+ )
65
+ }
66
+ imageView.sd_setImageWithURL(
67
+ NSURL.URLWithString(urlString),
68
+ placeholderImage = null,
69
+ options = sdImageOptions,
70
+ context = context,
71
+ progress = null,
72
+ completed = { image, error, _, _ ->
73
+ imageState = if (image != null && error == null) {
74
+ ImageState.Success
75
+ } else {
76
+ ImageState.Error
77
+ }
78
+ },
79
+ )
80
+ }
81
+
82
+
83
+ val appliedTint = remember { mutableStateOf(imageOptions.tint) }
84
+
85
+
86
+ DisposableEffect(Unit) {
87
+ onDispose { imageView.sd_cancelCurrentImageLoad() }
88
+ }
89
+
90
+ LaunchedEffect(Unit) {
91
+ appliedTint.value = imageOptions.tint
92
+ loadImage(imageOptions.tint)
93
+ }
94
+
95
+ UIKitView(
96
+ modifier = Modifier.matchParentSize(),
97
+ properties = UIKitInteropProperties(),
98
+ factory = {
99
+ imageView.apply {
100
+ this.contentMode = contentModeValue
101
+ setAlpha(imageOptions.alpha.toDouble())
102
+ translatesAutoresizingMaskIntoConstraints = true
103
+ sd_imageTransition = SDWebImageTransition.fadeTransitionWithDuration(0.3)
104
+ }
105
+ },
106
+ update = { view ->
107
+ view.contentMode = contentModeValue
108
+ view.setAlpha(imageOptions.alpha.toDouble())
109
+
110
+ if (appliedTint.value != imageOptions.tint) {
111
+ appliedTint.value = imageOptions.tint
112
+ loadImage(imageOptions.tint)
113
+ }
114
+ },
115
+ )
116
+ }
117
+ }
118
+ when (imageState) {
119
+ ImageState.Loading -> if (loading) Skeleton(cornerRadius = options?.skeletonCornerRadius ?: 0.dp)
120
+ ImageState.Error -> {
121
+ val theme = AppTheme.current
122
+ Icon(
123
+ source = "media_fail",
124
+ color = theme.colors.text.disable,
125
+ modifier = Modifier.align(Alignment.Center)
126
+ )
127
+ }
128
+
129
+ ImageState.Success -> {}
130
+ }
131
+
132
+ }
133
+
134
+ private fun ContentScale.toUIViewContentMode(): UIViewContentMode = when (this) {
135
+ ContentScale.Crop, ContentScale.FillWidth, ContentScale.FillHeight ->
136
+ UIViewContentMode.UIViewContentModeScaleAspectFill
137
+
138
+ ContentScale.Fit, ContentScale.Inside -> UIViewContentMode.UIViewContentModeScaleAspectFit
139
+ ContentScale.FillBounds -> UIViewContentMode.UIViewContentModeScaleToFill
140
+ ContentScale.None -> UIViewContentMode.UIViewContentModeCenter
141
+ else -> UIViewContentMode.UIViewContentModeScaleAspectFill
142
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict />
5
+ </plist>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict />
5
+ </plist>
@@ -13,7 +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
+ compottie = "2.1.0"
17
17
  androidx-activity = "1.9.1"
18
18
  androidx-appcompat = "1.7.0"
19
19
  material = "1.10.0"
@@ -21,6 +21,7 @@ maxapi = "0.1.1"
21
21
  vanniktechMavenPublish = "0.34.0"
22
22
  kits = "0.159.1-beta.7"
23
23
  nativemaxapi = "0.0.6"
24
+ coilGif = "2.6.0"
24
25
 
25
26
  [libraries]
26
27
  ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor-version" }
@@ -37,9 +38,20 @@ navigation-multiplatform = { module = "org.jetbrains.androidx.navigation:navigat
37
38
  coil-multiplatform-core = { module = "io.coil-kt.coil3:coil-core", version.ref = "coil3-multiplatform" }
38
39
  coil-multiplatform-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3-multiplatform" }
39
40
  coil-multiplatform-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3-multiplatform" }
41
+ coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3-multiplatform" }
42
+ coil-giff = { module = "io.coil-kt:coil-gif", version.ref = "coilGif" }
43
+ coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil3-multiplatform" }
44
+ coil-http = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil3-multiplatform" }
40
45
  r8 = { module = "com.android.tools:r8", version.ref = "r8" }
41
46
  airbnb-lottie = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "airbnb-lottie" }
42
- compottie = { module = "io.github.alexzhirkevich:compottie", version.ref = "compottie" }
47
+ # AI-GENERATED START
48
+ # Depend on compottie-core (not the `compottie` umbrella). The umbrella drags in the
49
+ # separate `keight` umbrella artifact, which collides with `keight-core` on the
50
+ # `findRoot__at__ScriptRuntime` symbol (duplicate symbol at the iOS `ld` link of
51
+ # shared.framework). compottie-core pulls only `keight-core` (which declares
52
+ # keight.js.JsAny, so the earlier unlinked-symbol error stays fixed too).
53
+ compottie = { module = "io.github.alexzhirkevich:compottie-core", version.ref = "compottie" }
54
+ # AI-GENERATED END
43
55
  native-max-api = { group = "vn.momo.maxapi", name = "NativeMaxApi", version.ref = "nativemaxapi" }
44
56
  kits = { module = "vn.momo.kits:kits", version.ref = "kits" }
45
57
  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.161.2-beta.9
21
+ version=0.161.2-nativeimage.1
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
package/local.properties CHANGED
@@ -4,5 +4,5 @@
4
4
  # Location of the SDK. This is only used by Gradle.
5
5
  # For customization when using a Version Control System, please read the
6
6
  # header note.
7
- #Mon Dec 22 10:07:29 ICT 2025
8
- sdk.dir=/Users/phuc/Library/Android/sdk
7
+ #Tue Jun 17 09:25:26 ICT 2025
8
+ sdk.dir=/Users/thanhle/Library/Android/sdk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.161.2-beta.9-debug",
3
+ "version": "0.161.2-nativeimage.1-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},
@@ -1,32 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(yarn react-native:*)",
5
- "Read(//Users/phuc/.claude/skills/ai-skill-generator/**)",
6
- "Bash(python3 scripts/install_skill.py --update)",
7
- "Bash(cd /Users/phuc/momo-kits/.claude/skills/design-system-kits && mkdir -p .claude/skills/design-system-kits && mv SKILL.md README.md references .claude/skills/design-system-kits/)",
8
- "Bash(mv .claude/skills/design-system-kits/* . && rmdir .claude/skills/design-system-kits && rmdir .claude/skills)",
9
- "Bash(python3 scripts/migrate_screen.py --dry-run sample/shared/src/commonMain/kotlin/screens/ScreenUsage.kt)",
10
- "Bash(python3 scripts/migrate_screen.py --dry-run sample/shared/src/commonMain/kotlin/screens/AnimatedHeaderUsage.kt sample/shared/src/commonMain/kotlin/screens/SnackBarScreenUsage.kt 2>&1)",
11
- "Bash(python3 scripts/migrate_screen.py --dry-run sample/shared/src/commonMain/kotlin/screens/SnackBarScreenUsage.kt)",
12
- "Bash(cp sample/shared/src/commonMain/kotlin/screens/SnackBarScreenUsage.kt /tmp/SnackBarScreenUsage.kt.orig && python3 scripts/migrate_screen.py sample/shared/src/commonMain/kotlin/screens/SnackBarScreenUsage.kt)",
13
- "Bash(cp -r /Users/phuc/.claude/skills/migrate-screen-to-navigation-container /Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container)",
14
- "Bash(mkdir -p /Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/scripts && mv /Users/phuc/momo-native-kits-2/momo-native-kits/scripts/migrate_screen.py /Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/scripts/migrate_screen.py)",
15
- "Bash(python3 .claude/skills/migrate-screen-to-navigation-container/scripts/generate_eval_cases.py)",
16
- "Bash(SKILL=\".claude/skills/migrate-screen-to-navigation-container\" && python3 $SKILL/scripts/migrate_screen.py $SKILL/eval/input/ 2>&1 | tail -20)",
17
- "Bash(SKILL=\".claude/skills/migrate-screen-to-navigation-container\" && cp -r $SKILL/eval/input $SKILL/eval/output && python3 $SKILL/scripts/migrate_screen.py $SKILL/eval/output/ 2>&1 | tail -20)",
18
- "Bash(SKILL=\".claude/skills/migrate-screen-to-navigation-container\" && cp -r $SKILL/eval/input $SKILL/eval/output && python3 $SKILL/scripts/migrate_screen.py $SKILL/eval/output/ 2>&1 | tail -25)",
19
- "Bash(SKILL=\".claude/skills/migrate-screen-to-navigation-container\" && cp -r \"$SKILL/eval/input\" \"$SKILL/eval/output\" && python3 \"$SKILL/scripts/migrate_screen.py\" \"$SKILL/eval/output/\" 2>&1 | tail -25)",
20
- "Bash(SKILL=\"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container\"\ncp -r \"$SKILL/eval/input\" \"$SKILL/eval/output\"\npython3 \"$SKILL/scripts/migrate_screen.py\" \"$SKILL/eval/output/\" 2>&1 | tail -25)",
21
- "Bash(cd \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container\" && python3 scripts/eval_migration.py 2>&1 | tail -80)",
22
- "Bash(cd \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container\" && python3 scripts/eval_migration.py 2>&1 | head -30)",
23
- "Bash(SKILL=\"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container\" && rm -rf \"$SKILL/eval/output\" && cp -r \"$SKILL/eval/input\" \"$SKILL/eval/output\" && python3 \"$SKILL/scripts/migrate_screen.py\" \"$SKILL/eval/output/\" 2>&1 | tail -5)",
24
- "Bash(SKILL=\"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container\"; rm -rf \"${SKILL}/eval/output\"; cp -r \"${SKILL}/eval/input\" \"${SKILL}/eval/output\" && python3 \"${SKILL}/scripts/migrate_screen.py\" \"${SKILL}/eval/output/\" 2>&1 | tail -5)",
25
- "Bash(rm -rf \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/eval/output\" && cp -r \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/eval/input\" \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/eval/output\" && python3 \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/scripts/migrate_screen.py\" \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/eval/output/\" 2>&1 | tail -5)",
26
- "Bash(python3 \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/scripts/eval_migration.py\" 2>&1 | head -35)",
27
- "Bash(python3 \"/Users/phuc/momo-native-kits-2/momo-native-kits/.claude/skills/migrate-screen-to-navigation-container/scripts/eval_migration.py\" 2>&1 | grep -E \"^=|Files eval|Passed|Failed|A0[0-9]|Per-Group|Failures\" | head -40)",
28
- "Bash(find /Users/phuc/momo-kits -name \"*.kt\" 2>/dev/null | head -20)",
29
- "Bash(find /Users/phuc -name \"*PaymentConfirmScreenFull*\" 2>/dev/null | head -10)"
30
- ]
31
- }
32
- }