@momo-kits/native-kits 0.160.8-debug → 0.160.9-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/build.gradle.kts +9 -3
- package/compose/build.gradle.kts.backup +8 -2
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +7 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +62 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +9 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +12 -4
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +33 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +7 -1
- package/gradle/libs.versions.toml +2 -0
- package/gradle.properties +1 -1
- package/ios/StatusBarTap/StatusBarTap.h +13 -0
- package/ios/StatusBarTap/StatusBarTap.m +75 -0
- package/ios/native-kits.podspec +2 -1
- package/package.json +1 -1
- package/settings.gradle.kts +15 -3
package/compose/build.gradle.kts
CHANGED
|
@@ -30,7 +30,7 @@ kotlin {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
val fwName = gitlabArtifactId
|
|
33
|
-
val iosTargets = listOf(
|
|
33
|
+
val iosTargets = listOf(iosArm64(), iosSimulatorArm64())
|
|
34
34
|
iosTargets.forEach {
|
|
35
35
|
it.binaries.framework {
|
|
36
36
|
baseName = fwName
|
|
@@ -40,7 +40,7 @@ kotlin {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
cocoapods {
|
|
43
|
-
version = "0.160.
|
|
43
|
+
version = "0.160.9-debug"
|
|
44
44
|
summary = "IOS Shared module"
|
|
45
45
|
homepage = "https://momo.vn"
|
|
46
46
|
ios.deploymentTarget = "15.0"
|
|
@@ -73,6 +73,7 @@ kotlin {
|
|
|
73
73
|
implementation(libs.jetbrains.serialization.json)
|
|
74
74
|
implementation(libs.kotlinx.datetime)
|
|
75
75
|
api(libs.native.max.api)
|
|
76
|
+
implementation(libs.compottie)
|
|
76
77
|
}
|
|
77
78
|
androidMain.dependencies {
|
|
78
79
|
implementation(libs.ktor.client.okhttp)
|
|
@@ -108,12 +109,17 @@ repositories {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
mavenCentral()
|
|
112
|
+
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
|
111
113
|
maven {
|
|
112
114
|
url = uri("https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven")
|
|
113
115
|
credentials {username = "download_packages"; password = "gldt-bjDqLpU_sPcHDuXau2ws" }
|
|
114
116
|
}
|
|
115
117
|
maven {
|
|
116
|
-
url = uri("
|
|
118
|
+
url = uri("https://artifacts.mservice.com.vn/repository/maven-momo-app/")
|
|
119
|
+
credentials {
|
|
120
|
+
username = "viewer"
|
|
121
|
+
password = "viewer"
|
|
122
|
+
}
|
|
117
123
|
isAllowInsecureProtocol = true
|
|
118
124
|
}
|
|
119
125
|
}
|
|
@@ -30,7 +30,7 @@ kotlin {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
val fwName = gitlabArtifactId
|
|
33
|
-
val iosTargets = listOf(
|
|
33
|
+
val iosTargets = listOf(iosArm64(), iosSimulatorArm64())
|
|
34
34
|
iosTargets.forEach {
|
|
35
35
|
it.binaries.framework {
|
|
36
36
|
baseName = fwName
|
|
@@ -73,6 +73,7 @@ kotlin {
|
|
|
73
73
|
implementation(libs.jetbrains.serialization.json)
|
|
74
74
|
implementation(libs.kotlinx.datetime)
|
|
75
75
|
api(libs.native.max.api)
|
|
76
|
+
implementation(libs.compottie)
|
|
76
77
|
}
|
|
77
78
|
androidMain.dependencies {
|
|
78
79
|
implementation(libs.ktor.client.okhttp)
|
|
@@ -108,12 +109,17 @@ repositories {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
mavenCentral()
|
|
112
|
+
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
|
111
113
|
maven {
|
|
112
114
|
url = uri("https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven")
|
|
113
115
|
credentials {username = "download_packages"; password = "gldt-bjDqLpU_sPcHDuXau2ws" }
|
|
114
116
|
}
|
|
115
117
|
maven {
|
|
116
|
-
url = uri("
|
|
118
|
+
url = uri("https://artifacts.mservice.com.vn/repository/maven-momo-app/")
|
|
119
|
+
credentials {
|
|
120
|
+
username = "viewer"
|
|
121
|
+
password = "viewer"
|
|
122
|
+
}
|
|
117
123
|
isAllowInsecureProtocol = true
|
|
118
124
|
}
|
|
119
125
|
}
|
package/compose/compose.podspec
CHANGED
|
@@ -78,8 +78,14 @@ actual fun LottieAnimation(
|
|
|
78
78
|
tintColor: Color?,
|
|
79
79
|
bgColor: Color?,
|
|
80
80
|
placedAsOverlay: Boolean,
|
|
81
|
-
modifier: Modifier
|
|
81
|
+
modifier: Modifier,
|
|
82
|
+
useCompose: Boolean
|
|
82
83
|
) {
|
|
84
|
+
if (useCompose) {
|
|
85
|
+
ComposeLottieAnimation(path, tintColor, bgColor, modifier)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
val json = readJson(path)
|
|
84
90
|
|
|
85
91
|
if (json.isEmpty()) {
|
|
@@ -150,6 +150,10 @@ internal fun StackScreen(
|
|
|
150
150
|
val footerHeightPx = remember { mutableIntStateOf(0) }
|
|
151
151
|
val headerRightWidthPx = remember { mutableIntStateOf(0) }
|
|
152
152
|
|
|
153
|
+
if (navigation.options.scrollData.scrollToTopCallback != null) {
|
|
154
|
+
RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
|
|
155
|
+
}
|
|
156
|
+
|
|
153
157
|
BackHandler(true) { navigator.onBackSafe() }
|
|
154
158
|
|
|
155
159
|
CompositionLocalProvider(
|
|
@@ -160,15 +164,16 @@ internal fun StackScreen(
|
|
|
160
164
|
LocalFooterHeightPx provides footerHeightPx,
|
|
161
165
|
LocalHeaderRightWidthPx provides headerRightWidthPx
|
|
162
166
|
) {
|
|
163
|
-
Box(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
Box(
|
|
168
|
+
Modifier
|
|
169
|
+
.fillMaxSize()
|
|
170
|
+
.background(options.backgroundColor ?: AppTheme.current.colors.background.default)
|
|
171
|
+
.conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
|
|
172
|
+
hideKeyboardOnTap()
|
|
173
|
+
}
|
|
174
|
+
.conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
|
|
175
|
+
imePadding()
|
|
176
|
+
}
|
|
172
177
|
) {
|
|
173
178
|
Box(Modifier.zIndex(1f)) {
|
|
174
179
|
HeaderBackground()
|
|
@@ -178,7 +183,7 @@ internal fun StackScreen(
|
|
|
178
183
|
Header(onBackHandler)
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
Column
|
|
186
|
+
Column(Modifier.zIndex(6f)) {
|
|
182
187
|
SearchAnimated(isScrollInProgress = scrollInProcess)
|
|
183
188
|
}
|
|
184
189
|
|
|
@@ -190,7 +195,7 @@ internal fun StackScreen(
|
|
|
190
195
|
FooterContent()
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
Box(Modifier.zIndex(7f)){
|
|
198
|
+
Box(Modifier.zIndex(7f)) {
|
|
194
199
|
FloatingContent()
|
|
195
200
|
}
|
|
196
201
|
|
|
@@ -200,15 +205,15 @@ internal fun StackScreen(
|
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
@Composable
|
|
203
|
-
fun FloatingContent(){
|
|
208
|
+
fun FloatingContent() {
|
|
204
209
|
val options = LocalOptions.current
|
|
205
210
|
val density = LocalDensity.current
|
|
206
211
|
val footerHeightPx = LocalFooterHeightPx.current
|
|
207
212
|
val scrollState = LocalScrollState.current
|
|
208
213
|
|
|
209
214
|
val fabProps = options.floatingButtonProps ?: return
|
|
210
|
-
val bottomPadding = fabProps.bottom
|
|
211
|
-
|
|
215
|
+
val bottomPadding = fabProps.bottom
|
|
216
|
+
?: (Spacing.M + if (options.footerComponent != null) with(density) { footerHeightPx.intValue.toDp() } else 0.dp)
|
|
212
217
|
|
|
213
218
|
FloatingButton(
|
|
214
219
|
scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
|
|
@@ -224,17 +229,19 @@ fun FloatingContent(){
|
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
@Composable
|
|
227
|
-
fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
232
|
+
fun ColumnScope.MainContent(content: @Composable () -> Unit) {
|
|
228
233
|
val options = LocalOptions.current
|
|
229
234
|
val inputSearchType = getInputSearchType(options)
|
|
230
235
|
val density = LocalDensity.current
|
|
231
236
|
val scrollState = LocalScrollState.current
|
|
232
237
|
|
|
233
|
-
Spacer(
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
Spacer(
|
|
239
|
+
Modifier.height(
|
|
240
|
+
if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
|
|
241
|
+
AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp
|
|
242
|
+
)
|
|
236
243
|
)
|
|
237
|
-
if (inputSearchType == InputSearchType.Animated){
|
|
244
|
+
if (inputSearchType == InputSearchType.Animated) {
|
|
238
245
|
val scrollDp = with(density) { scrollState.value.toDp() }
|
|
239
246
|
|
|
240
247
|
val animatedTopPadding by animateDpAsState(
|
|
@@ -243,17 +250,18 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
|
243
250
|
)
|
|
244
251
|
Spacer(Modifier.height(animatedTopPadding))
|
|
245
252
|
}
|
|
246
|
-
Column
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
253
|
+
Column(
|
|
254
|
+
Modifier
|
|
255
|
+
.fillMaxWidth()
|
|
256
|
+
.weight(1f)
|
|
257
|
+
.conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
|
|
258
|
+
verticalScroll(scrollState)
|
|
259
|
+
}
|
|
252
260
|
) {
|
|
253
261
|
ScreenContent(content = content)
|
|
254
262
|
}
|
|
255
263
|
|
|
256
|
-
if (options.footerComponent != null){
|
|
264
|
+
if (options.footerComponent != null) {
|
|
257
265
|
val footerHeight = LocalFooterHeightPx.current
|
|
258
266
|
val footerHeightDp = with(density) { footerHeight.value.toDp() }
|
|
259
267
|
Spacer(Modifier.height(footerHeightDp))
|
|
@@ -261,17 +269,17 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
@Composable
|
|
264
|
-
fun ScreenContent(content: @Composable () -> Unit){
|
|
272
|
+
fun ScreenContent(content: @Composable () -> Unit) {
|
|
265
273
|
val scrollState = LocalScrollState.current
|
|
266
274
|
val options = LocalOptions.current
|
|
267
275
|
|
|
268
|
-
if (options.headerType is HeaderType.Animated){
|
|
276
|
+
if (options.headerType is HeaderType.Animated) {
|
|
269
277
|
val animatedHeader = options.headerType
|
|
270
278
|
Box {
|
|
271
|
-
Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)){
|
|
279
|
+
Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)) {
|
|
272
280
|
animatedHeader.composable.invoke(scrollState.value)
|
|
273
281
|
}
|
|
274
|
-
Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)){
|
|
282
|
+
Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
|
|
275
283
|
content()
|
|
276
284
|
}
|
|
277
285
|
}
|
|
@@ -281,9 +289,9 @@ fun ScreenContent(content: @Composable () -> Unit){
|
|
|
281
289
|
}
|
|
282
290
|
|
|
283
291
|
@Composable
|
|
284
|
-
fun FooterContent(){
|
|
292
|
+
fun FooterContent() {
|
|
285
293
|
val options = LocalOptions.current
|
|
286
|
-
if (options.footerComponent != null){
|
|
294
|
+
if (options.footerComponent != null) {
|
|
287
295
|
val ime = WindowInsets.ime
|
|
288
296
|
val density = LocalDensity.current
|
|
289
297
|
val imeBottom = ime.getBottom(density)
|
|
@@ -295,11 +303,11 @@ fun FooterContent(){
|
|
|
295
303
|
}
|
|
296
304
|
|
|
297
305
|
@Composable
|
|
298
|
-
fun OverplayView(bottomTabIndex: Int, id: Int){
|
|
306
|
+
fun OverplayView(bottomTabIndex: Int, id: Int) {
|
|
299
307
|
val overplayType = OverplayComponentRegistry.getOverplayType()
|
|
300
308
|
|
|
301
309
|
if (overplayType != null) {
|
|
302
|
-
Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()){
|
|
310
|
+
Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()) {
|
|
303
311
|
if (bottomTabIndex != -1) return@Box
|
|
304
312
|
if (OverplayComponentRegistry.currentRootId() != id) return@Box
|
|
305
313
|
OverplayComponentRegistry.OverlayComponent()
|
|
@@ -322,22 +330,24 @@ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
|
|
|
322
330
|
Box(Modifier.onGloballyPositioned {
|
|
323
331
|
if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
|
|
324
332
|
}) {
|
|
325
|
-
Box(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
+
Box(
|
|
334
|
+
Modifier
|
|
335
|
+
.fillMaxWidth()
|
|
336
|
+
.background(AppTheme.current.colors.background.surface)
|
|
337
|
+
.conditional(IsShowBaseLineDebug) {
|
|
338
|
+
border(1.dp, Colors.blue_03)
|
|
339
|
+
}
|
|
340
|
+
.padding(top = Spacing.S, start = Spacing.M, end = Spacing.M, bottom = bottomPadding + Spacing.S)
|
|
341
|
+
) {
|
|
333
342
|
footerComponent.invoke()
|
|
334
343
|
}
|
|
335
344
|
|
|
336
|
-
Box(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
345
|
+
Box(
|
|
346
|
+
modifier = Modifier
|
|
347
|
+
.fillMaxWidth()
|
|
348
|
+
.height(6.dp)
|
|
349
|
+
.offset(x = 0.dp, y = (-6).dp)
|
|
350
|
+
.background(shadowBrush)
|
|
341
351
|
)
|
|
342
352
|
}
|
|
343
353
|
}
|
|
@@ -347,6 +357,7 @@ data class InputSearchLayoutParams(
|
|
|
347
357
|
val startPadding: Dp,
|
|
348
358
|
val endPadding: Dp
|
|
349
359
|
)
|
|
360
|
+
|
|
350
361
|
@Composable
|
|
351
362
|
fun SearchAnimated(
|
|
352
363
|
isScrollInProgress: Boolean
|
|
@@ -461,6 +472,7 @@ internal fun isKeyboardVisible(): Boolean {
|
|
|
461
472
|
val density = LocalDensity.current
|
|
462
473
|
return ime.getBottom(density) > 0
|
|
463
474
|
}
|
|
475
|
+
|
|
464
476
|
private fun quantize(value: Int, stepPx: Int): Int {
|
|
465
477
|
if (stepPx <= 1) return value
|
|
466
478
|
return (value / stepPx) * stepPx
|
|
@@ -540,13 +552,15 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
|
|
|
540
552
|
|
|
541
553
|
val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
|
|
542
554
|
|
|
543
|
-
internal fun getInputSearchType(options: NavigationOptions): InputSearchType{
|
|
555
|
+
internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
|
|
544
556
|
return when (val headerType = options.headerType) {
|
|
545
557
|
is HeaderType.DefaultOrExtended -> when {
|
|
546
558
|
headerType.inputSearchProps == null -> InputSearchType.None
|
|
547
559
|
headerType.useAnimated -> InputSearchType.Animated
|
|
548
560
|
else -> InputSearchType.Header
|
|
549
561
|
}
|
|
562
|
+
|
|
550
563
|
else -> InputSearchType.None
|
|
551
564
|
}
|
|
552
565
|
}
|
|
566
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package vn.momo.kits.platform
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.Image
|
|
4
|
+
import androidx.compose.foundation.background
|
|
5
|
+
import androidx.compose.foundation.layout.Box
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
7
|
+
import androidx.compose.runtime.Composable
|
|
8
|
+
import androidx.compose.runtime.getValue
|
|
9
|
+
import androidx.compose.ui.Modifier
|
|
10
|
+
import androidx.compose.ui.graphics.Color
|
|
11
|
+
import io.github.alexzhirkevich.compottie.Compottie
|
|
12
|
+
import io.github.alexzhirkevich.compottie.ExperimentalCompottieApi
|
|
13
|
+
import io.github.alexzhirkevich.compottie.LottieCompositionSpec
|
|
14
|
+
import io.github.alexzhirkevich.compottie.dynamic.rememberLottieDynamicProperties
|
|
15
|
+
import io.github.alexzhirkevich.compottie.rememberLottieComposition
|
|
16
|
+
import io.github.alexzhirkevich.compottie.rememberLottiePainter
|
|
17
|
+
import vn.momo.kits.utils.readJson
|
|
18
|
+
|
|
19
|
+
@OptIn(ExperimentalCompottieApi::class)
|
|
20
|
+
@Composable
|
|
21
|
+
internal fun ComposeLottieAnimation(
|
|
22
|
+
path: String,
|
|
23
|
+
tintColor: Color?,
|
|
24
|
+
bgColor: Color?,
|
|
25
|
+
modifier: Modifier,
|
|
26
|
+
) {
|
|
27
|
+
val json = readJson(path)
|
|
28
|
+
|
|
29
|
+
if (json.isEmpty()) {
|
|
30
|
+
Box(modifier.background(bgColor ?: Color.Transparent))
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
val composition by rememberLottieComposition(json) {
|
|
35
|
+
LottieCompositionSpec.JsonString(json)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Recolor only the fill/stroke color slots (matching the native lottie-ios per-keypath tint)
|
|
39
|
+
// instead of a global ColorFilter.tint, which would flatten a multicolor animation to a silhouette.
|
|
40
|
+
val dynamicProperties = if (tintColor != null) {
|
|
41
|
+
rememberLottieDynamicProperties(tintColor) {
|
|
42
|
+
shapeLayer("**") {
|
|
43
|
+
fill("**") { color { tintColor } }
|
|
44
|
+
stroke("**") { color { tintColor } }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Box(modifier.background(bgColor ?: Color.Transparent)) {
|
|
52
|
+
Image(
|
|
53
|
+
painter = rememberLottiePainter(
|
|
54
|
+
composition = composition,
|
|
55
|
+
iterations = Compottie.IterateForever,
|
|
56
|
+
dynamicProperties = dynamicProperties,
|
|
57
|
+
),
|
|
58
|
+
contentDescription = null,
|
|
59
|
+
modifier = Modifier.fillMaxSize(),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -44,13 +44,21 @@ fun supportsImePadding(): Boolean = when (val v = getOSVersion()) {
|
|
|
44
44
|
is OSVersion.IOS -> true
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @param useCompose when `true`, renders via the pure-Compose Compottie engine instead of the
|
|
49
|
+
* platform-native engine (airbnb-lottie on Android, lottie-ios on iOS). Defaults to `false` for
|
|
50
|
+
* backward compatibility. Note: when `useCompose = true`, `tintColor` recolors only the fill/stroke
|
|
51
|
+
* color slots (matching the native lottie-ios per-keypath tint), so gradient and text-layer colors
|
|
52
|
+
* are left untinted; `placedAsOverlay` is a no-op.
|
|
53
|
+
*/
|
|
47
54
|
@Composable
|
|
48
55
|
expect fun LottieAnimation(
|
|
49
56
|
path: String,
|
|
50
57
|
tintColor: Color? = null,
|
|
51
58
|
bgColor: Color? = null,
|
|
52
59
|
placedAsOverlay: Boolean = false,
|
|
53
|
-
modifier: Modifier = Modifier
|
|
60
|
+
modifier: Modifier = Modifier,
|
|
61
|
+
useCompose: Boolean = false
|
|
54
62
|
)
|
|
55
63
|
|
|
56
64
|
expect fun NativePaint.setColor(
|
|
@@ -24,13 +24,21 @@ fun getResource(name: String): DrawableResource {
|
|
|
24
24
|
@Composable
|
|
25
25
|
fun readJson(name: String): String {
|
|
26
26
|
val path = name.plus(".json")
|
|
27
|
+
val candidatePaths = listOf(
|
|
28
|
+
path,
|
|
29
|
+
"composeResources/vn.momo.compose.resources/$path",
|
|
30
|
+
)
|
|
27
31
|
|
|
28
32
|
val jsonContent by rememberState(path, { "" }) {
|
|
29
33
|
val cached = resourceCache.getOrPut(path) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
var content = ""
|
|
35
|
+
for (candidate in candidatePaths) {
|
|
36
|
+
try {
|
|
37
|
+
content = readResourceBytes(candidate).decodeToString()
|
|
38
|
+
break
|
|
39
|
+
} catch (_: Exception) {
|
|
40
|
+
// try next candidate
|
|
41
|
+
}
|
|
34
42
|
}
|
|
35
43
|
ResourceCache.JSON(content)
|
|
36
44
|
} as ResourceCache.JSON
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package vn.momo.kits.navigation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.DisposableEffect
|
|
5
|
+
import platform.Foundation.NSNotification
|
|
6
|
+
import platform.Foundation.NSNotificationCenter
|
|
7
|
+
import platform.Foundation.NSOperationQueue
|
|
8
|
+
import platform.darwin.NSObjectProtocol
|
|
9
|
+
|
|
10
|
+
private const val STATUS_BAR_TAPPED_NOTIFICATION = "statusBarSelected"
|
|
11
|
+
|
|
12
|
+
@Composable
|
|
13
|
+
internal actual fun RegisterScrollToTop(callback: (() -> Unit)?) {
|
|
14
|
+
val maxApi = LocalMaxApi.current
|
|
15
|
+
DisposableEffect(callback) {
|
|
16
|
+
val cb = callback
|
|
17
|
+
val token: NSObjectProtocol? = if (cb != null) {
|
|
18
|
+
NSNotificationCenter.defaultCenter.addObserverForName(
|
|
19
|
+
name = STATUS_BAR_TAPPED_NOTIFICATION,
|
|
20
|
+
`object` = null,
|
|
21
|
+
queue = NSOperationQueue.mainQueue,
|
|
22
|
+
) { _: NSNotification? ->
|
|
23
|
+
runCatching { cb() }.onFailure {
|
|
24
|
+
maxApi?.logFile("[ScrollToTop] callback threw: ${it.message}") {}
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else null
|
|
29
|
+
onDispose {
|
|
30
|
+
token?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -98,8 +98,14 @@ actual fun LottieAnimation(
|
|
|
98
98
|
tintColor: Color?,
|
|
99
99
|
bgColor: Color?,
|
|
100
100
|
placedAsOverlay: Boolean,
|
|
101
|
-
modifier: Modifier
|
|
101
|
+
modifier: Modifier,
|
|
102
|
+
useCompose: Boolean
|
|
102
103
|
) {
|
|
104
|
+
if (useCompose) {
|
|
105
|
+
ComposeLottieAnimation(path, tintColor, bgColor, modifier)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
var animation by remember { mutableStateOf<CompatibleAnimation?>(null) }
|
|
104
110
|
|
|
105
111
|
LaunchedEffect(Unit) {
|
|
@@ -13,6 +13,7 @@ androidGradlePlugin = "8.13.2"
|
|
|
13
13
|
r8 = "8.9.35"
|
|
14
14
|
kotlinx-datetime = "0.7.1"
|
|
15
15
|
airbnb-lottie = "5.2.0"
|
|
16
|
+
compottie = "2.2.0"
|
|
16
17
|
androidx-activity = "1.9.1"
|
|
17
18
|
androidx-appcompat = "1.7.0"
|
|
18
19
|
material = "1.10.0"
|
|
@@ -38,6 +39,7 @@ coil-multiplatform-compose = { module = "io.coil-kt.coil3:coil-compose", version
|
|
|
38
39
|
coil-multiplatform-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3-multiplatform" }
|
|
39
40
|
r8 = { module = "com.android.tools:r8", version.ref = "r8" }
|
|
40
41
|
airbnb-lottie = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "airbnb-lottie" }
|
|
42
|
+
compottie = { module = "io.github.alexzhirkevich:compottie", version.ref = "compottie" }
|
|
41
43
|
native-max-api = { group = "vn.momo.maxapi", name = "NativeMaxApi", version.ref = "nativemaxapi" }
|
|
42
44
|
kits = { module = "vn.momo.kits:kits", version.ref = "kits" }
|
|
43
45
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
package/gradle.properties
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// Posts `MoMoStatusBarTap.notificationName` on every status-bar tap.
|
|
6
|
+
/// The swizzle installs automatically at framework load (`+load`) — calling
|
|
7
|
+
/// `install` is a no-op kept for explicit/manual triggering.
|
|
8
|
+
@interface MoMoStatusBarTap : NSObject
|
|
9
|
+
@property (class, nonatomic, readonly) NSNotificationName notificationName;
|
|
10
|
+
+ (void)install;
|
|
11
|
+
@end
|
|
12
|
+
|
|
13
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
#import <objc/message.h>
|
|
3
|
+
#import <objc/runtime.h>
|
|
4
|
+
#import "StatusBarTap.h"
|
|
5
|
+
|
|
6
|
+
// Performs the actual swizzle once per process. Safe to call multiple times.
|
|
7
|
+
static void mm_installStatusBarTapSwizzleOnce(void) {
|
|
8
|
+
static dispatch_once_t onceToken;
|
|
9
|
+
dispatch_once(&onceToken, ^{
|
|
10
|
+
Class cls = [UIStatusBarManager class];
|
|
11
|
+
SEL originalSelector = NSSelectorFromString(@"handleTapAction:");
|
|
12
|
+
SEL swizzledSelector = NSSelectorFromString(@"mm_handleTapAction:");
|
|
13
|
+
|
|
14
|
+
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
|
|
15
|
+
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
|
|
16
|
+
if (!originalMethod || !swizzledMethod) return;
|
|
17
|
+
|
|
18
|
+
// Verify the original method matches the (id) -> void shape we expect.
|
|
19
|
+
// Encoding may include byte offsets (e.g. "v32@0:8@16") so we filter
|
|
20
|
+
// digits out before comparing.
|
|
21
|
+
const char *encPtr = method_getTypeEncoding(originalMethod);
|
|
22
|
+
if (encPtr) {
|
|
23
|
+
NSMutableString *enc = [NSMutableString string];
|
|
24
|
+
for (const char *p = encPtr; *p; p++) {
|
|
25
|
+
if (!(*p >= '0' && *p <= '9')) [enc appendFormat:@"%c", *p];
|
|
26
|
+
}
|
|
27
|
+
if (![enc isEqualToString:@"v@:@"]) return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
BOOL didAdd = class_addMethod(cls,
|
|
31
|
+
originalSelector,
|
|
32
|
+
method_getImplementation(swizzledMethod),
|
|
33
|
+
method_getTypeEncoding(swizzledMethod));
|
|
34
|
+
if (didAdd) {
|
|
35
|
+
class_replaceMethod(cls,
|
|
36
|
+
swizzledSelector,
|
|
37
|
+
method_getImplementation(originalMethod),
|
|
38
|
+
method_getTypeEncoding(originalMethod));
|
|
39
|
+
} else {
|
|
40
|
+
method_exchangeImplementations(originalMethod, swizzledMethod);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@implementation MoMoStatusBarTap
|
|
46
|
+
+ (NSNotificationName)notificationName { return @"statusBarSelected"; }
|
|
47
|
+
+ (void)install { mm_installStatusBarTapSwizzleOnce(); }
|
|
48
|
+
@end
|
|
49
|
+
|
|
50
|
+
// MARK: - Swizzle target on UIStatusBarManager
|
|
51
|
+
// `mm_handleTapAction:` is the replacement IMP. After the add/exchange,
|
|
52
|
+
// this selector points at the original UIKit IMP — calling it inside
|
|
53
|
+
// the body invokes UIKit's real handler so UIScrollView.scrollsToTop
|
|
54
|
+
// and any other side effects continue to work.
|
|
55
|
+
|
|
56
|
+
@interface UIStatusBarManager (MoMoStatusBarTap)
|
|
57
|
+
@end
|
|
58
|
+
|
|
59
|
+
@implementation UIStatusBarManager (MoMoStatusBarTap)
|
|
60
|
+
|
|
61
|
+
// Auto-install at framework load. Commented out so the swizzle stays
|
|
62
|
+
// opt-in via `[MoMoStatusBarTap install]`. Uncomment to revert to
|
|
63
|
+
// auto-installation before main().
|
|
64
|
+
//
|
|
65
|
+
// + (void)load {
|
|
66
|
+
// mm_installStatusBarTapSwizzleOnce();
|
|
67
|
+
// }
|
|
68
|
+
|
|
69
|
+
- (void)mm_handleTapAction:(id)action {
|
|
70
|
+
[self mm_handleTapAction:action];
|
|
71
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:MoMoStatusBarTap.notificationName
|
|
72
|
+
object:nil];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@end
|
package/ios/native-kits.podspec
CHANGED
|
@@ -10,7 +10,8 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.ios.deployment_target = '15.0'
|
|
11
11
|
s.swift_version = '5.0'
|
|
12
12
|
|
|
13
|
-
s.source_files = "**/*.swift"
|
|
13
|
+
s.source_files = "**/*.{swift,m,h}"
|
|
14
|
+
s.public_header_files = "StatusBarTap/*.h"
|
|
14
15
|
s.framework = 'SwiftUI', 'Combine'
|
|
15
16
|
s.dependency 'SDWebImageSwiftUI'
|
|
16
17
|
s.dependency 'lottie-ios'
|
package/package.json
CHANGED
package/settings.gradle.kts
CHANGED
|
@@ -19,7 +19,11 @@ pluginManagement {
|
|
|
19
19
|
credentials {username = "download_packages"; password = "gldt-bjDqLpU_sPcHDuXau2ws" }
|
|
20
20
|
}
|
|
21
21
|
maven {
|
|
22
|
-
url = uri("
|
|
22
|
+
url = uri("https://artifacts.mservice.com.vn/repository/maven-momo-app/")
|
|
23
|
+
credentials {
|
|
24
|
+
username = "viewer"
|
|
25
|
+
password = "viewer"
|
|
26
|
+
}
|
|
23
27
|
isAllowInsecureProtocol = true
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -35,12 +39,20 @@ dependencyResolutionManagement {
|
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
mavenCentral()
|
|
42
|
+
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
|
38
43
|
maven {
|
|
39
44
|
url = uri("https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven")
|
|
40
|
-
credentials {
|
|
45
|
+
credentials {
|
|
46
|
+
username = "download_packages";
|
|
47
|
+
password = "gldt-bjDqLpU_sPcHDuXau2ws"
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
maven {
|
|
43
|
-
url = uri("
|
|
51
|
+
url = uri("https://artifacts.mservice.com.vn/repository/maven-momo-app/")
|
|
52
|
+
credentials {
|
|
53
|
+
username = "viewer"
|
|
54
|
+
password = "viewer"
|
|
55
|
+
}
|
|
44
56
|
isAllowInsecureProtocol = true
|
|
45
57
|
}
|
|
46
58
|
}
|