@momo-kits/native-kits 0.156.6-debug → 0.156.6-dialog.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.
@@ -0,0 +1,54 @@
1
+ Pod::Spec.new do |spec|
2
+ spec.name = 'MoMoComposeKits'
3
+ spec.version = '0.0.1'
4
+ spec.homepage = 'MoMoComposeKits'
5
+ spec.source = { :http=> ''}
6
+ spec.authors = 'M_SERVICE'
7
+ spec.license = ''
8
+ spec.summary = 'MoMoComposeKits'
9
+ spec.vendored_frameworks = 'build/cocoapods/framework/compose.framework'
10
+ spec.libraries = 'c++'
11
+ spec.ios.deployment_target = '13.0'
12
+ spec.dependency 'SDWebImage', '>= 5.19.1'
13
+
14
+ if !Dir.exist?('build/cocoapods/framework/compose.framework') || Dir.empty?('build/cocoapods/framework/compose.framework')
15
+ raise "
16
+
17
+ Kotlin framework 'compose' doesn't exist yet, so a proper Xcode project can't be generated.
18
+ 'pod install' should be executed after running ':generateDummyFramework' Gradle task:
19
+
20
+ ./gradlew :compose:generateDummyFramework
21
+
22
+ Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
23
+ end
24
+
25
+ spec.xcconfig = {
26
+ 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
27
+ }
28
+
29
+ spec.pod_target_xcconfig = {
30
+ 'KOTLIN_PROJECT_PATH' => ':compose',
31
+ 'PRODUCT_MODULE_NAME' => 'compose',
32
+ }
33
+
34
+ spec.script_phases = [
35
+ {
36
+ :name => 'Build MoMoComposeKits',
37
+ :execution_position => :before_compile,
38
+ :shell_path => '/bin/sh',
39
+ :script => <<-SCRIPT
40
+ if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
41
+ echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
42
+ exit 0
43
+ fi
44
+ set -ev
45
+ REPO_ROOT="$PODS_TARGET_SRCROOT"
46
+ "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
47
+ -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
48
+ -Pkotlin.native.cocoapods.archs="$ARCHS" \
49
+ -Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
50
+ SCRIPT
51
+ }
52
+ ]
53
+ spec.resources = ['build/compose/cocoapods/compose-resources']
54
+ end
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.156.6-debug"
43
+ version = "0.156.6-dialog.1-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -54,6 +54,9 @@ kotlin {
54
54
  moduleName = "Lottie"
55
55
  version = "4.4.3"
56
56
  extraOpts += listOf("-compiler-option", "-fmodules")
57
+ }
58
+ pod("SDWebImage") {
59
+ version = ">= 5.19.1"
57
60
  }
58
61
  }
59
62
 
@@ -54,6 +54,9 @@ kotlin {
54
54
  moduleName = "Lottie"
55
55
  version = "4.4.3"
56
56
  extraOpts += listOf("-compiler-option", "-fmodules")
57
+ }
58
+ pod("SDWebImage") {
59
+ version = ">= 5.19.1"
57
60
  }
58
61
  }
59
62
 
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.156.1-beta.2'
3
+ spec.version = '0.156.1-test.6'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -9,28 +9,22 @@ 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.19.1'
12
13
  spec.dependency 'lottie-ios', '4.4.3'
13
-
14
14
  if !Dir.exist?('build/cocoapods/framework/kits.framework') || Dir.empty?('build/cocoapods/framework/kits.framework')
15
15
  raise "
16
-
17
16
  Kotlin framework 'kits' doesn't exist yet, so a proper Xcode project can't be generated.
18
17
  'pod install' should be executed after running ':generateDummyFramework' Gradle task:
19
-
20
18
  ./gradlew :compose:generateDummyFramework
21
-
22
19
  Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
23
20
  end
24
-
25
21
  spec.xcconfig = {
26
22
  'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
27
23
  }
28
-
29
24
  spec.pod_target_xcconfig = {
30
25
  'KOTLIN_PROJECT_PATH' => ':compose',
31
26
  'PRODUCT_MODULE_NAME' => 'kits',
32
27
  }
33
-
34
28
  spec.script_phases = [
35
29
  {
36
30
  :name => 'Build compose',
@@ -38,8 +32,8 @@ Pod::Spec.new do |spec|
38
32
  :shell_path => '/bin/sh',
39
33
  :script => <<-SCRIPT
40
34
  if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
41
- echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
42
- exit 0
35
+ echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
36
+ exit 0
43
37
  fi
44
38
  set -ev
45
39
  REPO_ROOT="$PODS_TARGET_SRCROOT"
@@ -51,4 +45,4 @@ Pod::Spec.new do |spec|
51
45
  }
52
46
  ]
53
47
  spec.resources = ['build/compose/cocoapods/compose-resources']
54
- end
48
+ end
@@ -9,12 +9,10 @@ import android.os.Build
9
9
  import androidx.compose.foundation.layout.Box
10
10
  import androidx.compose.runtime.Composable
11
11
  import androidx.compose.runtime.getValue
12
+ import androidx.compose.runtime.rememberUpdatedState
12
13
  import androidx.compose.ui.Modifier
13
- import androidx.compose.ui.draw.drawBehind
14
14
  import androidx.compose.ui.graphics.Color
15
15
  import androidx.compose.ui.graphics.NativePaint
16
- import androidx.compose.ui.graphics.Paint
17
- import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
18
16
  import androidx.compose.ui.graphics.toArgb
19
17
  import androidx.compose.ui.platform.LocalConfiguration
20
18
  import androidx.compose.ui.unit.Dp
@@ -26,6 +24,23 @@ import com.airbnb.lottie.compose.LottieConstants
26
24
  import com.airbnb.lottie.compose.rememberLottieComposition
27
25
  import com.airbnb.lottie.compose.rememberLottieDynamicProperties
28
26
  import com.airbnb.lottie.compose.rememberLottieDynamicProperty
27
+ import android.graphics.Matrix
28
+ import android.widget.ImageView
29
+ import androidx.compose.foundation.layout.fillMaxSize
30
+ import androidx.compose.ui.Alignment
31
+ import androidx.compose.ui.layout.ContentScale
32
+ import androidx.compose.ui.viewinterop.AndroidView
33
+ import androidx.core.view.doOnLayout
34
+ import coil3.load
35
+ import coil3.request.crossfade
36
+ import coil3.size.ViewSizeResolver
37
+ import android.view.ViewGroup
38
+ import android.view.WindowManager
39
+ import androidx.compose.runtime.SideEffect
40
+ import androidx.compose.ui.platform.LocalView
41
+ import androidx.compose.ui.window.DialogWindowProvider
42
+ import androidx.core.view.WindowCompat
43
+ import vn.momo.kits.components.Options
29
44
  import vn.momo.kits.const.AppNavigationBar
