@momo-kits/native-kits 0.300.4 → 0.300.5-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/build.gradle.kts +13 -0
- package/compose/build.gradle.kts +197 -0
- package/compose/build.gradle.kts.backup +197 -0
- package/compose/compose.podspec +47 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +20 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +110 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +1071 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +65 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +81 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +357 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +115 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +478 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +265 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +235 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +265 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +308 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +330 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +92 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +560 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +501 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +236 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +180 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +17 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +194 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +287 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +116 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +215 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +404 -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 +732 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +40 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +233 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +217 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +455 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +103 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +413 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +39 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +133 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +135 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +62 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +42 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +34 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
- package/gradle/libs.versions.toml +68 -0
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/gradle.properties +26 -0
- package/gradlew +252 -0
- package/gradlew.bat +94 -0
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/README.md +120 -0
- package/package.json +1 -1
- package/settings.gradle.kts +64 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
package vn.momo.kits.navigation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.tween
|
|
4
|
+
import androidx.compose.animation.fadeIn
|
|
5
|
+
import androidx.compose.animation.slideInVertically
|
|
6
|
+
import androidx.compose.animation.slideOutVertically
|
|
7
|
+
import androidx.compose.runtime.Composable
|
|
8
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
9
|
+
import androidx.compose.runtime.DisposableEffect
|
|
10
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
11
|
+
import androidx.compose.runtime.mutableStateOf
|
|
12
|
+
import androidx.compose.runtime.remember
|
|
13
|
+
import androidx.compose.runtime.saveable.rememberSaveable
|
|
14
|
+
import androidx.compose.runtime.staticCompositionLocalOf
|
|
15
|
+
import androidx.compose.ui.unit.Dp
|
|
16
|
+
import androidx.navigation.compose.NavHost
|
|
17
|
+
import androidx.navigation.compose.composable
|
|
18
|
+
import androidx.navigation.compose.rememberNavController
|
|
19
|
+
import androidx.navigation.toRoute
|
|
20
|
+
import kotlinx.coroutines.delay
|
|
21
|
+
import kotlinx.coroutines.flow.collectLatest
|
|
22
|
+
import vn.momo.kits.application.DesignSystemConfig
|
|
23
|
+
import vn.momo.kits.application.DesignSystemKits
|
|
24
|
+
import vn.momo.kits.application.LocalContext
|
|
25
|
+
import vn.momo.kits.application.LocalDesignSystemConfig
|
|
26
|
+
import vn.momo.kits.application.LocalLocalize
|
|
27
|
+
import vn.momo.kits.application.Localize
|
|
28
|
+
import vn.momo.kits.application.MiniAppContext
|
|
29
|
+
import vn.momo.kits.const.AppNavigationBar
|
|
30
|
+
import vn.momo.kits.const.AppStatusBar
|
|
31
|
+
import vn.momo.kits.const.AppTheme
|
|
32
|
+
import vn.momo.kits.const.Theme
|
|
33
|
+
import vn.momo.kits.const.defaultTheme
|
|
34
|
+
import vn.momo.kits.layout.LocalScreenLayout
|
|
35
|
+
import vn.momo.kits.layout.calculateScreenLayout
|
|
36
|
+
import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
|
|
37
|
+
import vn.momo.kits.platform.ProvideOsFontScale
|
|
38
|
+
import vn.momo.kits.utils.getAppStatusBarHeight
|
|
39
|
+
import vn.momo.kits.utils.getNavigationBarHeight
|
|
40
|
+
// AI-GENERATED START: consume typed NativeMaxApi results
|
|
41
|
+
import vn.momo.maxapi.IMaxApi
|
|
42
|
+
import vn.momo.maxapi.runtime.ApiResult
|
|
43
|
+
// AI-GENERATED END: consume typed NativeMaxApi results
|
|
44
|
+
import kotlin.time.Duration.Companion.milliseconds
|
|
45
|
+
|
|
46
|
+
@Composable
|
|
47
|
+
fun NavigationContainer(
|
|
48
|
+
initialScreenName: String,
|
|
49
|
+
initialScreen: @Composable () -> Unit,
|
|
50
|
+
maxApi: IMaxApi,
|
|
51
|
+
options: NavigationOptions? = null,
|
|
52
|
+
initialTheme: Theme = defaultTheme,
|
|
53
|
+
miniAppContext: MiniAppContext? = null,
|
|
54
|
+
setNavigator: ((Navigator) -> Unit)? = null,
|
|
55
|
+
statusBarHeight: Dp? = null,
|
|
56
|
+
localize: Localize? = null,
|
|
57
|
+
designSystemConfig: DesignSystemConfig? = null,
|
|
58
|
+
) {
|
|
59
|
+
val navController = rememberNavController()
|
|
60
|
+
val registry = remember { DynamicScreenRegistry() }
|
|
61
|
+
// AI-GENERATED START: retain the current host client after recomposition
|
|
62
|
+
val navigator = remember(navController, maxApi, registry) {
|
|
63
|
+
Navigator(navController = navController, maxApi = maxApi, registry = registry)
|
|
64
|
+
}
|
|
65
|
+
// AI-GENERATED END: retain the current host client after recomposition
|
|
66
|
+
val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
|
|
67
|
+
val navigationBarHeight = getNavigationBarHeight()
|
|
68
|
+
val screenLayout = calculateScreenLayout(statusBarHeight, parent = LocalScreenLayout.current)
|
|
69
|
+
|
|
70
|
+
val parentContext = LocalContext.current
|
|
71
|
+
val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
|
|
72
|
+
|
|
73
|
+
val stackTransition = remember { StackTransitionCoordinator() }
|
|
74
|
+
val theme = remember { mutableStateOf(initialTheme) }
|
|
75
|
+
val resolvedDesignSystemConfig =
|
|
76
|
+
designSystemConfig ?: LocalDesignSystemConfig.current ?: DesignSystemKits.getConfigs().designSystemConfig
|
|
77
|
+
val headerBar = resolvedDesignSystemConfig?.headerBar
|
|
78
|
+
// RN parity: the theme's own header image wins; config headerBar is only the fallback.
|
|
79
|
+
val resolvedTheme = remember(theme.value, headerBar) {
|
|
80
|
+
if (theme.value.isHeaderImage() || headerBar.isNullOrEmpty()) {
|
|
81
|
+
theme.value
|
|
82
|
+
} else {
|
|
83
|
+
theme.value.copy(assets = theme.value.assets.copy(headerBackground = headerBar))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
val fallbackLocalize = remember { Localize() }
|
|
87
|
+
|
|
88
|
+
val resolvedLocalize = localize ?: fallbackLocalize
|
|
89
|
+
|
|
90
|
+
// AI-GENERATED START: cancel language delivery when its composition owner changes
|
|
91
|
+
DisposableEffect(maxApi, resolvedLocalize) {
|
|
92
|
+
val api = maxApi
|
|
93
|
+
val target = resolvedLocalize
|
|
94
|
+
var active = true
|
|
95
|
+
val handle = runCatching {
|
|
96
|
+
api.getLanguage { result ->
|
|
97
|
+
if (active && result is ApiResult.Success &&
|
|
98
|
+
result.value.trim().lowercase().startsWith(Localize.EN)) {
|
|
99
|
+
target.changeLanguage(Localize.EN)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}.getOrNull()
|
|
103
|
+
onDispose {
|
|
104
|
+
active = false
|
|
105
|
+
handle?.cancel()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// AI-GENERATED END: cancel language delivery when its composition owner changes
|
|
109
|
+
|
|
110
|
+
val screenId = rememberSaveable { registry.nextId() }
|
|
111
|
+
registry.bind(screenId, initialScreenName, initialScreen, options)
|
|
112
|
+
val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
|
|
113
|
+
|
|
114
|
+
ProvideOsFontScale {
|
|
115
|
+
ProvideNavigationEventDispatcherOwner(navigator) {
|
|
116
|
+
CompositionLocalProvider(
|
|
117
|
+
LocalNavigator provides navigator,
|
|
118
|
+
LocalDynamicScreenRegistry provides registry,
|
|
119
|
+
LocalMaxApi provides maxApi,
|
|
120
|
+
AppTheme provides resolvedTheme,
|
|
121
|
+
AppStatusBar provides statusBarHeight,
|
|
122
|
+
AppNavigationBar provides navigationBarHeight,
|
|
123
|
+
LocalScreenLayout provides screenLayout,
|
|
124
|
+
LocalContext provides mergedContext,
|
|
125
|
+
LocalDesignSystemConfig provides resolvedDesignSystemConfig,
|
|
126
|
+
LocalLocalize provides resolvedLocalize,
|
|
127
|
+
LocalStackTransition provides stackTransition,
|
|
128
|
+
) {
|
|
129
|
+
LaunchedEffect(Unit) {
|
|
130
|
+
setNavigator?.invoke(navigator)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
SwipeBackHaptic(maxApi)
|
|
134
|
+
|
|
135
|
+
LaunchedEffect(navController, registry) {
|
|
136
|
+
navController.currentBackStack.collectLatest { entries ->
|
|
137
|
+
delay(STACK_TRANSITION_DURATION.milliseconds)
|
|
138
|
+
val aliveIds = entries.mapNotNullTo(mutableSetOf()) { entry ->
|
|
139
|
+
runCatching { entry.toRoute<DynamicScreenRoute>().id }.getOrNull()
|
|
140
|
+
?: runCatching { entry.toRoute<DynamicDialogRoute>().id }.getOrNull()
|
|
141
|
+
}
|
|
142
|
+
aliveIds.add(screenId)
|
|
143
|
+
registry.retainOnly(aliveIds)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
NavHost(
|
|
148
|
+
navController,
|
|
149
|
+
startDestination = startDestination,
|
|
150
|
+
enterTransition = { stackEnter() },
|
|
151
|
+
exitTransition = { stackExit() },
|
|
152
|
+
popEnterTransition = { stackPopEnter() },
|
|
153
|
+
popExitTransition = { stackPopExit() },
|
|
154
|
+
) {
|
|
155
|
+
composable<DynamicScreenRoute> { backStackEntry ->
|
|
156
|
+
val route = backStackEntry.toRoute<DynamicScreenRoute>()
|
|
157
|
+
val screen = registry.getScreen(route.id)
|
|
158
|
+
|
|
159
|
+
if (screen != null) {
|
|
160
|
+
StackTransitionDecor(id = route.id, isStackRoute = true) {
|
|
161
|
+
StackScreen(
|
|
162
|
+
id = route.id,
|
|
163
|
+
name = screen.name,
|
|
164
|
+
content = screen.content,
|
|
165
|
+
navigationOptions = screen.options
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
composable<DynamicDialogRoute>(
|
|
172
|
+
enterTransition = {
|
|
173
|
+
slideInVertically(
|
|
174
|
+
animationSpec = tween(300),
|
|
175
|
+
initialOffsetY = { it }
|
|
176
|
+
)
|
|
177
|
+
},
|
|
178
|
+
exitTransition = null,
|
|
179
|
+
popEnterTransition = { fadeIn(animationSpec = tween(0)) },
|
|
180
|
+
popExitTransition = {
|
|
181
|
+
slideOutVertically(
|
|
182
|
+
animationSpec = tween(300),
|
|
183
|
+
targetOffsetY = { it }
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
) { backStackEntry ->
|
|
187
|
+
val route = backStackEntry.toRoute<DynamicDialogRoute>()
|
|
188
|
+
val screen = registry.getScreen(route.id)
|
|
189
|
+
|
|
190
|
+
if (screen != null) {
|
|
191
|
+
StackTransitionDecor(id = route.id, isStackRoute = false) {
|
|
192
|
+
StackScreen(
|
|
193
|
+
id = route.id,
|
|
194
|
+
name = screen.name,
|
|
195
|
+
content = screen.content,
|
|
196
|
+
navigationOptions = screen.options
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
DisposableEffect(Unit) {
|
|
207
|
+
onDispose {
|
|
208
|
+
navigator.dispose()
|
|
209
|
+
registry.unregisterScreen(screenId)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> { null }
|
|
215
|
+
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
package vn.momo.kits.navigation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Box
|
|
4
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
5
|
+
import androidx.compose.runtime.Composable
|
|
6
|
+
import androidx.compose.runtime.MutableState
|
|
7
|
+
import androidx.compose.runtime.mutableStateOf
|
|
8
|
+
import androidx.compose.runtime.staticCompositionLocalOf
|
|
9
|
+
import androidx.compose.ui.Alignment
|
|
10
|
+
import androidx.compose.ui.Modifier
|
|
11
|
+
import androidx.navigation.NavController
|
|
12
|
+
import androidx.navigation.toRoute
|
|
13
|
+
import kotlinx.coroutines.CoroutineScope
|
|
14
|
+
import kotlinx.coroutines.Dispatchers
|
|
15
|
+
import kotlinx.coroutines.SupervisorJob
|
|
16
|
+
import kotlinx.coroutines.cancel
|
|
17
|
+
import kotlinx.coroutines.delay
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
import kotlinx.serialization.Serializable
|
|
20
|
+
import vn.momo.kits.components.PopupAction
|
|
21
|
+
import vn.momo.kits.components.PopupNotify
|
|
22
|
+
import vn.momo.kits.navigation.BottomHeader.*
|
|
23
|
+
import vn.momo.kits.navigation.component.SnackBar
|
|
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
|
|
28
|
+
import kotlin.time.Duration.Companion.milliseconds
|
|
29
|
+
|
|
30
|
+
class Navigator(
|
|
31
|
+
private val navController: NavController,
|
|
32
|
+
private val maxApi: IMaxApi?,
|
|
33
|
+
private val registry: DynamicScreenRegistry
|
|
34
|
+
) {
|
|
35
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
36
|
+
|
|
37
|
+
// currentScreen() reads the root back stack, so it never sees the nested tab NavHost.
|
|
38
|
+
internal var tabBackHandler: (() -> Unit)? = null
|
|
39
|
+
|
|
40
|
+
private fun currentScreen(): DynamicScreen? {
|
|
41
|
+
val entry = navController.currentBackStackEntry ?: return null
|
|
42
|
+
val id = runCatching { entry.toRoute<DynamicScreenRoute>().id }.getOrNull()
|
|
43
|
+
?: runCatching { entry.toRoute<DynamicDialogRoute>().id }.getOrNull()
|
|
44
|
+
?: return null
|
|
45
|
+
return registry.getScreen(id)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun push(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
49
|
+
val route = registry.register(screenName, content, options)
|
|
50
|
+
navController.navigate(DynamicScreenRoute(route.id))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun replace(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
54
|
+
val replacedScreen = currentScreen()
|
|
55
|
+
replacedScreen?.let { registry.unregisterScreen(it.id) }
|
|
56
|
+
navController.popBackStack()
|
|
57
|
+
push(screenName, content, options)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun onBackSafe(callBack: (() -> Unit)? = null) {
|
|
61
|
+
// An open modal or bottom sheet takes the back first, as its BackHandler does for gestures.
|
|
62
|
+
val overplayType = OverplayComponentRegistry.getOverplayType()
|
|
63
|
+
if (overplayType != null && overplayType != OverplayComponentType.SNACK_BAR) {
|
|
64
|
+
pop(1, callBack)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
tabBackHandler?.let {
|
|
68
|
+
it()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
val currentScreen = currentScreen()
|
|
72
|
+
val options = currentScreen?.options
|
|
73
|
+
if (options?.onBackHandler != null) {
|
|
74
|
+
options.onBackHandler.invoke()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
val preventBack = options?.headerBackProps?.preventBack
|
|
78
|
+
if (preventBack != null) {
|
|
79
|
+
showModal(
|
|
80
|
+
content = {
|
|
81
|
+
PopupNotify(preventBack.copy(
|
|
82
|
+
primary = PopupAction(
|
|
83
|
+
title = preventBack.primary?.title ?: "",
|
|
84
|
+
onPress = {
|
|
85
|
+
preventBack.primary?.onPress?.invoke()
|
|
86
|
+
pop(2, callBack) // close popup notify, then close the screen
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
))
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
} else {
|
|
93
|
+
pop(1, callBack)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The swipe-back opt-out predicate in StackScreen needs this: the root screen must keep custom
|
|
98
|
+
// back handling so it still reaches maxApi.dismiss.
|
|
99
|
+
// Public: the iOS host asks this to decide whether the kit owns the swipe on this screen.
|
|
100
|
+
fun canPop(): Boolean = navController.previousBackStackEntry != null
|
|
101
|
+
|
|
102
|
+
fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
|
|
103
|
+
scope.launch {
|
|
104
|
+
repeat(count) {
|
|
105
|
+
if (OverplayComponentRegistry.getOverplayType() == OverplayComponentType.SNACK_BAR){
|
|
106
|
+
dismissScreen()
|
|
107
|
+
OverplayComponentRegistry.hardClearAfterDismiss()
|
|
108
|
+
}
|
|
109
|
+
else if (OverplayComponentRegistry.isOverplayShowing()){
|
|
110
|
+
dismissOverplay()
|
|
111
|
+
} else {
|
|
112
|
+
dismissScreen()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
callBack?.invoke()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
private suspend fun dismissOverplay(isDelay: Boolean = true) {
|
|
119
|
+
OverplayComponentRegistry.clear()
|
|
120
|
+
if (isDelay) delay(300L.milliseconds)
|
|
121
|
+
OverplayComponentRegistry.hardClearAfterDismiss()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private suspend fun dismissScreen() {
|
|
125
|
+
if (navController.previousBackStackEntry != null){
|
|
126
|
+
val dismissing = currentScreen()
|
|
127
|
+
navController.popBackStack()
|
|
128
|
+
delay(STACK_TRANSITION_DURATION.milliseconds)
|
|
129
|
+
dismissing?.let { registry.unregisterScreen(it.id) }
|
|
130
|
+
} else {
|
|
131
|
+
// AI-GENERATED START: preserve root-screen dismissal
|
|
132
|
+
maxApi?.dismiss(DismissTarget.Current()) {}
|
|
133
|
+
// AI-GENERATED END: preserve root-screen dismissal
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fun present(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
138
|
+
val route = registry.register(screenName, content, options)
|
|
139
|
+
navController.navigate(DynamicDialogRoute(route.id))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fun reset(screenName: String, content: @Composable () -> Unit, options: NavigationOptions? = null) {
|
|
143
|
+
registry.unregisterAll()
|
|
144
|
+
|
|
145
|
+
val route = registry.register(screenName, content, options)
|
|
146
|
+
navController.navigate(DynamicScreenRoute(route.id)) {
|
|
147
|
+
popUpTo(0) { inclusive = true }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fun showModal(
|
|
152
|
+
content: @Composable () -> Unit,
|
|
153
|
+
barrierDismissible: Boolean = true,
|
|
154
|
+
onDismiss: (() -> Unit)? = null
|
|
155
|
+
){
|
|
156
|
+
val id = currentScreen()?.id ?: -1
|
|
157
|
+
OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.MODAL, false, barrierDismissible, onDismiss)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fun showBottomSheet(
|
|
161
|
+
content: @Composable () -> Unit,
|
|
162
|
+
isSurface: Boolean = false,
|
|
163
|
+
barrierDismissible: Boolean = true,
|
|
164
|
+
onDismiss: (() -> Unit)? = null,
|
|
165
|
+
bottomSheetHeader: BottomHeader? = null,
|
|
166
|
+
draggable: Boolean = true,
|
|
167
|
+
useBottomInset: Boolean = true,
|
|
168
|
+
footerComponent: (@Composable () -> Unit)? = null
|
|
169
|
+
){
|
|
170
|
+
val id = currentScreen()?.id ?: -1
|
|
171
|
+
OverplayComponentRegistry.registerOverplay(id, content, OverplayComponentType.BOTTOM_SHEET, isSurface, barrierDismissible, onDismiss, bottomSheetHeader, draggable, useBottomInset, footerComponent)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fun showSnackBar(snackBar: SnackBar, onDismiss: (() -> Unit)? = null) {
|
|
175
|
+
val id = currentScreen()?.id ?: -1
|
|
176
|
+
scope.launch {
|
|
177
|
+
OverplayComponentRegistry.registerOverplay(
|
|
178
|
+
id = id,
|
|
179
|
+
content = {
|
|
180
|
+
SnackBar(snackBar, onDismiss)
|
|
181
|
+
},
|
|
182
|
+
type = OverplayComponentType.SNACK_BAR,
|
|
183
|
+
onDismiss = onDismiss
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fun hideSnackBar() {
|
|
189
|
+
scope.launch {
|
|
190
|
+
OverplayComponentRegistry.clear()
|
|
191
|
+
delay(250L.milliseconds)
|
|
192
|
+
OverplayComponentRegistry.hardClearAfterDismiss()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fun dispose(){
|
|
197
|
+
scope.cancel()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
val LocalNavigator = staticCompositionLocalOf<Navigator> {
|
|
202
|
+
error("No NavigationStack provided")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Serializable
|
|
206
|
+
data class DynamicScreenRoute(val id: Int)
|
|
207
|
+
|
|
208
|
+
@Serializable
|
|
209
|
+
data class DynamicDialogRoute(val id: Int)
|
|
210
|
+
|
|
211
|
+
data class DynamicScreen(
|
|
212
|
+
val id: Int,
|
|
213
|
+
val name: String,
|
|
214
|
+
val content: @Composable () -> Unit,
|
|
215
|
+
var options: NavigationOptions? = null
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
class DynamicScreenRegistry {
|
|
219
|
+
private val screens = mutableMapOf<Int, DynamicScreen>()
|
|
220
|
+
|
|
221
|
+
fun register(screenName: String, content: @Composable () -> Unit, options: NavigationOptions?): DynamicScreenRoute {
|
|
222
|
+
val id = nextGlobalId()
|
|
223
|
+
screens[id] = DynamicScreen(
|
|
224
|
+
id = id,
|
|
225
|
+
name = screenName,
|
|
226
|
+
content = content,
|
|
227
|
+
options = options
|
|
228
|
+
)
|
|
229
|
+
return DynamicScreenRoute(id)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fun nextId(): Int = nextGlobalId()
|
|
233
|
+
|
|
234
|
+
fun bind(id: Int, screenName: String, content: @Composable () -> Unit, options: NavigationOptions?) {
|
|
235
|
+
screens[id] = DynamicScreen(
|
|
236
|
+
id = id,
|
|
237
|
+
name = screenName,
|
|
238
|
+
content = content,
|
|
239
|
+
options = options
|
|
240
|
+
)
|
|
241
|
+
advanceGlobalIdPast(id) // keep counter ahead after process-death restore
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fun unregisterScreen(id: Int) {
|
|
245
|
+
screens.remove(id)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Gesture pops go through composeNavigator.popBackStack() inside NavHost and never reach
|
|
249
|
+
// Navigator.dismissScreen(), so screens would leak without a stack-driven prune.
|
|
250
|
+
fun retainOnly(ids: Set<Int>) {
|
|
251
|
+
val stale = screens.keys.filter { it !in ids }
|
|
252
|
+
for (id in stale) {
|
|
253
|
+
screens.remove(id)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fun unregisterAll(){
|
|
258
|
+
val ids = screens.keys.toList()
|
|
259
|
+
for (id in ids) {
|
|
260
|
+
unregisterScreen(id)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fun getScreen(id: Int): DynamicScreen? = screens[id]
|
|
265
|
+
|
|
266
|
+
fun setOptions(id: Int, options: NavigationOptions){
|
|
267
|
+
screens[id]?.options = options
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
companion object {
|
|
271
|
+
private var globalIdCounter = 1
|
|
272
|
+
|
|
273
|
+
private fun nextGlobalId(): Int = globalIdCounter++
|
|
274
|
+
|
|
275
|
+
private fun advanceGlobalIdPast(id: Int) {
|
|
276
|
+
if (id >= globalIdCounter) globalIdCounter = id + 1
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
val LocalDynamicScreenRegistry = staticCompositionLocalOf<DynamicScreenRegistry?> { null }
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
sealed class OverplayComponentParams {
|
|
285
|
+
class Modal(
|
|
286
|
+
val onDismiss: (() -> Unit)? = null,
|
|
287
|
+
val barrierDismissible: Boolean = true
|
|
288
|
+
) : OverplayComponentParams()
|
|
289
|
+
|
|
290
|
+
class BottomSheet(
|
|
291
|
+
val isSurface: Boolean = false,
|
|
292
|
+
val onDismiss: (() -> Unit)? = null,
|
|
293
|
+
val barrierDismissible: Boolean = true,
|
|
294
|
+
val bottomSheetHeader: BottomHeader? = null,
|
|
295
|
+
val draggable: Boolean = true,
|
|
296
|
+
val useBottomInset: Boolean = true,
|
|
297
|
+
val footerComponent: (@Composable () -> Unit)? = null,
|
|
298
|
+
) : OverplayComponentParams()
|
|
299
|
+
|
|
300
|
+
class SnackBar : OverplayComponentParams()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
data class OverplayComponent(
|
|
304
|
+
val id: Int,
|
|
305
|
+
val type: OverplayComponentType? = null,
|
|
306
|
+
val content: @Composable () -> Unit,
|
|
307
|
+
val params: OverplayComponentParams? = null
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
enum class OverplayComponentType {
|
|
311
|
+
MODAL, BOTTOM_SHEET, SNACK_BAR
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
object OverplayComponentRegistry {
|
|
315
|
+
private var currentOverlayComponent : MutableState<OverplayComponent?> = mutableStateOf(null)
|
|
316
|
+
private var requestClose: (() -> Unit)? = null
|
|
317
|
+
internal fun bindClose(handler: (() -> Unit)?) {
|
|
318
|
+
requestClose = handler
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fun registerOverplay(
|
|
322
|
+
id: Int,
|
|
323
|
+
content: @Composable () -> Unit,
|
|
324
|
+
type: OverplayComponentType,
|
|
325
|
+
isSurface: Boolean = false,
|
|
326
|
+
barrierDismissible: Boolean = true,
|
|
327
|
+
onDismiss: (() -> Unit)?,
|
|
328
|
+
bottomSheetHeader: BottomHeader? = null,
|
|
329
|
+
draggable: Boolean = true,
|
|
330
|
+
useBottomInset: Boolean = true,
|
|
331
|
+
footerComponent: (@Composable () -> Unit)? = null,
|
|
332
|
+
){
|
|
333
|
+
val params = when(type){
|
|
334
|
+
OverplayComponentType.MODAL -> OverplayComponentParams.Modal(onDismiss, barrierDismissible)
|
|
335
|
+
OverplayComponentType.BOTTOM_SHEET -> OverplayComponentParams.BottomSheet(isSurface, onDismiss, barrierDismissible, bottomSheetHeader, draggable, useBottomInset, footerComponent)
|
|
336
|
+
OverplayComponentType.SNACK_BAR -> OverplayComponentParams.SnackBar()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
currentOverlayComponent.value = OverplayComponent(
|
|
340
|
+
id = id,
|
|
341
|
+
type = type,
|
|
342
|
+
content = content,
|
|
343
|
+
params = params
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fun isOverplayShowing(): Boolean = currentOverlayComponent.value != null
|
|
348
|
+
|
|
349
|
+
fun getOverplayType(): OverplayComponentType? = currentOverlayComponent.value?.type
|
|
350
|
+
|
|
351
|
+
fun currentRootId(): Int? = currentOverlayComponent.value?.id
|
|
352
|
+
|
|
353
|
+
fun clear(){
|
|
354
|
+
if (requestClose != null) {
|
|
355
|
+
requestClose?.invoke()
|
|
356
|
+
} else {
|
|
357
|
+
currentOverlayComponent.value = null
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
internal fun hardClearAfterDismiss() {
|
|
362
|
+
currentOverlayComponent.value = null
|
|
363
|
+
requestClose = null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@Composable
|
|
367
|
+
internal fun OverlayComponent(){
|
|
368
|
+
val overplay = currentOverlayComponent.value ?: return
|
|
369
|
+
|
|
370
|
+
when (val params = overplay.params) {
|
|
371
|
+
is OverplayComponentParams.BottomSheet -> {
|
|
372
|
+
BottomSheet(
|
|
373
|
+
content = overplay.content,
|
|
374
|
+
header = params.bottomSheetHeader ?: Title(),
|
|
375
|
+
isSurface = params.isSurface,
|
|
376
|
+
barrierDismissible = params.barrierDismissible,
|
|
377
|
+
draggable = params.draggable,
|
|
378
|
+
useBottomInset = params.useBottomInset,
|
|
379
|
+
footerComponent = params.footerComponent,
|
|
380
|
+
onDismiss = params.onDismiss
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
is OverplayComponentParams.Modal -> {
|
|
385
|
+
ModalScreen(
|
|
386
|
+
content = overplay.content,
|
|
387
|
+
barrierDismissible = params.barrierDismissible,
|
|
388
|
+
onDismiss = params.onDismiss
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
is OverplayComponentParams.SnackBar -> {
|
|
393
|
+
Box(
|
|
394
|
+
modifier = Modifier.fillMaxSize(),
|
|
395
|
+
contentAlignment = Alignment.BottomCenter
|
|
396
|
+
) {
|
|
397
|
+
overplay.content()
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
null -> {}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|