@momo-kits/native-kits 0.167.1-sizescale.2-debug → 0.300.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.md +4 -4
- package/README.md +10 -2
- package/build.gradle.kts +3 -1
- package/compose/build.gradle.kts +19 -20
- package/compose/build.gradle.kts.backup +18 -19
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +1 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +25 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +23 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +11 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +6 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +22 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +28 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +6 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +26 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +4 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +47 -30
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +40 -32
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +47 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +6 -2
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +1 -3
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +6 -11
- package/gradle/libs.versions.toml +9 -3
- package/gradle/wrapper/gradle-wrapper.properties +3 -1
- package/gradle.properties +1 -1
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/package.json +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitSizeScale.kt +0 -56
- package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Example.xcscheme +0 -32
- package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/WorkspaceSettings.xcsettings +0 -16
- package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
- package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -60
- package/local.properties +0 -8
|
@@ -6,33 +6,46 @@ import androidx.compose.foundation.layout.width
|
|
|
6
6
|
import androidx.compose.runtime.Composable
|
|
7
7
|
import androidx.compose.runtime.compositionLocalOf
|
|
8
8
|
import androidx.compose.ui.Modifier
|
|
9
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
9
10
|
import androidx.compose.ui.unit.Dp
|
|
10
11
|
import androidx.compose.ui.unit.dp
|
|
12
|
+
import vn.momo.kits.platform.getScreenDimensions
|
|
13
|
+
import kotlin.math.floor
|
|
11
14
|
|
|
12
15
|
internal data class GridContext(
|
|
13
16
|
val numberOfColumns: Int = 12,
|
|
14
|
-
val
|
|
15
|
-
val
|
|
17
|
+
val gutterPx: Int = 0,
|
|
18
|
+
val columnPx: Float = 0f,
|
|
19
|
+
val density: Float = 1f,
|
|
16
20
|
) {
|
|
17
|
-
fun sizeForSpan(span: Int): Dp =
|
|
21
|
+
fun sizeForSpan(span: Int): Dp = (floor(columnPx * span + gutterPx * (span - 1)) / density).dp
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
internal val LocalGridContext = compositionLocalOf<GridContext?> { null }
|
|
21
25
|
|
|
22
26
|
@Composable
|
|
23
|
-
internal fun
|
|
24
|
-
|
|
27
|
+
internal fun gridColumns(): Int = if (getScreenDimensions().width > 600) 24 else 12
|
|
28
|
+
|
|
29
|
+
@Composable
|
|
30
|
+
internal fun gridContext(maxWidthPx: Int, columns: Int, gap: Dp): GridContext {
|
|
31
|
+
val density = LocalDensity.current
|
|
32
|
+
val gutterPx = with(density) { gap.roundToPx() }
|
|
33
|
+
return GridContext(columns, gutterPx, (maxWidthPx - gutterPx * (columns - 1)).toFloat() / columns, density.density)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Composable
|
|
37
|
+
fun Item(
|
|
38
|
+
widthSpan: Int? = null,
|
|
25
39
|
heightSpan: Int = 0,
|
|
26
40
|
modifier: Modifier = Modifier,
|
|
41
|
+
contentModifier: Modifier = Modifier,
|
|
27
42
|
content: @Composable () -> Unit,
|
|
28
43
|
) {
|
|
29
44
|
val grid = LocalGridContext.current
|
|
30
45
|
var m = modifier
|
|
31
46
|
if (grid != null) {
|
|
32
|
-
m = m.width(grid.sizeForSpan(widthSpan))
|
|
47
|
+
m = m.width(grid.sizeForSpan(widthSpan ?: grid.numberOfColumns))
|
|
33
48
|
if (heightSpan > 0) m = m.height(grid.sizeForSpan(heightSpan))
|
|
34
49
|
}
|
|
35
|
-
|
|
36
|
-
// on top of each other — matches the React Item's flex-column content.
|
|
37
|
-
Column(modifier = m) { content() }
|
|
50
|
+
Column(modifier = m.then(contentModifier)) { content() }
|
|
38
51
|
}
|
|
@@ -3,6 +3,7 @@ package vn.momo.kits.layout
|
|
|
3
3
|
import androidx.compose.foundation.layout.Arrangement
|
|
4
4
|
import androidx.compose.foundation.layout.Box
|
|
5
5
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
6
|
+
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
6
7
|
import androidx.compose.foundation.layout.FlowRow
|
|
7
8
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
8
9
|
import androidx.compose.foundation.layout.padding
|
|
@@ -11,27 +12,28 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|
|
11
12
|
import androidx.compose.ui.Modifier
|
|
12
13
|
import androidx.compose.ui.unit.dp
|
|
13
14
|
import vn.momo.kits.components.Image
|
|
15
|
+
import vn.momo.kits.const.Spacing
|
|
14
16
|
|
|
15
17
|
@Composable
|
|
16
|
-
|
|
18
|
+
@OptIn(ExperimentalLayoutApi::class)
|
|
19
|
+
fun Section(
|
|
17
20
|
useMargin: Boolean = true,
|
|
18
21
|
backgroundImage: String? = null,
|
|
19
22
|
modifier: Modifier = Modifier,
|
|
20
23
|
content: @Composable () -> Unit,
|
|
21
24
|
) {
|
|
22
|
-
val columns =
|
|
23
|
-
val
|
|
24
|
-
val margin = if (useMargin)
|
|
25
|
-
Box(modifier =
|
|
25
|
+
val columns = gridColumns()
|
|
26
|
+
val gap = Spacing.M
|
|
27
|
+
val margin = if (useMargin) Spacing.M else 0.dp
|
|
28
|
+
Box(modifier = Modifier.fillMaxWidth().padding(horizontal = margin).then(modifier)) {
|
|
26
29
|
if (backgroundImage != null) {
|
|
27
30
|
Image(source = backgroundImage, modifier = Modifier.matchParentSize())
|
|
28
31
|
}
|
|
29
32
|
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
|
30
|
-
|
|
31
|
-
CompositionLocalProvider(LocalGridContext provides GridContext(columns, gutter, sizePerSpan)) {
|
|
33
|
+
CompositionLocalProvider(LocalGridContext provides gridContext(constraints.maxWidth, columns, gap)) {
|
|
32
34
|
FlowRow(
|
|
33
|
-
horizontalArrangement = Arrangement.spacedBy(
|
|
34
|
-
verticalArrangement = Arrangement.spacedBy(
|
|
35
|
+
horizontalArrangement = Arrangement.spacedBy(gap),
|
|
36
|
+
verticalArrangement = Arrangement.spacedBy(gap),
|
|
35
37
|
) { content() }
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -30,13 +30,15 @@ import vn.momo.kits.const.AppNavigationBar
|
|
|
30
30
|
import vn.momo.kits.const.AppStatusBar
|
|
31
31
|
import vn.momo.kits.const.AppTheme
|
|
32
32
|
import vn.momo.kits.const.Theme
|
|
33
|
-
import vn.momo.kits.const.ProvideKitScale
|
|
34
33
|
import vn.momo.kits.const.defaultTheme
|
|
35
|
-
import vn.momo.kits.const.pendingSizeScale
|
|
36
34
|
import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
|
|
35
|
+
import vn.momo.kits.platform.ProvideOsFontScale
|
|
37
36
|
import vn.momo.kits.utils.getAppStatusBarHeight
|
|
38
37
|
import vn.momo.kits.utils.getNavigationBarHeight
|
|
38
|
+
// AI-GENERATED START: consume typed NativeMaxApi results
|
|
39
39
|
import vn.momo.maxapi.IMaxApi
|
|
40
|
+
import vn.momo.maxapi.runtime.ApiResult
|
|
41
|
+
// AI-GENERATED END: consume typed NativeMaxApi results
|
|
40
42
|
import kotlin.time.Duration.Companion.milliseconds
|
|
41
43
|
|
|
42
44
|
@Composable
|
|
@@ -54,11 +56,13 @@ fun NavigationContainer(
|
|
|
54
56
|
) {
|
|
55
57
|
val navController = rememberNavController()
|
|
56
58
|
val registry = remember { DynamicScreenRegistry() }
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// AI-GENERATED START: retain the current host client after recomposition
|
|
60
|
+
val navigator = remember(navController, maxApi, registry) {
|
|
61
|
+
Navigator(navController = navController, maxApi = maxApi, registry = registry)
|
|
62
|
+
}
|
|
63
|
+
// AI-GENERATED END: retain the current host client after recomposition
|
|
64
|
+
val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
|
|
65
|
+
val navigationBarHeight = getNavigationBarHeight()
|
|
62
66
|
|
|
63
67
|
val parentContext = LocalContext.current
|
|
64
68
|
val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
|
|
@@ -80,22 +84,31 @@ fun NavigationContainer(
|
|
|
80
84
|
|
|
81
85
|
val resolvedLocalize = localize ?: fallbackLocalize
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// AI-GENERATED START: cancel language delivery when its composition owner changes
|
|
88
|
+
DisposableEffect(maxApi, resolvedLocalize) {
|
|
89
|
+
val api = maxApi
|
|
90
|
+
val target = resolvedLocalize
|
|
91
|
+
var active = true
|
|
92
|
+
val handle = runCatching {
|
|
93
|
+
api.getLanguage { result ->
|
|
94
|
+
if (active && result is ApiResult.Success &&
|
|
95
|
+
result.value.trim().lowercase().startsWith(Localize.EN)) {
|
|
96
|
+
target.changeLanguage(Localize.EN)
|
|
89
97
|
}
|
|
90
|
-
}
|
|
98
|
+
}
|
|
99
|
+
}.getOrNull()
|
|
100
|
+
onDispose {
|
|
101
|
+
active = false
|
|
102
|
+
handle?.cancel()
|
|
91
103
|
}
|
|
92
104
|
}
|
|
105
|
+
// AI-GENERATED END: cancel language delivery when its composition owner changes
|
|
93
106
|
|
|
94
107
|
val screenId = rememberSaveable { registry.nextId() }
|
|
95
108
|
registry.bind(screenId, initialScreenName, initialScreen, options)
|
|
96
109
|
val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
ProvideOsFontScale {
|
|
99
112
|
ProvideNavigationEventDispatcherOwner(navigator) {
|
|
100
113
|
CompositionLocalProvider(
|
|
101
114
|
LocalNavigator provides navigator,
|
|
@@ -196,9 +209,3 @@ fun NavigationContainer(
|
|
|
196
209
|
|
|
197
210
|
val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> { null }
|
|
198
211
|
|
|
199
|
-
private fun parseLanguage(raw: Map<String, Any?>?): String? {
|
|
200
|
-
val code = (raw?.get("response") as? String)?.trim()?.lowercase()
|
|
201
|
-
if (code.isNullOrEmpty()) return null
|
|
202
|
-
return if (code.startsWith(Localize.EN)) Localize.EN else Localize.VI
|
|
203
|
-
}
|
|
204
|
-
|
|
@@ -22,6 +22,9 @@ import vn.momo.kits.components.PopupNotify
|
|
|
22
22
|
import vn.momo.kits.navigation.BottomHeader.*
|
|
23
23
|
import vn.momo.kits.navigation.component.SnackBar
|
|
24
24
|
import vn.momo.maxapi.IMaxApi
|
|
25
|
+
// AI-GENERATED START: dismiss through the typed navigator target
|
|
26
|
+
import vn.momo.maxapi.contract.navigation.model.DismissTarget
|
|
27
|
+
// AI-GENERATED END: dismiss through the typed navigator target
|
|
25
28
|
import kotlin.time.Duration.Companion.milliseconds
|
|
26
29
|
|
|
27
30
|
class Navigator(
|
|
@@ -119,7 +122,9 @@ class Navigator(
|
|
|
119
122
|
delay(STACK_TRANSITION_DURATION.milliseconds)
|
|
120
123
|
dismissing?.let { registry.unregisterScreen(it.id) }
|
|
121
124
|
} else {
|
|
122
|
-
|
|
125
|
+
// AI-GENERATED START: preserve root-screen dismissal
|
|
126
|
+
maxApi?.dismiss(DismissTarget.Current()) {}
|
|
127
|
+
// AI-GENERATED END: preserve root-screen dismissal
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -6,6 +6,9 @@ import androidx.compose.runtime.snapshotFlow
|
|
|
6
6
|
import vn.momo.kits.platform.getScreenDimensions
|
|
7
7
|
import vn.momo.kits.platform.rememberBackGestureProgress
|
|
8
8
|
import vn.momo.maxapi.IMaxApi
|
|
9
|
+
// AI-GENERATED START: use the typed light vibration pattern
|
|
10
|
+
import vn.momo.maxapi.contract.device.vibration.model.VibrateStyle
|
|
11
|
+
// AI-GENERATED END: use the typed light vibration pattern
|
|
9
12
|
|
|
10
13
|
// Unscaled, so the tick lands at the same physical distance on every device.
|
|
11
14
|
private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 50f
|
|
@@ -25,7 +28,9 @@ internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
|
|
|
25
28
|
snapshotFlow { progress.value }.collect { fraction ->
|
|
26
29
|
val travelled = fraction ?: 0f
|
|
27
30
|
if (travelled >= threshold && !isSendHaptic) {
|
|
28
|
-
|
|
31
|
+
// AI-GENERATED START: keep one light tick per threshold crossing
|
|
32
|
+
runCatching { maxApi.vibrate(VibrateStyle.LIGHT) }
|
|
33
|
+
// AI-GENERATED END: keep one light tick per threshold crossing
|
|
29
34
|
isSendHaptic = true
|
|
30
35
|
} else if (travelled < threshold && isSendHaptic) {
|
|
31
36
|
isSendHaptic = false
|
|
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.*
|
|
|
10
10
|
import androidx.compose.runtime.Composable
|
|
11
11
|
import androidx.compose.runtime.DisposableEffect
|
|
12
12
|
import androidx.compose.runtime.LaunchedEffect
|
|
13
|
+
import androidx.compose.runtime.remember
|
|
13
14
|
import androidx.compose.ui.Alignment
|
|
14
15
|
import androidx.compose.ui.Modifier
|
|
15
16
|
import androidx.compose.ui.unit.dp
|
|
@@ -26,6 +27,7 @@ import vn.momo.kits.navigation.LocalNavigator
|
|
|
26
27
|
import vn.momo.kits.navigation.NavigationOptions
|
|
27
28
|
import vn.momo.kits.navigation.StackScreen
|
|
28
29
|
import vn.momo.kits.navigation.component.HeaderType
|
|
30
|
+
import vn.momo.kits.platform.BackHandler
|
|
29
31
|
import vn.momo.kits.platform.reportFloatingArrowBack
|
|
30
32
|
|
|
31
33
|
private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
|
|
@@ -43,11 +45,17 @@ internal fun getBottomTabOption(index: Int): NavigationOptions? {
|
|
|
43
45
|
@Composable
|
|
44
46
|
fun BottomTab(
|
|
45
47
|
items: List<BottomTabItem>,
|
|
46
|
-
floatingButton: BottomTabFloatingButton? = null
|
|
48
|
+
floatingButton: BottomTabFloatingButton? = null,
|
|
49
|
+
initialRouteName: String? = null,
|
|
50
|
+
onTabPress: ((index: Int, switchTab: () -> Unit) -> Unit)? = null,
|
|
51
|
+
backToInitialTab: Boolean = true,
|
|
47
52
|
) {
|
|
48
53
|
val navigation = LocalNavigation.current
|
|
49
54
|
val navigator = LocalNavigator.current
|
|
50
55
|
val navController = rememberNavController()
|
|
56
|
+
val startRoute = remember {
|
|
57
|
+
"option${items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0)}"
|
|
58
|
+
}
|
|
51
59
|
|
|
52
60
|
bottomTabOptionItems = items.mapIndexed { index, item ->
|
|
53
61
|
item.options ?: NavigationOptions()
|
|
@@ -62,15 +70,14 @@ fun BottomTab(
|
|
|
62
70
|
)
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
// Tab 0 keeps the seeked transition; other tabs go to the host's floating arrow. Matches RN.
|
|
66
73
|
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
|
67
|
-
val
|
|
74
|
+
val onStartTab = currentRoute == null || currentRoute == startRoute
|
|
75
|
+
val isFirstTab = onStartTab || !backToInitialTab
|
|
76
|
+
val backToStartTab = { navController.popBackStack(startRoute, inclusive = false) }
|
|
68
77
|
|
|
69
78
|
DisposableEffect(isFirstTab) {
|
|
70
79
|
reportFloatingArrowBack(!isFirstTab)
|
|
71
|
-
navigator.tabBackHandler = if (isFirstTab) null else ({
|
|
72
|
-
navController.popBackStack(navController.graph.startDestinationRoute!!, false)
|
|
73
|
-
})
|
|
80
|
+
navigator.tabBackHandler = if (isFirstTab) null else ({ backToStartTab() })
|
|
74
81
|
// both live outside this composition
|
|
75
82
|
onDispose {
|
|
76
83
|
reportFloatingArrowBack(false)
|
|
@@ -84,7 +91,7 @@ fun BottomTab(
|
|
|
84
91
|
) {
|
|
85
92
|
NavHost(
|
|
86
93
|
navController = navController,
|
|
87
|
-
startDestination =
|
|
94
|
+
startDestination = startRoute
|
|
88
95
|
) {
|
|
89
96
|
items.forEachIndexed { index, item ->
|
|
90
97
|
composable(
|
|
@@ -125,11 +132,8 @@ fun BottomTab(
|
|
|
125
132
|
navigationOptions = option,
|
|
126
133
|
bottomTabIndex = index,
|
|
127
134
|
onBackHandler = {
|
|
128
|
-
if (index !=
|
|
129
|
-
|
|
130
|
-
navController.graph.startDestinationRoute!!,
|
|
131
|
-
inclusive = false
|
|
132
|
-
)
|
|
135
|
+
if (backToInitialTab && "option$index" != startRoute) {
|
|
136
|
+
backToStartTab()
|
|
133
137
|
} else {
|
|
134
138
|
navigator.pop()
|
|
135
139
|
}
|
|
@@ -139,17 +143,22 @@ fun BottomTab(
|
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
}
|
|
146
|
+
BackHandler(enabled = !onStartTab) {
|
|
147
|
+
if (backToInitialTab) backToStartTab() else navigator.pop()
|
|
148
|
+
}
|
|
142
149
|
Column {
|
|
143
150
|
BottomTabBar(
|
|
144
151
|
items = items,
|
|
145
152
|
floatingButton = floatingButton,
|
|
146
153
|
navController = navController,
|
|
147
|
-
onTabSelected = {
|
|
148
|
-
val
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
onTabSelected = { index ->
|
|
155
|
+
val switchTab = {
|
|
156
|
+
val targetRoute = "option$index"
|
|
157
|
+
if (navController.currentBackStackEntry?.destination?.route != targetRoute) {
|
|
158
|
+
navController.navigate(targetRoute)
|
|
159
|
+
}
|
|
152
160
|
}
|
|
161
|
+
onTabPress?.invoke(index, switchTab) ?: switchTab()
|
|
153
162
|
}
|
|
154
163
|
)
|
|
155
164
|
Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
|
|
@@ -67,11 +67,10 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
67
67
|
else -> HEADER_HEIGHT
|
|
68
68
|
}
|
|
69
69
|
val opacity by animateFloatAsState(
|
|
70
|
-
targetValue =
|
|
71
|
-
0f
|
|
72
|
-
|
|
73
|
-
(scrollState.value / opacityHeight.toFloat())
|
|
74
|
-
.coerceIn(0f, 1f)
|
|
70
|
+
targetValue = when {
|
|
71
|
+
options.headerType is HeaderType.Transparent -> 0f
|
|
72
|
+
opacityHeight <= 0 -> if (scrollState.value > 0) 1f else 0f
|
|
73
|
+
else -> (scrollState.value / opacityHeight.toFloat()).coerceIn(0f, 1f)
|
|
75
74
|
}
|
|
76
75
|
)
|
|
77
76
|
|
|
@@ -16,6 +16,17 @@ import androidx.compose.foundation.shape.CircleShape
|
|
|
16
16
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
17
17
|
import androidx.compose.runtime.Composable
|
|
18
18
|
import androidx.compose.runtime.LaunchedEffect
|
|
19
|
+
// AI-GENERATED START: dispose toolkit requests with their UI owner
|
|
20
|
+
import androidx.compose.runtime.DisposableEffect
|
|
21
|
+
import kotlinx.serialization.json.booleanOrNull
|
|
22
|
+
import kotlinx.serialization.json.jsonPrimitive
|
|
23
|
+
import vn.momo.maxapi.contract.navigation.model.DismissTarget
|
|
24
|
+
import vn.momo.maxapi.contract.toolkit.model.ToolAction
|
|
25
|
+
import vn.momo.maxapi.contract.toolkit.model.ToolkitOptions
|
|
26
|
+
import vn.momo.maxapi.contract.toolkit.model.ToolkitToolGroup
|
|
27
|
+
import vn.momo.maxapi.contract.toolkit.model.ToolkitTool
|
|
28
|
+
import vn.momo.maxapi.runtime.ApiResult
|
|
29
|
+
// AI-GENERATED END: dispose toolkit requests with their UI owner
|
|
19
30
|
import androidx.compose.runtime.derivedStateOf
|
|
20
31
|
import androidx.compose.runtime.getValue
|
|
21
32
|
import androidx.compose.runtime.mutableStateOf
|
|
@@ -71,37 +82,43 @@ internal fun Toolkit(
|
|
|
71
82
|
var isFavorite by remember { mutableStateOf(false) }
|
|
72
83
|
val isLoading by remember { mutableStateOf(false) }
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
// AI-GENERATED START: retain custom tool data and typed callback outcomes
|
|
86
|
+
val requests = remember(api, context?.appCode) { ToolkitRequests() }
|
|
87
|
+
DisposableEffect(requests) { onDispose { requests.close() } }
|
|
88
|
+
|
|
89
|
+
LaunchedEffect(requests) {
|
|
90
|
+
val client = api ?: return@LaunchedEffect
|
|
91
|
+
requests.launch<Boolean>({ callback -> client.checkFavorite(context?.appCode, callback) }) { result ->
|
|
92
|
+
if (result is ApiResult.Success) isFavorite = result.value
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
fun onPressShortcut() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
val client = api ?: return
|
|
98
|
+
requests.launch({ callback ->
|
|
99
|
+
client.performToolAction(ToolAction.FAVORITE, context?.appCode, callback)
|
|
100
|
+
}) { result ->
|
|
101
|
+
if (result is ApiResult.Success) {
|
|
102
|
+
result.value.jsonPrimitive.booleanOrNull?.let { isFavorite = it }
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
fun onPressMore() {
|
|
92
|
-
api
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
val client = api ?: return
|
|
109
|
+
val options = ToolkitOptions(
|
|
110
|
+
customToolGroups = headerRight.tools.map { group ->
|
|
111
|
+
ToolkitToolGroup(group.title, group.items.map { tool ->
|
|
112
|
+
ToolkitTool(tool.key, tool.icon, tool.showBadge, tool.name, tool.showRightIcon)
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
useSystemTools = headerRight.useSystemTools,
|
|
116
|
+
)
|
|
117
|
+
requests.launch({ callback -> client.showToolkit(options, callback) }) { result ->
|
|
118
|
+
if (result is ApiResult.Success) headerRight.toolCallback?.invoke(result.value.actionKey)
|
|
103
119
|
}
|
|
104
120
|
}
|
|
121
|
+
// AI-GENERATED END: retain custom tool data and typed callback outcomes
|
|
105
122
|
|
|
106
123
|
val navButtonConfig = getNavigationButtonConfig(
|
|
107
124
|
headerRight,
|
|
@@ -156,15 +173,13 @@ internal fun Toolkit(
|
|
|
156
173
|
size = 20.dp,
|
|
157
174
|
color = headerColor.tintIconColor,
|
|
158
175
|
modifier = Modifier.padding(4.dp).activeOpacityClickable {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
) {}
|
|
176
|
+
// AI-GENERATED START: open support through the typed action
|
|
177
|
+
api?.let { client ->
|
|
178
|
+
requests.launch({ callback ->
|
|
179
|
+
client.performToolAction(ToolAction.SUPPORT, context.appCode, callback)
|
|
180
|
+
}) {}
|
|
181
|
+
}
|
|
182
|
+
// AI-GENERATED END: open support through the typed action
|
|
168
183
|
}
|
|
169
184
|
)
|
|
170
185
|
Box(
|
|
@@ -179,7 +194,9 @@ internal fun Toolkit(
|
|
|
179
194
|
size = 20.dp,
|
|
180
195
|
color = headerColor.tintIconColor,
|
|
181
196
|
modifier = Modifier.padding(4.dp).activeOpacityClickable {
|
|
182
|
-
|
|
197
|
+
// AI-GENERATED START: preserve dismiss-all without selecting Home
|
|
198
|
+
api?.dismiss(DismissTarget.All) {}
|
|
199
|
+
// AI-GENERATED END: preserve dismiss-all without selecting Home
|
|
183
200
|
}
|
|
184
201
|
)
|
|
185
202
|
}
|
|
@@ -9,10 +9,12 @@ import androidx.compose.ui.text.style.TextOverflow
|
|
|
9
9
|
import androidx.compose.ui.unit.sp
|
|
10
10
|
import androidx.compose.ui.zIndex
|
|
11
11
|
import vn.momo.kits.components.Text
|
|
12
|
+
import vn.momo.kits.const.KitDesignSystem
|
|
12
13
|
import vn.momo.kits.const.Typography
|
|
13
14
|
import vn.momo.kits.modifier.setAutomationId
|
|
14
15
|
|
|
15
|
-
internal
|
|
16
|
+
internal val headerFontFamily: String?
|
|
17
|
+
get() = if (KitDesignSystem.isWhiteList) "MoMoTrustDisplay" else null
|
|
16
18
|
|
|
17
19
|
@Composable
|
|
18
20
|
internal fun HeaderTitle(
|
|
@@ -29,7 +31,7 @@ internal fun HeaderTitle(
|
|
|
29
31
|
fontSize = 15.sp,
|
|
30
32
|
lineHeight = 22.sp,
|
|
31
33
|
),
|
|
32
|
-
fontFamily =
|
|
34
|
+
fontFamily = headerFontFamily,
|
|
33
35
|
color = color,
|
|
34
36
|
maxLines = 1,
|
|
35
37
|
overflow = TextOverflow.Ellipsis
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// File: ToolkitRequests.kt
|
|
2
|
+
// Created At: 2026-10-12 +07:00 | Created By: AI | AI Agent: Codex | Model: GPT-6
|
|
3
|
+
// AI-GENERATED START: cancel only toolkit requests owned by this composition
|
|
4
|
+
package vn.momo.kits.navigation.component
|
|
5
|
+
|
|
6
|
+
import vn.momo.maxapi.runtime.ApiCallback
|
|
7
|
+
import vn.momo.maxapi.runtime.ApiRequestHandle
|
|
8
|
+
|
|
9
|
+
internal class ToolkitRequests {
|
|
10
|
+
private var active = true
|
|
11
|
+
private val handles = mutableSetOf<ApiRequestHandle>()
|
|
12
|
+
|
|
13
|
+
fun <T> launch(request: (ApiCallback<T>) -> ApiRequestHandle, onResult: ApiCallback<T>) {
|
|
14
|
+
if (!active) return
|
|
15
|
+
var completed = false
|
|
16
|
+
var handle: ApiRequestHandle? = null
|
|
17
|
+
val created = request { result ->
|
|
18
|
+
completed = true
|
|
19
|
+
handle?.let(handles::remove)
|
|
20
|
+
if (active) onResult(result)
|
|
21
|
+
}
|
|
22
|
+
handle = created
|
|
23
|
+
if (!active) created.cancel() else if (!completed) handles.add(created)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun close() {
|
|
27
|
+
active = false
|
|
28
|
+
val pending = handles.toList()
|
|
29
|
+
handles.clear()
|
|
30
|
+
pending.forEach { it.cancel() }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// AI-GENERATED END: cancel only toolkit requests owned by this composition
|