30
45
  import vn.momo.kits.const.AppStatusBar
31
46
  import vn.momo.kits.utils.readJson
@@ -66,8 +81,6 @@ actual fun getScreenHeight(): Dp {
66
81
  return getScreenDimensions().height.dp + if (getAndroidBuildVersion() >= 35) 0.dp else AppStatusBar.current + AppNavigationBar.current
67
82
  }
68
83
 
69
- actual fun getAndroidBuildVersion(): Int = Build.VERSION.SDK_INT
70
-
71
84
  @Composable
72
85
  actual fun LottieAnimation(
73
86
  path: String,
@@ -111,3 +124,147 @@ actual fun NativePaint.setColor(color: Color){
111
124
  this.color = color.toArgb()
112
125
  }
113
126
 
127
+ actual fun getAndroidBuildVersion(): Int = Build.VERSION.SDK_INT
128
+
129
+ @Composable
130
+ actual fun NativeImage(
131
+ source: Any,
132
+ options: Options,
133
+ loading: Boolean,
134
+ onLoading: () -> Unit,
135
+ onSuccess: () -> Unit,
136
+ onError: () -> Unit,
137
+ ) {
138
+ val currentOptions by rememberUpdatedState(options)
139
+
140
+ AndroidView(
141
+ modifier = Modifier.fillMaxSize(),
142
+ factory = { ctx ->
143
+ ImageView(ctx).apply {
144
+ adjustViewBounds = false
145
+
146
+ addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
147
+ (v as ImageView).apply {
148
+ applyOrDefer(currentOptions.contentScale, currentOptions.alignment)
149
+ colorFilter = currentOptions.tintColor?.let {
150
+ PorterDuffColorFilter(it.toArgb(), PorterDuff.Mode.SRC_ATOP)
151
+ }
152
+ }
153
+ }
154
+ }
155
+ },
156
+ update = { imageView ->
157
+ imageView.alpha = currentOptions.alpha.coerceIn(0f, 1f)
158
+ imageView.applyOrDefer(currentOptions.contentScale, currentOptions.alignment)
159
+ imageView.colorFilter = currentOptions.tintColor?.let {
160
+ PorterDuffColorFilter(it.toArgb(), PorterDuff.Mode.SRC_ATOP)
161
+ }
162
+
163
+ imageView.load(source) {
164
+ crossfade(loading)
165
+ size(ViewSizeResolver(imageView))
166
+ listener(
167
+ onStart = { onLoading() },
168
+ onSuccess = { _, _ ->
169
+ imageView.applyOrDefer(currentOptions.contentScale, currentOptions.alignment)
170
+ imageView.colorFilter = currentOptions.tintColor?.let {
171
+ PorterDuffColorFilter(it.toArgb(), PorterDuff.Mode.SRC_ATOP)
172
+ }
173
+ onSuccess()
174
+ },
175
+ onError = { _, _ -> onError() }
176
+ )
177
+ }
178
+ }
179
+ )
180
+ }
181
+ private fun ImageView.applyMatrixInternal(
182
+ contentScale: ContentScale,
183
+ alignment: Alignment
184
+ ): Boolean {
185
+ val d = drawable ?: return false
186
+
187
+ val vw = width - paddingLeft - paddingRight
188
+ val vh = height - paddingTop - paddingBottom
189
+ if (vw <= 0 || vh <= 0) return false
190
+
191
+ val dw = (d.intrinsicWidth).takeIf { it > 0 } ?: return false
192
+ val dh = (d.intrinsicHeight).takeIf { it > 0 } ?: return false
193
+
194
+ val sxFit = vw.toFloat() / dw.toFloat()
195
+ val syFit = vh.toFloat() / dh.toFloat()
196
+
197
+ val (sx, sy) = when (contentScale) {
198
+ ContentScale.Crop -> maxOf(sxFit, syFit).let { it to it }
199
+ ContentScale.Fit -> minOf(sxFit, syFit).let { it to it }
200
+ ContentScale.FillWidth -> sxFit to sxFit
201
+ ContentScale.FillHeight -> syFit to syFit
202
+ ContentScale.FillBounds -> sxFit to syFit
203
+ ContentScale.Inside -> minOf(1f, minOf(sxFit, syFit)).let { it to it }
204
+ ContentScale.None -> 1f to 1f
205
+ else -> maxOf(sxFit, syFit).let { it to it }
206
+ }
207
+
208
+ val (bx, by) = when (alignment) {
209
+ Alignment.TopStart -> -1f to -1f
210
+ Alignment.TopCenter -> 0f to -1f
211
+ Alignment.TopEnd -> 1f to -1f
212
+ Alignment.CenterStart -> -1f to 0f
213
+ Alignment.Center -> 0f to 0f
214
+ Alignment.CenterEnd -> 1f to 0f
215
+ Alignment.BottomStart -> -1f to 1f
216
+ Alignment.BottomCenter -> 0f to 1f
217
+ Alignment.BottomEnd -> 1f to 1f
218
+ else -> 0f to 0f
219
+ }
220
+
221
+ val scaledW = dw * sx
222
+ val scaledH = dh * sy
223
+
224
+ val tx = (vw - scaledW) * (bx + 1f) / 2f
225
+ val ty = (vh - scaledH) * (by + 1f) / 2f
226
+
227
+ if (scaleType != ImageView.ScaleType.MATRIX) {
228
+ scaleType = ImageView.ScaleType.MATRIX
229
+ }
230
+
231
+ val m = imageMatrix ?: Matrix()
232
+ m.reset()
233
+ m.setScale(sx, sy)
234
+ m.postTranslate(tx, ty)
235
+ imageMatrix = m
236
+ return true
237
+ }
238
+
239
+ private fun ImageView.applyOrDefer(contentScale: ContentScale, alignment: Alignment) {
240
+ if (!applyMatrixInternal(contentScale, alignment)) {
241
+ doOnLayout { if (!applyMatrixInternal(contentScale, alignment)) post { applyMatrixInternal(contentScale, alignment) } }
242
+ }
243
+ }
244
+
245
+ @Composable
246
+ actual fun ConfigureDialogWindow(decorFitsSystemWindows: Boolean) {
247
+ val view = LocalView.current
248
+ if (!view.isInEditMode) {
249
+ SideEffect {
250
+ // In Compose, LocalView.current inside a Dialog is the DialogLayout
251
+ // which itself implements DialogWindowProvider.
252
+ val window = (view as? DialogWindowProvider)?.window
253
+ ?: (view.parent as? DialogWindowProvider)?.window
254
+ ?: return@SideEffect
255
+
256
+ WindowCompat.setDecorFitsSystemWindows(window, decorFitsSystemWindows)
257
+
258
+ if (!decorFitsSystemWindows) {
259
+ // Extend layout behind the status bar and navigation bar so the
260
+ // dialog scrim covers the full screen uniformly.
261
+ window.setLayout(
262
+ ViewGroup.LayoutParams.MATCH_PARENT,
263
+ ViewGroup.LayoutParams.MATCH_PARENT
264
+ )
265
+ window.statusBarColor = android.graphics.Color.TRANSPARENT
266
+ window.navigationBarColor = android.graphics.Color.TRANSPARENT
267
+ }
268
+ }
269
+ }
270
+ }
@@ -370,9 +370,14 @@ private class LiteScreenHeaderPolicy(
370
370
  maxWidth = (realConstraints.maxWidth * (1 - scrollPercent)).toInt()
371
371
  .coerceAtLeast(minWidth)
372
372
  )
