@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.
- package/compose/MoMoComposeKits.podspec +54 -0
- package/compose/build.gradle.kts +4 -1
- package/compose/build.gradle.kts.backup +3 -0
- package/compose/compose.podspec +5 -11
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +162 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +9 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +10 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +12 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +12 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +47 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +48 -152
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +1 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +1 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +22 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +178 -5
- package/gradle/libs.versions.toml +4 -4
- package/gradle.properties +1 -1
- package/ios/Popup/PopupDisplay.swift +1 -5
- package/local.properties +8 -0
- package/package.json +1 -1
|
@@ -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
|
package/compose/build.gradle.kts
CHANGED
|
@@ -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
|
|
package/compose/compose.podspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Pod::Spec.new do |spec|
|
|
2
2
|
spec.name = 'compose'
|
|
3
|
-
spec.version = '0.156.1-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
374
|
-
|
|
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
|
-
|
|
52
|
+
Image(
|
|
53
|
+
source = iconUrl,
|
|
63
54
|
modifier = Modifier.matchParentSize(),
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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()
|
|
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(
|
|
@@ -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
|
-
|
|
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
|
|
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 {
|
|
99
|
-
DynamicScreenRegistry.unregisterScreen(
|
|
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
|
|
126
|
-
|
|
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
|
|
137
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
7
|
+
kotlin = "2.3.0"
|
|
8
8
|
kSerialize = "1.7.1"
|
|
9
|
-
compose = "1.
|
|
10
|
-
navigation-multiplatform = "2.9.
|
|
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.
|
|
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
|
@@ -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 {
|
package/local.properties
ADDED
|
@@ -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
|