@momo-kits/native-kits 0.160.1-beta.9-debug → 0.160.1-container.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/.claude/settings.local.json +7 -0
- 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/application/LiteScreen.kt +324 -117
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +1 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +11 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +14 -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 +68 -71
- 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/local.properties +8 -0
- package/package.json +1 -1
- package/settings.gradle.kts +15 -3
|
@@ -8,6 +8,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
|
|
|
8
8
|
import androidx.compose.animation.core.tween
|
|
9
9
|
import androidx.compose.foundation.background
|
|
10
10
|
import androidx.compose.foundation.border
|
|
11
|
+
import androidx.compose.ui.draw.alpha
|
|
11
12
|
import androidx.compose.foundation.clickable
|
|
12
13
|
import androidx.compose.foundation.layout.Arrangement
|
|
13
14
|
import androidx.compose.foundation.layout.Box
|
|
@@ -131,7 +132,9 @@ fun InputOTP(
|
|
|
131
132
|
},
|
|
132
133
|
decorationBox = { innerTextField ->
|
|
133
134
|
Box {
|
|
134
|
-
Box(
|
|
135
|
+
Box(
|
|
136
|
+
modifier = Modifier.fillMaxWidth().height(56.dp).alpha(0f)
|
|
137
|
+
) { innerTextField() }
|
|
135
138
|
if (floatingValue.isNotEmpty()) {
|
|
136
139
|
Box(
|
|
137
140
|
modifier = Modifier.wrapContentSize()
|
|
@@ -3,6 +3,9 @@ package vn.momo.kits.navigation
|
|
|
3
3
|
import androidx.compose.animation.*
|
|
4
4
|
import androidx.compose.animation.core.tween
|
|
5
5
|
import androidx.compose.runtime.*
|
|
6
|
+
// AI-GENERATED START: import rememberSaveable for stable screen id across nav back-stack save/restore
|
|
7
|
+
import androidx.compose.runtime.saveable.rememberSaveable
|
|
8
|
+
// AI-GENERATED END: import rememberSaveable for stable screen id across nav back-stack save/restore
|
|
6
9
|
import androidx.compose.ui.unit.Dp
|
|
7
10
|
import androidx.navigation.compose.NavHost
|
|
8
11
|
import androidx.navigation.compose.composable
|
|
@@ -49,7 +52,11 @@ fun NavigationContainer(
|
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
// AI-GENERATED START: keep initial screen id stable with restored nav back stack and refresh its live content
|
|
56
|
+
val screenId = rememberSaveable { DynamicScreenRegistry.nextId() }
|
|
57
|
+
DynamicScreenRegistry.bind(screenId, initialScreenName, initialScreen, options)
|
|
58
|
+
val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
|
|
59
|
+
// AI-GENERATED END: keep initial screen id stable with restored nav back stack and refresh its live content
|
|
53
60
|
|
|
54
61
|
ProvideNavigationEventDispatcherOwner {
|
|
55
62
|
CompositionLocalProvider(
|
|
@@ -132,6 +139,9 @@ fun NavigationContainer(
|
|
|
132
139
|
DisposableEffect(Unit) {
|
|
133
140
|
onDispose {
|
|
134
141
|
navigator.dispose()
|
|
142
|
+
// AI-GENERATED START: unregister initial screen on dispose to avoid leaking a stale registry entry
|
|
143
|
+
DynamicScreenRegistry.unregisterScreen(screenId)
|
|
144
|
+
// AI-GENERATED END: unregister initial screen on dispose to avoid leaking a stale registry entry
|
|
135
145
|
}
|
|
136
146
|
}
|
|
137
147
|
}
|
|
@@ -196,6 +196,20 @@ object DynamicScreenRegistry {
|
|
|
196
196
|
return DynamicScreenRoute(id)
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
// AI-GENERATED START: stable-id registration so a saveable screen id stays in sync with restored nav back stack
|
|
200
|
+
fun nextId(): Int = idCounter++
|
|
201
|
+
|
|
202
|
+
fun bind(id: Int, screenName: String, content: @Composable () -> Unit, options: NavigationOptions?) {
|
|
203
|
+
screens[id] = DynamicScreen(
|
|
204
|
+
id = id,
|
|
205
|
+
name = screenName,
|
|
206
|
+
content = content,
|
|
207
|
+
options = options
|
|
208
|
+
)
|
|
209
|
+
if (id >= idCounter) idCounter = id + 1 // keep counter ahead after process-death restore
|
|
210
|
+
}
|
|
211
|
+
// AI-GENERATED END: stable-id registration so a saveable screen id stays in sync with restored nav back stack
|
|
212
|
+
|
|
199
213
|
fun unregisterScreen(id: Int) {
|
|
200
214
|
screens.remove(id)
|
|
201
215
|
}
|
|
@@ -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,29 +164,26 @@ 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()
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
Box(Modifier.zIndex(1.5f)) {
|
|
178
|
-
AnimatedHeaderImage()
|
|
179
|
-
}
|
|
180
|
-
|
|
181
182
|
Box(Modifier.zIndex(5f)) {
|
|
182
183
|
Header(onBackHandler)
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
Column
|
|
186
|
+
Column(Modifier.zIndex(6f)) {
|
|
186
187
|
SearchAnimated(isScrollInProgress = scrollInProcess)
|
|
187
188
|
}
|
|
188
189
|
|
|
@@ -194,7 +195,7 @@ internal fun StackScreen(
|
|
|
194
195
|
FooterContent()
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
Box(Modifier.zIndex(7f)){
|
|
198
|
+
Box(Modifier.zIndex(7f)) {
|
|
198
199
|
FloatingContent()
|
|
199
200
|
}
|
|
200
201
|
|
|
@@ -204,15 +205,15 @@ internal fun StackScreen(
|
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
@Composable
|
|
207
|
-
fun FloatingContent(){
|
|
208
|
+
fun FloatingContent() {
|
|
208
209
|
val options = LocalOptions.current
|
|
209
210
|
val density = LocalDensity.current
|
|
210
211
|
val footerHeightPx = LocalFooterHeightPx.current
|
|
211
212
|
val scrollState = LocalScrollState.current
|
|
212
213
|
|
|
213
214
|
val fabProps = options.floatingButtonProps ?: return
|
|
214
|
-
val bottomPadding = fabProps.bottom
|
|
215
|
-
|
|
215
|
+
val bottomPadding = fabProps.bottom
|
|
216
|
+
?: (Spacing.M + if (options.footerComponent != null) with(density) { footerHeightPx.intValue.toDp() } else 0.dp)
|
|
216
217
|
|
|
217
218
|
FloatingButton(
|
|
218
219
|
scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
|
|
@@ -228,17 +229,19 @@ fun FloatingContent(){
|
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
@Composable
|
|
231
|
-
fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
232
|
+
fun ColumnScope.MainContent(content: @Composable () -> Unit) {
|
|
232
233
|
val options = LocalOptions.current
|
|
233
234
|
val inputSearchType = getInputSearchType(options)
|
|
234
235
|
val density = LocalDensity.current
|
|
235
236
|
val scrollState = LocalScrollState.current
|
|
236
237
|
|
|
237
|
-
Spacer(
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
)
|
|
240
243
|
)
|
|
241
|
-
if (inputSearchType == InputSearchType.Animated){
|
|
244
|
+
if (inputSearchType == InputSearchType.Animated) {
|
|
242
245
|
val scrollDp = with(density) { scrollState.value.toDp() }
|
|
243
246
|
|
|
244
247
|
val animatedTopPadding by animateDpAsState(
|
|
@@ -247,17 +250,18 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
|
247
250
|
)
|
|
248
251
|
Spacer(Modifier.height(animatedTopPadding))
|
|
249
252
|
}
|
|
250
|
-
Column
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
Column(
|
|
254
|
+
Modifier
|
|
255
|
+
.fillMaxWidth()
|
|
256
|
+
.weight(1f)
|
|
257
|
+
.conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
|
|
258
|
+
verticalScroll(scrollState)
|
|
259
|
+
}
|
|
256
260
|
) {
|
|
257
261
|
ScreenContent(content = content)
|
|
258
262
|
}
|
|
259
263
|
|
|
260
|
-
if (options.footerComponent != null){
|
|
264
|
+
if (options.footerComponent != null) {
|
|
261
265
|
val footerHeight = LocalFooterHeightPx.current
|
|
262
266
|
val footerHeightDp = with(density) { footerHeight.value.toDp() }
|
|
263
267
|
Spacer(Modifier.height(footerHeightDp))
|
|
@@ -265,14 +269,19 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
@Composable
|
|
268
|
-
fun ScreenContent(content: @Composable () -> Unit){
|
|
272
|
+
fun ScreenContent(content: @Composable () -> Unit) {
|
|
273
|
+
val scrollState = LocalScrollState.current
|
|
269
274
|
val options = LocalOptions.current
|
|
270
275
|
|
|
271
|
-
if (options.headerType is HeaderType.Animated){
|
|
276
|
+
if (options.headerType is HeaderType.Animated) {
|
|
272
277
|
val animatedHeader = options.headerType
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
Box {
|
|
279
|
+
Box(Modifier.fillMaxWidth().aspectRatio(animatedHeader.aspectRatio.value)) {
|
|
280
|
+
animatedHeader.composable.invoke(scrollState.value)
|
|
281
|
+
}
|
|
282
|
+
Box(Modifier.offset(x = 0.dp, y = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
|
|
283
|
+
content()
|
|
284
|
+
}
|
|
276
285
|
}
|
|
277
286
|
} else {
|
|
278
287
|
content()
|
|
@@ -280,27 +289,9 @@ fun ScreenContent(content: @Composable () -> Unit){
|
|
|
280
289
|
}
|
|
281
290
|
|
|
282
291
|
@Composable
|
|
283
|
-
fun
|
|
284
|
-
val options = LocalOptions.current
|
|
285
|
-
val scrollState = LocalScrollState.current
|
|
286
|
-
val animatedHeader = options.headerType as? HeaderType.Animated ?: return
|
|
287
|
-
val density = LocalDensity.current
|
|
288
|
-
val scrollDp = with(density) { scrollState.value.toDp() }
|
|
289
|
-
|
|
290
|
-
Box(
|
|
291
|
-
Modifier
|
|
292
|
-
.fillMaxWidth()
|
|
293
|
-
.offset(y = -scrollDp)
|
|
294
|
-
.aspectRatio(animatedHeader.aspectRatio.value)
|
|
295
|
-
) {
|
|
296
|
-
animatedHeader.composable.invoke(scrollState.value)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
@Composable
|
|
301
|
-
fun FooterContent(){
|
|
292
|
+
fun FooterContent() {
|
|
302
293
|
val options = LocalOptions.current
|
|
303
|
-
if (options.footerComponent != null){
|
|
294
|
+
if (options.footerComponent != null) {
|
|
304
295
|
val ime = WindowInsets.ime
|
|
305
296
|
val density = LocalDensity.current
|
|
306
297
|
val imeBottom = ime.getBottom(density)
|
|
@@ -312,11 +303,11 @@ fun FooterContent(){
|
|
|
312
303
|
}
|
|
313
304
|
|
|
314
305
|
@Composable
|
|
315
|
-
fun OverplayView(bottomTabIndex: Int, id: Int){
|
|
306
|
+
fun OverplayView(bottomTabIndex: Int, id: Int) {
|
|
316
307
|
val overplayType = OverplayComponentRegistry.getOverplayType()
|
|
317
308
|
|
|
318
309
|
if (overplayType != null) {
|
|
319
|
-
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()) {
|
|
320
311
|
if (bottomTabIndex != -1) return@Box
|
|
321
312
|
if (OverplayComponentRegistry.currentRootId() != id) return@Box
|
|
322
313
|
OverplayComponentRegistry.OverlayComponent()
|
|
@@ -339,22 +330,24 @@ fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
|
|
|
339
330
|
Box(Modifier.onGloballyPositioned {
|
|
340
331
|
if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
|
|
341
332
|
}) {
|
|
342
|
-
Box(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
) {
|
|
350
342
|
footerComponent.invoke()
|
|
351
343
|
}
|
|
352
344
|
|
|
353
|
-
Box(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
345
|
+
Box(
|
|
346
|
+
modifier = Modifier
|
|
347
|
+
.fillMaxWidth()
|
|
348
|
+
.height(6.dp)
|
|
349
|
+
.offset(x = 0.dp, y = (-6).dp)
|
|
350
|
+
.background(shadowBrush)
|
|
358
351
|
)
|
|
359
352
|
}
|
|
360
353
|
}
|
|
@@ -364,6 +357,7 @@ data class InputSearchLayoutParams(
|
|
|
364
357
|
val startPadding: Dp,
|
|
365
358
|
val endPadding: Dp
|
|
366
359
|
)
|
|
360
|
+
|
|
367
361
|
@Composable
|
|
368
362
|
fun SearchAnimated(
|
|
369
363
|
isScrollInProgress: Boolean
|
|
@@ -478,6 +472,7 @@ internal fun isKeyboardVisible(): Boolean {
|
|
|
478
472
|
val density = LocalDensity.current
|
|
479
473
|
return ime.getBottom(density) > 0
|
|
480
474
|
}
|
|
475
|
+
|
|
481
476
|
private fun quantize(value: Int, stepPx: Int): Int {
|
|
482
477
|
if (stepPx <= 1) return value
|
|
483
478
|
return (value / stepPx) * stepPx
|
|
@@ -557,13 +552,15 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
|
|
|
557
552
|
|
|
558
553
|
val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
|
|
559
554
|
|
|
560
|
-
internal fun getInputSearchType(options: NavigationOptions): InputSearchType{
|
|
555
|
+
internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
|
|
561
556
|
return when (val headerType = options.headerType) {
|
|
562
557
|
is HeaderType.DefaultOrExtended -> when {
|
|
563
558
|
headerType.inputSearchProps == null -> InputSearchType.None
|
|
564
559
|
headerType.useAnimated -> InputSearchType.Animated
|
|
565
560
|
else -> InputSearchType.Header
|
|
566
561
|
}
|
|
562
|
+
|
|
567
563
|
else -> InputSearchType.None
|
|
568
564
|
}
|
|
569
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/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
|
+
#Tue May 19 15:41:04 GMT+07:00 2026
|
|
8
|
+
sdk.dir=/Users/phuc/Library/Android/sdk
|