373
- } else realConstraints.copy(
374
- maxWidth = realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spacing12 * 2
375
- )
373
+ } else {
374
+ var spaceConsumed = 0
375
+ if (backIconPlaceable.safeWidth != 0) spaceConsumed += backIconPlaceable.safeWidth + spacing12
376
+ if (headerRightPlaceable.safeWidth != 0) spaceConsumed += headerRightPlaceable.safeWidth + spacing12
377
+ realConstraints.copy(
378
+ maxWidth = realConstraints.maxWidth - spaceConsumed
379
+ )
380
+ }
376
381
  val inputSearchPlaceable = measurables.find { it.layoutId == HeaderId.INPUT_SEARCH_ID }
377
382
  ?.measure(inputSearchConstraints)
378
383
  val titlePlaceable = measurables.find { it.layoutId == HeaderId.TITLE_ID }?.measure(
@@ -516,7 +521,7 @@ private fun LiteInputSearch(
516
521
 
517
522
  val isShowBtnText by remember(inputSearchProps.isShowBtnText, inputSearchProps.btnText) {
518
523
  derivedStateOf {
519
- inputSearchProps.isShowBtnText && inputSearchProps.btnText.isNullOrEmpty()
524
+ inputSearchProps.isShowBtnText && !inputSearchProps.btnText.isNullOrEmpty()
520
525
  }
521
526
  }
522
527
  val inputFieldStyle = remember(theme) {
@@ -5,17 +5,14 @@ import androidx.compose.foundation.layout.Box
5
5
  import androidx.compose.foundation.layout.size
6
6
  import androidx.compose.runtime.Composable
7
7
  import androidx.compose.runtime.remember
8
+ import androidx.compose.ui.Alignment
8
9
  import androidx.compose.ui.Modifier
9
10
  import androidx.compose.ui.graphics.Color
10
- import androidx.compose.ui.graphics.ColorFilter
11
11
  import androidx.compose.ui.layout.ContentScale
12
12
  import androidx.compose.ui.semantics.contentDescription
13
13
  import androidx.compose.ui.semantics.semantics
14
14
  import androidx.compose.ui.unit.Dp
15
15
  import androidx.compose.ui.unit.dp
16
- import coil3.compose.AsyncImage
17
- import coil3.compose.LocalPlatformContext
18
- import coil3.request.ImageRequest
19
16
  import vn.momo.kits.application.IsShowBaseLineDebug
20
17
  import vn.momo.kits.const.AppTheme
21
18
  import vn.momo.kits.const.Colors
@@ -30,9 +27,6 @@ fun Icon(
30
27
  color: Color? = AppTheme.current.colors.text.default,
31
28
  modifier: Modifier = Modifier,
32
29
  ) {
33
- // decode image without downscaling it
34
- val context = LocalPlatformContext.current
35
-
36
30
  val iconUrl = remember(source) {
37
31
  if (source.contains("https")) {
38
32
  source
@@ -41,14 +35,10 @@ fun Icon(
41
35
  }
42
36
  }
43
37
 
44
- val iconColor = remember(color) {
38
+ val iconColor = remember(source, color) {
45
39
  if (noThemeIcons.contains(source)) null else color
46
40
  }
47
41
 
48
- val colorFilter = remember(iconColor) {
49
- iconColor?.let { ColorFilter.tint(it) }
50
- }
51
-
52
42
  val contentDesc = remember(iconUrl) { "img|$iconUrl" }
53
43
 
54
44
  Box(
@@ -59,18 +49,15 @@ fun Icon(
59
49
  }
60
50
  .semantics { contentDescription = contentDesc }
61
51
  ) {
62
- AsyncImage(
52
+ Image(
53
+ source = iconUrl,
63
54
  modifier = Modifier.matchParentSize(),
64
- model = ImageRequest.Builder(context)
65
- .data(iconUrl)
66
- .size(
67
- coil3.size.Dimension.Undefined,
68
- coil3.size.Dimension.Undefined
69
- )
70
- .build(),
71
- contentDescription = null,
72
- contentScale = ContentScale.Fit,
73
- colorFilter = colorFilter,
55
+ options = Options(
56
+ contentScale = ContentScale.Fit,
57
+ tintColor = iconColor,
58
+ alignment = Alignment.Center,
59
+ ),
60
+ loading = false,
74
61
  )
75
62
  }
76
63
  }
@@ -9,6 +9,8 @@ import androidx.compose.runtime.remember
9
9
  import androidx.compose.runtime.setValue
10
10
  import androidx.compose.ui.Alignment
11
11
  import androidx.compose.ui.Modifier
12
+ import androidx.compose.ui.draw.clipToBounds
13
+ import androidx.compose.ui.graphics.Color
12
14
  import androidx.compose.ui.graphics.ColorFilter
13
15
  import androidx.compose.ui.graphics.DefaultAlpha
14
16
  import androidx.compose.ui.graphics.painter.Painter
@@ -22,11 +24,12 @@ import vn.momo.kits.application.IsShowBaseLineDebug
22
24
  import vn.momo.kits.const.AppTheme
23
25
  import vn.momo.kits.const.Colors
24
26
  import vn.momo.kits.modifier.conditional
27
+ import vn.momo.kits.platform.NativeImage
25
28
 
26
29
  data class Options(
27
30
  val alignment: Alignment = Alignment.TopStart,
28
31
  val contentScale: ContentScale = ContentScale.Crop,
29
- val colorFilter: ColorFilter? = null,
32
+ val tintColor: Color? = null,
30
33
  val alpha: Float = DefaultAlpha,
31
34
  )
32
35
 
@@ -96,15 +99,14 @@ fun Image(
96
99
  options: Options? = null,
97
100
  loading: Boolean = true,
98
101
  ) {
99
- val imageOptions = remember(options) {
100
- options ?: Options()
101
- }
102
+ val imageOptions = remember(options) { options ?: Options() }
102
103
 
103
104
  var imageState by remember { mutableStateOf(ImageState.Loading) }
104
105
  val density = LocalDensity.current
105
106
 
106
107
  BoxWithConstraints(
107
108
  modifier = modifier
109
+ .clipToBounds()
108
110
  .conditional(IsShowBaseLineDebug) {
109
111
  border(1.dp, Colors.blue_03)
110
112
  }
@@ -113,7 +115,6 @@ fun Image(
113
115
  val urlToLoad = remember(source, maxWidth, maxHeight, density) {
114
116
  when (source) {
115
117
  is String -> {
116
- // Check cache first
117
118
  val cacheKey = "$source|${maxWidth}|${density.density}"
118
119
  urlCache[cacheKey] ?: run {
119
120
  val processedUrl = processImageUrl(source, maxWidth, density)
@@ -125,17 +126,13 @@ fun Image(
125
126
  }
126
127
  }
127
128
 
128
- AsyncImage(
129
- modifier = Modifier.matchParentSize(),
130
- model = urlToLoad,
131
- contentDescription = null,
132
- contentScale = imageOptions.contentScale,
133
- alignment = imageOptions.alignment,
134
- colorFilter = imageOptions.colorFilter,
135
- alpha = imageOptions.alpha,
129
+ NativeImage(
130
+ source = urlToLoad,
131
+ options = imageOptions,
132
+ loading = loading,
136
133
  onLoading = { imageState = ImageState.Loading },
137
134
  onSuccess = { imageState = ImageState.Success },
138
- onError = { imageState = ImageState.Error },
135
+ onError = { imageState = ImageState.Error },
139
136
  )
140
137
 
141
138
  when (imageState) {
@@ -148,7 +145,7 @@ fun Image(
148
145
  modifier = Modifier.align(Alignment.Center)
149
146
  )
150
147
  }
151
- ImageState.Success -> {}
148
+ ImageState.Success -> Unit
152
149
  }
153
150
  }
154
151
  }
@@ -49,9 +49,20 @@ import vn.momo.kits.utils.formatNumberToMoney
49
49
 
50
50
  class CustomConverter : VisualTransformation {
51
51
  override fun filter(text: AnnotatedString): TransformedText {
52
- if (text.text.isEmpty() || text.text == "0") {
52
+ if (text.text.isEmpty()) {
53
+ return TransformedText(
54
+ AnnotatedString("0"),
55
+ object : OffsetMapping {
56
+ override fun originalToTransformed(offset: Int): Int = 0
57
+ override fun transformedToOriginal(offset: Int): Int = 0
58
+ }
59
+ )
60
+ }
61
+
62
+ if (text.text == "0") {
53
63
  return TransformedText(AnnotatedString("0"), OffsetMapping.Identity)
54
64
  }
65
+
55
66
  val formattedText = formatNumberToMoney(text.text.toLong())
56
67
 
57
68
  return TransformedText(
@@ -112,7 +112,6 @@ internal fun BottomSheet(
112
112
  }
113
113
 
114
114
  DisposableEffect(Unit) {
115
- OverplayComponentRegistry.bindClose { closeEvent() }
116
115
  onDispose {
117
116
  onDismiss?.invoke()
118
117
  }
@@ -80,7 +80,6 @@ internal fun ModalScreen(
80
80
  }
81
81
 
82
82
  DisposableEffect(Unit) {
83
- OverplayComponentRegistry.bindClose { closeEvent() }
84
83
  onDispose {
85
84
  onDismiss?.invoke()
86
85
  }
@@ -18,8 +18,10 @@ import androidx.compose.runtime.remember
18
18
  import androidx.compose.runtime.staticCompositionLocalOf
19
19
  import androidx.compose.ui.unit.Dp
20
20
  import androidx.compose.ui.unit.dp
21
+ import androidx.compose.ui.window.DialogProperties
21
22
  import androidx.navigation.compose.NavHost
22
23
  import androidx.navigation.compose.composable
24
+ import androidx.navigation.compose.dialog
23
25
  import androidx.navigation.compose.rememberNavController
24
26
  import androidx.navigation.toRoute
25
27
  import vn.momo.kits.application.AppConfig
@@ -34,6 +36,7 @@ import vn.momo.kits.const.AppThemeController
34
36
  import vn.momo.kits.const.Theme
35
37
  import vn.momo.kits.const.ThemeAssets
36
38
  import vn.momo.kits.const.defaultTheme
39
+ import vn.momo.kits.platform.ConfigureDialogWindow
37
40
  import vn.momo.kits.utils.getAppStatusBarHeight
38
41
  import vn.momo.maxapi.IMaxApi
39
42
 
@@ -92,6 +95,7 @@ fun NavigationContainer(
92
95
  }
93
96
 
94
97
  NavHost(navController, startDestination = startDestination) {
98
+ // ── Stack screens (push/replace/reset) ──────────────────────────
95
99
  composable<DynamicScreenRoute>(
96
100
  enterTransition = {
97
101
  slideInHorizontally(
@@ -120,6 +124,7 @@ fun NavigationContainer(
120
124
  }
121
125
  }
122
126
 
127
+ // ── Present screens (vertical slide dialog) ──────────────────────
123
128
  composable<DynamicDialogRoute>(
124
129
  enterTransition = {
125
130
  slideInVertically (
@@ -147,6 +152,48 @@ fun NavigationContainer(
147
152
  )
148
153
  }
149
154
  }
155
+
156
+ // ── Bottom Sheet dialog ──────────────────────────────────────────
157
+ dialog<DynamicBottomSheetRoute>(
158
+ dialogProperties = DialogProperties(
159
+ dismissOnBackPress = false,
160
+ dismissOnClickOutside = false,
161
+ usePlatformDefaultWidth = false
162
+ )
163
+ ) { backStackEntry ->
164
+ val route = backStackEntry.toRoute<DynamicBottomSheetRoute>()
165
+ val screen = DynamicScreenRegistry.getScreen(route.id)
166
+ ConfigureDialogWindow()
167
+ screen?.content?.invoke()
168
+ }
169
+
170
+ // ── Modal dialog ─────────────────────────────────────────────────
171
+ dialog<DynamicModalRoute>(
172
+ dialogProperties = DialogProperties(
173
+ dismissOnBackPress = false,
174
+ dismissOnClickOutside = false,
175
+ usePlatformDefaultWidth = false,
176
+ )
177
+ ) { backStackEntry ->
178
+ val route = backStackEntry.toRoute<DynamicModalRoute>()
179
+ val screen = DynamicScreenRegistry.getScreen(route.id)
180
+ ConfigureDialogWindow()
181
+ screen?.content?.invoke()
182
+ }
183
+
184
+ // ── Snack Bar dialog ─────────────────────────────────────────────
185
+ dialog<DynamicSnackBarRoute>(
186
+ dialogProperties = DialogProperties(
187
+ dismissOnBackPress = false,
188
+ dismissOnClickOutside = false,
189
+ usePlatformDefaultWidth = false
190
+ )
191
+ ) { backStackEntry ->
192
+ val route = backStackEntry.toRoute<DynamicSnackBarRoute>()
193
+ val screen = DynamicScreenRegistry.getScreen(route.id)
194
+ ConfigureDialogWindow()
195
+ screen?.content?.invoke()
196
+ }
150
197
  }
151
198
  }
152
199
 
@@ -160,4 +207,3 @@ fun NavigationContainer(
160
207
  val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> {
161
208
  error("No MaxApi provided")
162
209
  }
163
-
@@ -3,8 +3,6 @@ package vn.momo.kits.navigation
3
3
  import androidx.compose.foundation.layout.Box
4
4
  import androidx.compose.foundation.layout.fillMaxSize
5
5
  import androidx.compose.runtime.Composable
6
- import androidx.compose.runtime.MutableState
7
- import androidx.compose.runtime.mutableStateOf
8
6
  import androidx.compose.runtime.staticCompositionLocalOf
9
7
  import androidx.compose.ui.Alignment
10
8
  import androidx.compose.ui.Modifier
@@ -72,31 +70,18 @@ class Navigator(
72
70
  fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
73
71
  scope.launch {
74
72
  repeat(count) {
75
- if (OverplayComponentRegistry.getOverplayType() == OverplayComponentType.SNACK_BAR){
76
- dismissScreen()
77
- OverplayComponentRegistry.hardClearAfterDismiss()
78
- }
79
- else if (OverplayComponentRegistry.isOverplayShowing()){
80
- dismissOverplay()
81
- } else {
82
- dismissScreen()
83
- }
73
+ dismissEntry()
84
74
  }
85
75
  callBack?.invoke()
86
76
  }
87
77
  }
88
- private suspend fun dismissOverplay(isDelay: Boolean = true) {
89
- OverplayComponentRegistry.clear()
90
- if (isDelay) delay(300L)
91
- OverplayComponentRegistry.hardClearAfterDismiss()
92
- }
93
78
 
94
- private suspend fun dismissScreen() {
95
- if (navController.previousBackStackEntry != null){
79
+ private suspend fun dismissEntry() {
80
+ if (navController.previousBackStackEntry != null) {
96
81
  navController.popBackStack()
97
82
  delay(300L)
98
- DynamicScreenRegistry.getLatestScreen()?.let { it1 ->
99
- DynamicScreenRegistry.unregisterScreen(it1.id)
83
+ DynamicScreenRegistry.getLatestScreen()?.let {
84
+ DynamicScreenRegistry.unregisterScreen(it.id)
100
85
  }
101
86
  } else {
102
87
  maxApi?.dismiss { }
@@ -122,8 +107,14 @@ class Navigator(
122
107
  barrierDismissible: Boolean = true,
123
108
  onDismiss: (() -> Unit)? = null
124
109
  ){
125
- val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
126
- OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.MODAL, false, barrierDismissible, onDismiss)
110
+ val route = DynamicScreenRegistry.register({
111
+ ModalScreen(
112
+ content = content,
113
+ barrierDismissible = barrierDismissible,
114
+ onDismiss = onDismiss
115
+ )
116
+ }, null)
117
+ navController.navigate(DynamicModalRoute(route.id))
127
118
  }
128
119
 
129
120
  fun showBottomSheet(
@@ -133,32 +124,42 @@ class Navigator(
133
124
  onDismiss: (() -> Unit)? = null,
134
125
  bottomSheetHeader: BottomHeader? = null
135
126
  ){
136
- val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
137
- OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.BOTTOM_SHEET, isSurface, barrierDismissible, onDismiss, bottomSheetHeader)
127
+ val route = DynamicScreenRegistry.register({
128
+ BottomSheet(
129
+ content = content,
130
+ header = bottomSheetHeader ?: Title(),
131
+ isSurface = isSurface,
132
+ barrierDismissible = barrierDismissible,
133
+ onDismiss = onDismiss
134
+ )
135
+ }, null)
136
+ navController.navigate(DynamicBottomSheetRoute(route.id))
138
137
  }
139
138
 
140
139
  fun showSnackBar(snackBar: SnackBar, onDismiss: (() -> Unit)? = null) {
141
- val id = DynamicScreenRegistry.getLatestScreen()?.id ?: -1
142
- scope.launch {
143
- OverplayComponentRegistry.registerOverplay(
144
- id = id,
145
- content = {
146
- SnackBar(snackBar, onDismiss)
147
- },
148
- type = OverplayComponentType.SNACK_BAR,
149
- onDismiss = onDismiss
150
- )
151
- }
140
+ val capturedSnackBar = snackBar
141
+ val capturedOnDismiss = onDismiss
142
+ val route = DynamicScreenRegistry.register({
143
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
144
+ SnackBar(capturedSnackBar, capturedOnDismiss)
145
+ }
146
+ }, null)
147
+ navController.navigate(DynamicSnackBarRoute(route.id))
152
148
  }
153
149
 
154
150
  fun hideSnackBar() {
155
151
  scope.launch {
156
- OverplayComponentRegistry.clear()
157
- delay(250L)
158
- OverplayComponentRegistry.hardClearAfterDismiss()
152
+ if (navController.previousBackStackEntry != null) {
153
+ navController.popBackStack()
154
+ delay(250L)
155
+ DynamicScreenRegistry.getLatestScreen()?.let {
156
+ DynamicScreenRegistry.unregisterScreen(it.id)
157
+ }
158
+ }
159
159
  }
160
160
  }
161
161
 
162
+
162
163
  fun dispose(){
163
164
  scope.cancel()
164
165
  }
@@ -174,6 +175,15 @@ data class DynamicScreenRoute(val id: Int)
174
175
  @Serializable
175
176
  data class DynamicDialogRoute(val id: Int)
176
177
 
178
+ @Serializable
179
+ data class DynamicBottomSheetRoute(val id: Int)
180
+
181
+ @Serializable
182
+ data class DynamicModalRoute(val id: Int)
183
+
184
+ @Serializable
185
+ data class DynamicSnackBarRoute(val id: Int)
186
+
177
187
  data class DynamicScreen(
178
188
  val id: Int,
179
189
  val content: @Composable () -> Unit,
@@ -215,117 +225,3 @@ object DynamicScreenRegistry {
215
225
  screens[id]?.options = options
216
226
  }
217
227
  }
218
-
219
-
220
- sealed class OverplayComponentParams {
221
- class Modal(
222
- val onDismiss: (() -> Unit)? = null,
223
- val barrierDismissible: Boolean = true
224
- ) : OverplayComponentParams()
225
-
226
- class BottomSheet(
227
- val isSurface: Boolean = false,
228
- val onDismiss: (() -> Unit)? = null,
229
- val barrierDismissible: Boolean = true,
230
- val bottomSheetHeader: BottomHeader? = null,
231
- ) : OverplayComponentParams()
232
-
233
- class SnackBar() : OverplayComponentParams()
234
- }
235
-
236
- data class OverplayComponent(
237
- val id: Int,
238
- val type: OverplayComponentType? = null,
239
- val content: @Composable () -> Unit,
240
- val params: OverplayComponentParams? = null
241
- )
242
-
243
- enum class OverplayComponentType {
244
- MODAL, BOTTOM_SHEET, SNACK_BAR
245
- }
246
-
247
- object OverplayComponentRegistry {
248
- private var currentOverlayComponent : MutableState<OverplayComponent?> = mutableStateOf(null)
249
- private var requestClose: (() -> Unit)? = null
250
- internal fun bindClose(handler: (() -> Unit)?) {
251
- requestClose = handler
252
- }
253
-
254
- fun registerOverplay(
255
- id: Int,
256
- content: @Composable () -> Unit,
257
- type: OverplayComponentType,
258
- isSurface: Boolean = false,
259
- barrierDismissible: Boolean = true,
260
- onDismiss: (() -> Unit)?,
261
- bottomSheetHeader: BottomHeader? = null,
262
- ){
263
- val params = when(type){
264
- OverplayComponentType.MODAL -> OverplayComponentParams.Modal(onDismiss, barrierDismissible)
265
- OverplayComponentType.BOTTOM_SHEET -> OverplayComponentParams.BottomSheet(isSurface, onDismiss, barrierDismissible, bottomSheetHeader)
266
- OverplayComponentType.SNACK_BAR -> OverplayComponentParams.SnackBar()
267
- }
268
-
269
- currentOverlayComponent.value = OverplayComponent(
270
- id = id,
271
- type = type,
272
- content = content,
273
- params = params
274
- )
275
- }
276
-
277
- fun isOverplayShowing(): Boolean = currentOverlayComponent.value != null
278
-
279
- fun getOverplayType(): OverplayComponentType? = currentOverlayComponent.value?.type
280
-
281
- fun currentRootId(): Int? = currentOverlayComponent.value?.id
282
-
283
- fun clear(){
284
- if (requestClose != null) {
285
- requestClose?.invoke()
286
- } else {
287
- currentOverlayComponent.value = null
288
- }
289
- }
290
-
291
- internal fun hardClearAfterDismiss() {
292
- currentOverlayComponent.value = null
293
- requestClose = null
294
- }
295
-
296
- @Composable
297
- fun OverlayComponent(){
298
- val overplay = currentOverlayComponent.value ?: return
299
-
300
- when (val params = overplay.params) {
301
- is OverplayComponentParams.BottomSheet -> {
302
- BottomSheet(
303
- content = overplay.content,
304
- header = params.bottomSheetHeader ?: Title(),
305
- isSurface = params.isSurface,
306
- barrierDismissible = params.barrierDismissible,
307
- onDismiss = params.onDismiss
308
- )
309
- }
310
-
311
- is OverplayComponentParams.Modal -> {
312
- ModalScreen(
313
- content = overplay.content,
314
- barrierDismissible = params.barrierDismissible,
315
- onDismiss = params.onDismiss
316
- )
317
- }
318
-
319
- is OverplayComponentParams.SnackBar -> {
320
- Box(
321
- modifier = Modifier.fillMaxSize(),
322
- contentAlignment = Alignment.BottomCenter
323
- ) {
324
- overplay.content()
325
- }
326
- }
327
-
328
- null -> {}
329
- }
330
- }
331
- }
@@ -138,8 +138,6 @@ internal fun StackScreen(
138
138
  Box(Modifier.zIndex(7f)){
139
139
  FloatingContent()
140
140
  }
141
-
142
- OverplayView(bottomTabIndex = bottomTabIndex, id = id)
143
141
  }
144
142
  }
145
143
  }
@@ -239,18 +237,7 @@ fun FooterContent(){
239
237
  }
240
238
  }
241
239
 
242
- @Composable
243
- fun OverplayView(bottomTabIndex: Int, id: Int){
244
- val overplayType = OverplayComponentRegistry.getOverplayType()
245
-
246
- if (overplayType != null) {
247
- Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()){
248
- if (bottomTabIndex != -1) return@Box
249
- if (OverplayComponentRegistry.currentRootId() != id) return@Box
250
- OverplayComponentRegistry.OverlayComponent()
251
- }
252
- }
253
- }
240
+
254
241
 
255
242
  @Composable
256
243
  fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
@@ -29,7 +29,7 @@ import vn.momo.kits.const.AppNavigationBar
29
29
  import vn.momo.kits.navigation.LocalFooterHeightPx
30
30
  import vn.momo.kits.navigation.LocalNavigator
31
31
  import vn.momo.kits.navigation.LocalOptions
32
- import vn.momo.kits.navigation.OverplayComponentRegistry
32
+
33
33
 
34
34
  sealed class SnackBar(open val duration: Long? = null) {
35
35
  data class Custom(
@@ -102,9 +102,6 @@ fun SnackBar(
102
102
  }
103
103
 
104
104
  DisposableEffect(Unit) {
105
- OverplayComponentRegistry.bindClose {
106
- closeEvent()
107
- }
108
105
  onDispose {
109
106
  onDismiss?.invoke()
110
107
  }
@@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
5
5
  import androidx.compose.ui.graphics.Color
6
6
  import androidx.compose.ui.graphics.NativePaint
7
7
  import androidx.compose.ui.unit.Dp
8
+ import vn.momo.kits.components.Options
8
9
 
9
10
  data class ScreenDimension(
10
11
  val width: Int,
@@ -39,4 +40,24 @@ expect fun LottieAnimation(
39
40
 
40
41
  expect fun NativePaint.setColor(
41
42
  color: Color = Color.Black
42
- )
43
+ )
44
+ @Composable
45
+ expect fun NativeImage(
46
+ source: Any,
47
+ options: Options,
48
+ loading: Boolean,
49
+ onLoading: () -> Unit = {},
50
+ onSuccess: () -> Unit = {},
51
+ onError: () -> Unit = {},
52
+ )
53
+
54
+ /**
55
+ * Configures the current Dialog window's system-bar behaviour.
56
+ *
57
+ * @param decorFitsSystemWindows When `false` (the default) the dialog window
58
+ * extends behind the status bar and navigation bar so that a full-screen
59
+ * scrim/background covers the entire display uniformly.
60
+ * Pass `true` to restore the default platform inset handling.
61
+ */
62
+ @Composable
63
+ expect fun ConfigureDialogWindow(decorFitsSystemWindows: Boolean = false)
@@ -12,26 +12,36 @@ import androidx.compose.runtime.mutableStateOf
12
12
  import androidx.compose.runtime.remember
13
13
  import androidx.compose.runtime.setValue
14
14
  import androidx.compose.ui.Modifier
15
- import androidx.compose.ui.draw.drawBehind
16
15
  import androidx.compose.ui.graphics.Color
17
16
  import androidx.compose.ui.graphics.NativePaint
18
- import androidx.compose.ui.graphics.Paint
19
- import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
20
17
  import androidx.compose.ui.graphics.toArgb
21
- import androidx.compose.ui.interop.UIKitView
22
18
  import androidx.compose.ui.unit.Dp
23
19
  import androidx.compose.ui.unit.dp
24
20
  import cocoapods.lottie_ios.CompatibleAnimation
25
21
  import cocoapods.lottie_ios.CompatibleAnimationKeypath
26
22
  import cocoapods.lottie_ios.CompatibleAnimationView
23
+ import androidx.compose.ui.Alignment
24
+ import androidx.compose.ui.ExperimentalComposeUiApi
25
+ import androidx.compose.ui.layout.ContentScale
26
+ import androidx.compose.ui.viewinterop.UIKitInteropProperties
27
+ import androidx.compose.ui.viewinterop.UIKitView
28
+ import cocoapods.SDWebImage.sd_setImageWithURL
27
29
  import kotlinx.cinterop.ExperimentalForeignApi
28
30
  import kotlinx.cinterop.get
29
31
  import kotlinx.cinterop.memScoped
32
+ import kotlinx.cinterop.useContents
30
33
  import org.jetbrains.skia.FilterBlurMode
31
34
  import org.jetbrains.skia.MaskFilter
35
+ import platform.CoreGraphics.CGRectMake
32
36
  import platform.Foundation.NSBundle
33
37
  import platform.UIKit.UIColor
38
+ import platform.UIKit.UIImageRenderingMode
34
39
  import platform.UIKit.UIScreen
40
+ import platform.Foundation.NSURL
41
+ import platform.UIKit.UIImageView
42
+ import platform.UIKit.UIViewContentMode
43
+ import platform.UIKit.UIView
44
+ import vn.momo.kits.components.Options
35
45
 
36
46
  actual fun getPlatformName(): String = "iOS"
37
47
 
@@ -149,4 +159,167 @@ fun Color.toUIColor(): UIColor {
149
159
 
150
160
  actual fun NativePaint.setColor(color: Color){
151
161
  this.color = color.toArgb()
152
- }
162
+ }
163
+ /**
164
+ * A container UIView that positions an inner UIImageView using the same
165
+ * scale + alignment math as the Android Matrix implementation.
166
+ */
167
+ @OptIn(ExperimentalForeignApi::class)
168
+ class ScaledAlignedImageView(
169
+ var contentScale: ContentScale,
170
+ var alignment: Alignment,
171
+ var imageTintColor: UIColor? = null,
172
+ ) : UIView(frame = CGRectMake(0.0, 0.0, 0.0, 0.0)) {
173
+
174
+ val imageView = UIImageView().apply {
175
+ backgroundColor = UIColor.clearColor()
176
+ clipsToBounds = false
177
+ contentMode = UIViewContentMode.UIViewContentModeScaleToFill
178
+ }
179
+
180
+ init {
181
+ clipsToBounds = true
182
+ addSubview(imageView)
183
+ }
184
+
185
+ fun applyTint() {
186
+ val img = imageView.image ?: return
187
+ val color = imageTintColor
188
+ if (color != null) {
189
+ imageView.tintColor = color
190
+ imageView.image = img.imageWithRenderingMode(UIImageRenderingMode.UIImageRenderingModeAlwaysTemplate)
191
+ } else {
192
+ imageView.tintColor = super.tintColor
193
+ imageView.image = img.imageWithRenderingMode(UIImageRenderingMode.UIImageRenderingModeAlwaysOriginal)
194
+ }
195
+ }
196
+
197
+ override fun layoutSubviews() {
198
+ super.layoutSubviews()
199
+ applyImageFrame()
200
+ }
201
+
202
+ @OptIn(ExperimentalForeignApi::class)
203
+ fun applyImageFrame() {
204
+ memScoped {
205
+ val img = imageView.image ?: return
206
+ val rect = this@ScaledAlignedImageView.bounds.ptr[0]
207
+ val vw: Double = rect.size.width
208
+ val vh: Double = rect.size.height
209
+ if (vw <= 0.0 || vh <= 0.0) return
210
+
211
+ val imgSize = img.size
212
+ val imgScale: Double = img.scale
213
+ val deviceScale: Double = UIScreen.mainScreen.scale
214
+ // Normalize: UIImage.size is in pts at img.scale, convert to dp-equivalent pts
215
+ // (img pixel count = img.size × img.scale → dp = pixels / deviceScale)
216
+ val dw: Double = (imgSize.useContents { width } * imgScale / deviceScale).takeIf { it > 0.0 } ?: return
217
+ val dh: Double = (imgSize.useContents { height } * imgScale / deviceScale).takeIf { it > 0.0 } ?: return
218
+
219
+ val sxFit: Double = vw / dw
220
+ val syFit: Double = vh / dh
221
+
222
+ val (sx: Double, sy: Double) = when (contentScale) {
223
+ ContentScale.Crop -> maxOf(sxFit, syFit).let { it to it }
224
+ ContentScale.Fit -> minOf(sxFit, syFit).let { it to it }
225
+ ContentScale.FillWidth -> sxFit to sxFit
226
+ ContentScale.FillHeight -> syFit to syFit
227
+ ContentScale.FillBounds -> sxFit to syFit
228
+ ContentScale.Inside -> minOf(1.0, minOf(sxFit, syFit)).let { it to it }
229
+ ContentScale.None -> 1.0 to 1.0
230
+ else -> maxOf(sxFit, syFit).let { it to it }
231
+ }
232
+
233
+ val (bx: Double, by: Double) = when (alignment) {
234
+ Alignment.TopStart -> -1.0 to -1.0
235
+ Alignment.TopCenter -> 0.0 to -1.0
236
+ Alignment.TopEnd -> 1.0 to -1.0
237
+ Alignment.CenterStart -> -1.0 to 0.0
238
+ Alignment.Center -> 0.0 to 0.0
239
+ Alignment.CenterEnd -> 1.0 to 0.0
240
+ Alignment.BottomStart -> -1.0 to 1.0
241
+ Alignment.BottomCenter -> 0.0 to 1.0
242
+ Alignment.BottomEnd -> 1.0 to 1.0
243
+ else -> 0.0 to 0.0
244
+ }
245
+
246
+ val scaledW: Double = dw * sx
247
+ val scaledH: Double = dh * sy
248
+ val tx: Double = (vw - scaledW) * (bx + 1.0) / 2.0
249
+ val ty: Double = (vh - scaledH) * (by + 1.0) / 2.0
250
+
251
+ imageView.setFrame(CGRectMake(tx, ty, scaledW, scaledH))
252
+ }
253
+ }
254
+ }
255
+
256
+ @OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class)
257
+ @Composable
258
+ actual fun NativeImage(
259
+ source: Any,
260
+ options: Options,
261
+ loading: Boolean,
262
+ onLoading: () -> Unit,
263
+ onSuccess: () -> Unit,
264
+ onError: () -> Unit,
265
+ ) {
266
+ UIKitView(
267
+ modifier = Modifier.fillMaxSize(),
268
+ factory = {
269
+ ScaledAlignedImageView(
270
+ contentScale = options.contentScale,
271
+ alignment = options.alignment,
272
+ imageTintColor = options.tintColor?.toUIColor(),
273
+ ).apply {
274
+ backgroundColor = UIColor.clearColor()
275
+ alpha = options.alpha.toDouble().coerceIn(0.0, 1.0)
276
+ }
277
+ },
278
+ update = { container ->
279
+ container.contentScale = options.contentScale
280
+ container.alignment = options.alignment
281
+ container.imageTintColor = options.tintColor?.toUIColor()
282
+ container.alpha = options.alpha.toDouble().coerceIn(0.0, 1.0)
283
+ container.applyImageFrame()
284
+
285
+ val url = (source as? String)?.let { NSURL(string = it) } ?: run {
286
+ container.imageView.image = null
287
+ onError()
288
+ return@UIKitView
289
+ }
290
+
291
+ try {
292
+ onLoading()
293
+ container.imageView.sd_setImageWithURL(
294
+ url = url,
295
+ placeholderImage = null,
296
+ options = 0u,
297
+ completed = { _, error, _, _ ->
298
+ if (error == null) {
299
+ container.applyTint()
300
+ container.applyImageFrame()
301
+ onSuccess()
302
+ } else {
303
+ onError()
304
+ }
305
+ }
306
+ )
307
+ true
308
+ } catch (_: Throwable) {
309
+ onError()
310
+ }
311
+ },
312
+ properties = UIKitInteropProperties(
313
+ placedAsOverlay = true,
314
+ // Disable all touch handling on the interop host view so that
315
+ // Compose click handlers on parent composables (e.g. Icon, BackButton)
316
+ // receive touches correctly on iOS.
317
+ interactionMode = null,
318
+ )
319
+ )
320
+ }
321
+
322
+ @Composable
323
+ actual fun ConfigureDialogWindow(decorFitsSystemWindows: Boolean) {
324
+ // iOS: dialogs already render behind system bars — no additional setup needed.
325
+ }
@@ -4,13 +4,13 @@ android-minSdk = "24"
4
4
  android-targetSdk = "35"
5
5
  ktor-version = "3.0.0-rc-1"
6
6
  coroutines = "1.8.1"
7
- kotlin = "2.1.21"
7
+ kotlin = "2.3.0"
8
8
  kSerialize = "1.7.1"
9
- compose = "1.8.1"
10
- navigation-multiplatform = "2.9.0-beta02"
9
+ compose = "1.10.1"
10
+ navigation-multiplatform = "2.9.1"
11
11
  coil3-multiplatform = "3.0.0-alpha10"
12
12
  androidGradlePlugin = "8.6.0"
13
- r8 = "8.9.35"
13
+ r8 = "8.13.19"
14
14
  kotlinx-datetime = "0.7.1"
15
15
  airbnb-lottie = "5.2.0"
16
16
  androidx-activity = "1.9.1"
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.156.6
21
+ version=0.156.6-dialog.1
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -113,11 +113,7 @@ public struct PopupDisplay: View {
113
113
  }
114
114
 
115
115
  VStack(spacing: 0) {
116
- if(url.isEmpty) {
117
- Icon(source: "media_fail")
118
- .frame(width: .infinity, height: 184)
119
- }
120
- else {
116
+ if(!url.isEmpty) {
121
117
  WebImage(url: URL(string: url), isAnimating: .constant(true))
122
118
  .resizable()
123
119
  .placeholder {
@@ -0,0 +1,8 @@
1
+ ## This file must *NOT* be checked into Version Control Systems,
2
+ # as it contains information specific to your local configuration.
3
+ #
4
+ # Location of the SDK. This is only used by Gradle.
5
+ # For customization when using a Version Control System, please read the
6
+ # header note.
7
+ #Thu Oct 02 17:53:13 ICT 2025
8
+ sdk.dir=/Users/phuc/Library/Android/sdk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.156.6-debug",
3
+ "version": "0.156.6-dialog.1-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},