create-cmp-cli 0.2.0
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/LICENSE +21 -0
- package/README.md +232 -0
- package/bin/create-cmp.mjs +91 -0
- package/options.schema.json +110 -0
- package/package.json +52 -0
- package/src/bootstrap/checks.mjs +283 -0
- package/src/bootstrap/exec.mjs +90 -0
- package/src/commands/clean.mjs +195 -0
- package/src/commands/create.mjs +223 -0
- package/src/commands/doctor.mjs +256 -0
- package/src/commands/upgrade.mjs +196 -0
- package/src/commands/verify.mjs +94 -0
- package/src/doctor.mjs +109 -0
- package/src/lib/args.mjs +38 -0
- package/src/lib/clean.mjs +72 -0
- package/src/lib/fsutil.mjs +90 -0
- package/src/lib/log.mjs +40 -0
- package/src/lib/project-doctor.mjs +329 -0
- package/src/lib/registry.mjs +102 -0
- package/src/lib/rename.mjs +116 -0
- package/src/lib/schema.mjs +123 -0
- package/src/lib/toggle.mjs +130 -0
- package/src/lib/tokens.mjs +96 -0
- package/src/lib/toml.mjs +137 -0
- package/src/lib/upgrade.mjs +159 -0
- package/src/lib/verify.mjs +97 -0
- package/src/scaffold.mjs +347 -0
- package/src/verify.mjs +4 -0
- package/src/versions/registry.json +42 -0
- package/template/.github/workflows/verify.yml +70 -0
- package/template/build.gradle.kts +17 -0
- package/template/composeApp/build.gradle.kts +257 -0
- package/template/composeApp/google-services.json +29 -0
- package/template/composeApp/proguard-rules.pro +34 -0
- package/template/composeApp/src/androidDebug/AndroidManifest.xml +13 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/ComposeRootRegistry.kt +40 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorCatalog.kt +80 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +322 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +16 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt +102 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/RemoteControlPage.kt +106 -0
- package/template/composeApp/src/androidDebug/res/xml/debug_network_security_config.xml +9 -0
- package/template/composeApp/src/androidMain/AndroidManifest.xml +28 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/AppApplication.kt +72 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/MainActivity.kt +24 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +42 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/data/local/DatabaseBuilder.android.kt +13 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/di/AndroidModule.kt +9 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/presentation/components/TestTagAutomation.android.kt +11 -0
- package/template/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml +9 -0
- package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/values/colors.xml +6 -0
- package/template/composeApp/src/androidMain/res/values/themes.xml +9 -0
- package/template/composeApp/src/androidMain/res/values-v31/themes.xml +8 -0
- package/template/composeApp/src/androidRelease/kotlin/com/example/app/inspector/InspectorInit.kt +13 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Bold.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Medium.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Regular.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_SemiBold.ttf +0 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/App.kt +7 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +7 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/AppDatabase.kt +28 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/DatabaseBuilder.kt +14 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/ItemDao.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/FirebaseConfig.kt +5 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +22 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +23 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/Item.kt +8 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +8 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +12 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/App.kt +13 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +69 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +39 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +87 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +35 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +43 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +144 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppTab.kt +25 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +15 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +43 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/DesignToken.kt +25 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Shape.kt +12 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Theme.kt +64 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Tokens.kt +19 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt +36 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.desktop.kt +9 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/data/local/DatabaseBuilder.desktop.kt +13 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/di/DesktopModule.kt +32 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/main.kt +29 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/presentation/components/TestTagAutomation.desktop.kt +6 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/KoinHelper.kt +50 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/MainViewController.kt +10 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +32 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/data/local/DatabaseBuilder.ios.kt +10 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/presentation/components/TestTagAutomation.ios.kt +11 -0
- package/template/docs/dev-client.md +52 -0
- package/template/gitignore +21 -0
- package/template/gradle/libs.versions.toml +100 -0
- package/template/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/template/gradle.properties +9 -0
- package/template/gradlew +152 -0
- package/template/gradlew.bat +94 -0
- package/template/iosApp/Podfile +27 -0
- package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png +0 -0
- package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/template/iosApp/iosApp/Assets.xcassets/Contents.json +6 -0
- package/template/iosApp/iosApp/ContentView.swift +17 -0
- package/template/iosApp/iosApp/GoogleService-Info.plist +34 -0
- package/template/iosApp/iosApp/Info.plist +43 -0
- package/template/iosApp/iosApp/PrivacyInfo.xcprivacy +42 -0
- package/template/iosApp/iosApp/iOSApp.swift +33 -0
- package/template/iosApp/project.yml +56 -0
- package/template/local.properties.example +2 -0
- package/template/manifest.json +125 -0
- package/template/qa/appium/README.md +23 -0
- package/template/qa/appium/lib/appium-client.mjs +225 -0
- package/template/qa/appium/package.json +8 -0
- package/template/qa/appium/run-android-smoke.mjs +39 -0
- package/template/settings.gradle.kts +34 -0
- package/template/tests/appium/cmp/conftest.py +96 -0
- package/template/tests/appium/cmp/test_smoke.py +17 -0
package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.home
|
|
2
|
+
|
|
3
|
+
import androidx.lifecycle.ViewModel
|
|
4
|
+
import androidx.lifecycle.viewModelScope
|
|
5
|
+
import __PACKAGE__.domain.model.Item
|
|
6
|
+
import __PACKAGE__.domain.usecase.GetItemsUseCase
|
|
7
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
8
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
9
|
+
import kotlinx.coroutines.flow.asStateFlow
|
|
10
|
+
import kotlinx.coroutines.launch
|
|
11
|
+
|
|
12
|
+
data class HomeUiState(
|
|
13
|
+
val isLoading: Boolean = true,
|
|
14
|
+
val items: List<Item> = emptyList(),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class HomeViewModel(
|
|
18
|
+
private val getItems: GetItemsUseCase,
|
|
19
|
+
) : ViewModel() {
|
|
20
|
+
|
|
21
|
+
private val _state = MutableStateFlow(HomeUiState())
|
|
22
|
+
val state: StateFlow<HomeUiState> = _state.asStateFlow()
|
|
23
|
+
|
|
24
|
+
init {
|
|
25
|
+
load()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun load() {
|
|
29
|
+
viewModelScope.launch {
|
|
30
|
+
_state.value = HomeUiState(isLoading = true)
|
|
31
|
+
val items = getItems()
|
|
32
|
+
_state.value = HomeUiState(isLoading = false, items = items)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.navigation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.navigation.NavType
|
|
5
|
+
import androidx.navigation.compose.NavHost
|
|
6
|
+
import androidx.navigation.compose.composable
|
|
7
|
+
import androidx.navigation.compose.rememberNavController
|
|
8
|
+
import androidx.navigation.navArgument
|
|
9
|
+
// Nav 2.9 (multiplatform): backStackEntry.arguments is a SavedState, not an Android Bundle.
|
|
10
|
+
// Read it via the androidx.savedstate.read extension, NOT Bundle.getString().
|
|
11
|
+
import androidx.savedstate.read
|
|
12
|
+
import __PACKAGE__.presentation.home.HomeScreen
|
|
13
|
+
import __PACKAGE__.presentation.profile.ProfileScreen
|
|
14
|
+
|
|
15
|
+
@Composable
|
|
16
|
+
fun AppNavHost() {
|
|
17
|
+
val navController = rememberNavController()
|
|
18
|
+
|
|
19
|
+
NavHost(navController = navController, startDestination = Screen.Shell.route) {
|
|
20
|
+
composable(Screen.Shell.route) {
|
|
21
|
+
val tabs = appTabs(
|
|
22
|
+
home = {
|
|
23
|
+
HomeScreen(
|
|
24
|
+
onItemClick = { itemId -> navController.navigate(Routes.detail(itemId)) },
|
|
25
|
+
)
|
|
26
|
+
},
|
|
27
|
+
profile = { ProfileScreen() },
|
|
28
|
+
)
|
|
29
|
+
AppShell(tabs = tabs)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
composable(
|
|
33
|
+
route = Screen.Detail.route,
|
|
34
|
+
arguments = listOf(navArgument("itemId") { type = NavType.StringType }),
|
|
35
|
+
) { backStackEntry ->
|
|
36
|
+
val itemId = backStackEntry.arguments?.read { getStringOrNull("itemId") }.orEmpty()
|
|
37
|
+
__PACKAGE__.presentation.home.DetailScreen(
|
|
38
|
+
itemId = itemId,
|
|
39
|
+
onBack = { navController.popBackStack() },
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.navigation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.clickable
|
|
5
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
6
|
+
import androidx.compose.foundation.layout.Box
|
|
7
|
+
import androidx.compose.foundation.layout.Column
|
|
8
|
+
import androidx.compose.foundation.layout.Row
|
|
9
|
+
import androidx.compose.foundation.layout.defaultMinSize
|
|
10
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
11
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
12
|
+
import androidx.compose.foundation.layout.height
|
|
13
|
+
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
14
|
+
import androidx.compose.foundation.layout.padding
|
|
15
|
+
import androidx.compose.foundation.layout.size
|
|
16
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
17
|
+
import androidx.compose.material3.Icon
|
|
18
|
+
import androidx.compose.material3.Text
|
|
19
|
+
import androidx.compose.runtime.Composable
|
|
20
|
+
import androidx.compose.runtime.getValue
|
|
21
|
+
import androidx.compose.runtime.mutableIntStateOf
|
|
22
|
+
import androidx.compose.runtime.saveable.rememberSaveable
|
|
23
|
+
import androidx.compose.runtime.setValue
|
|
24
|
+
import androidx.compose.ui.Alignment
|
|
25
|
+
import androidx.compose.ui.Modifier
|
|
26
|
+
import androidx.compose.ui.draw.clip
|
|
27
|
+
import androidx.compose.ui.semantics.semantics
|
|
28
|
+
import androidx.compose.ui.semantics.testTag
|
|
29
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
30
|
+
import androidx.compose.ui.unit.dp
|
|
31
|
+
import androidx.compose.ui.unit.sp
|
|
32
|
+
import __PACKAGE__.presentation.components.BaseScreen
|
|
33
|
+
import __PACKAGE__.presentation.components.exposeTestTagsForAutomation
|
|
34
|
+
import __PACKAGE__.presentation.theme.__THEME_PREFIX__Colors
|
|
35
|
+
import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
|
|
36
|
+
import __PACKAGE__.presentation.theme.designToken
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generic bottom-nav shell. Parameterized by a [tabs] list — NOT role-hardcoded.
|
|
40
|
+
* Hosts the selected tab's content inside a [BaseScreen] so each tab gets correct insets;
|
|
41
|
+
* the bottom bar reserves the navigation-bar inset exactly once.
|
|
42
|
+
*/
|
|
43
|
+
@Composable
|
|
44
|
+
fun AppShell(tabs: List<AppTab>) {
|
|
45
|
+
var selected by rememberSaveable { mutableIntStateOf(0) }
|
|
46
|
+
|
|
47
|
+
BaseScreen(
|
|
48
|
+
// Expose Compose testTags to the platform automation layer (Android resource-ids /
|
|
49
|
+
// iOS accessibilityIdentifiers) so Appium/uiautomator id-selectors and the
|
|
50
|
+
// cmp-test-generated suites find them; covers the whole subtree. Desktop: no-op.
|
|
51
|
+
modifier = Modifier.exposeTestTagsForAutomation(),
|
|
52
|
+
// The bottom bar owns the navigation-bar inset; the body must not also pad it.
|
|
53
|
+
applyNavBarPadding = false,
|
|
54
|
+
bottomBar = {
|
|
55
|
+
AppBottomNav(
|
|
56
|
+
tabs = tabs,
|
|
57
|
+
selectedIndex = selected,
|
|
58
|
+
onSelect = { selected = it },
|
|
59
|
+
)
|
|
60
|
+
},
|
|
61
|
+
) {
|
|
62
|
+
Box(Modifier.fillMaxSize()) {
|
|
63
|
+
tabs.getOrNull(selected)?.content?.invoke()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Composable
|
|
69
|
+
private fun AppBottomNav(
|
|
70
|
+
tabs: List<AppTab>,
|
|
71
|
+
selectedIndex: Int,
|
|
72
|
+
onSelect: (Int) -> Unit,
|
|
73
|
+
) {
|
|
74
|
+
Column(Modifier.fillMaxWidth()) {
|
|
75
|
+
Box(
|
|
76
|
+
Modifier
|
|
77
|
+
.fillMaxWidth()
|
|
78
|
+
.height(1.dp)
|
|
79
|
+
.background(__THEME_PREFIX__Colors.OutlineVariant)
|
|
80
|
+
)
|
|
81
|
+
Row(
|
|
82
|
+
modifier = Modifier
|
|
83
|
+
.fillMaxWidth()
|
|
84
|
+
.background(__THEME_PREFIX__Colors.Surface)
|
|
85
|
+
// Lift tabs above the gesture pill / 3-button nav (maps to iOS safe area).
|
|
86
|
+
.navigationBarsPadding()
|
|
87
|
+
.height(__THEME_PREFIX__Tokens.BottomNavHeight)
|
|
88
|
+
// Inspector: the bottom-nav container self-reports its height token.
|
|
89
|
+
.designToken(
|
|
90
|
+
tokens = listOf("BottomNavHeight"),
|
|
91
|
+
resolved = mapOf("height" to "72dp"),
|
|
92
|
+
)
|
|
93
|
+
.semantics { testTag = "app_bottom_nav" }
|
|
94
|
+
.padding(bottom = 8.dp),
|
|
95
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
96
|
+
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
97
|
+
) {
|
|
98
|
+
tabs.forEachIndexed { index, tab ->
|
|
99
|
+
NavItem(
|
|
100
|
+
label = tab.label,
|
|
101
|
+
selected = selectedIndex == index,
|
|
102
|
+
onClick = { onSelect(index) },
|
|
103
|
+
) {
|
|
104
|
+
Icon(
|
|
105
|
+
imageVector = tab.icon,
|
|
106
|
+
contentDescription = tab.label,
|
|
107
|
+
tint = if (selectedIndex == index) __THEME_PREFIX__Colors.Primary
|
|
108
|
+
else __THEME_PREFIX__Colors.OnSurfaceVariant.copy(alpha = 0.55f),
|
|
109
|
+
modifier = Modifier.size(24.dp),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Composable
|
|
118
|
+
private fun NavItem(
|
|
119
|
+
label: String,
|
|
120
|
+
selected: Boolean,
|
|
121
|
+
onClick: () -> Unit,
|
|
122
|
+
icon: @Composable () -> Unit,
|
|
123
|
+
) {
|
|
124
|
+
Column(
|
|
125
|
+
modifier = Modifier
|
|
126
|
+
.clip(RoundedCornerShape(8.dp))
|
|
127
|
+
.clickable(onClick = onClick)
|
|
128
|
+
// a11y: guarantee the 48dp minimum touch target regardless of label width
|
|
129
|
+
// (the inspector's audit_a11y flags anything smaller).
|
|
130
|
+
.defaultMinSize(minWidth = 48.dp, minHeight = 48.dp)
|
|
131
|
+
.padding(horizontal = 8.dp, vertical = 4.dp),
|
|
132
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
133
|
+
verticalArrangement = Arrangement.Center,
|
|
134
|
+
) {
|
|
135
|
+
icon()
|
|
136
|
+
Text(
|
|
137
|
+
text = label,
|
|
138
|
+
fontSize = 10.sp,
|
|
139
|
+
fontWeight = if (selected) FontWeight.Bold else FontWeight.SemiBold,
|
|
140
|
+
color = if (selected) __THEME_PREFIX__Colors.Primary else __THEME_PREFIX__Colors.OnSurfaceVariant,
|
|
141
|
+
modifier = Modifier.padding(top = 4.dp),
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppTab.kt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.navigation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.material.icons.Icons
|
|
4
|
+
import androidx.compose.material.icons.filled.Home
|
|
5
|
+
import androidx.compose.material.icons.filled.Person
|
|
6
|
+
import androidx.compose.runtime.Composable
|
|
7
|
+
import androidx.compose.ui.graphics.vector.ImageVector
|
|
8
|
+
|
|
9
|
+
// A single bottom-nav tab: its label, icon, and the screen it renders.
|
|
10
|
+
data class AppTab(
|
|
11
|
+
val label: String,
|
|
12
|
+
val icon: ImageVector,
|
|
13
|
+
val content: @Composable () -> Unit,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// The tab list drives AppShell + AppBottomNav generically (no role-hardcoded shells).
|
|
17
|
+
// The scaffolder regenerates this list from the configured `tabs`.
|
|
18
|
+
@Composable
|
|
19
|
+
fun appTabs(
|
|
20
|
+
home: @Composable () -> Unit,
|
|
21
|
+
profile: @Composable () -> Unit,
|
|
22
|
+
): List<AppTab> = listOf(
|
|
23
|
+
AppTab("Home", Icons.Filled.Home, home),
|
|
24
|
+
AppTab("Profile", Icons.Filled.Person, profile),
|
|
25
|
+
)
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.navigation
|
|
2
|
+
|
|
3
|
+
// Type-safe route registry. Add a Screen object + a Routes constant per destination.
|
|
4
|
+
sealed class Screen(val route: String) {
|
|
5
|
+
data object Shell : Screen(Routes.SHELL)
|
|
6
|
+
// Example detail destination reachable from a tab. Add your own below.
|
|
7
|
+
data object Detail : Screen(Routes.DETAIL)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
object Routes {
|
|
11
|
+
const val SHELL = "shell"
|
|
12
|
+
const val DETAIL = "detail/{itemId}"
|
|
13
|
+
|
|
14
|
+
fun detail(itemId: String) = "detail/$itemId"
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.profile
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
4
|
+
import androidx.compose.foundation.layout.Column
|
|
5
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
6
|
+
import androidx.compose.foundation.layout.padding
|
|
7
|
+
import androidx.compose.material3.MaterialTheme
|
|
8
|
+
import androidx.compose.material3.Text
|
|
9
|
+
import androidx.compose.runtime.Composable
|
|
10
|
+
import androidx.compose.ui.Alignment
|
|
11
|
+
import androidx.compose.ui.Modifier
|
|
12
|
+
import androidx.compose.ui.semantics.semantics
|
|
13
|
+
import androidx.compose.ui.semantics.testTag
|
|
14
|
+
import androidx.compose.ui.unit.dp
|
|
15
|
+
import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
|
|
16
|
+
import __PACKAGE__.presentation.theme.designToken
|
|
17
|
+
|
|
18
|
+
// Feature stub. Copy the `home` feature's data→domain→presentation→DI wiring to flesh this out.
|
|
19
|
+
@Composable
|
|
20
|
+
fun ProfileScreen() {
|
|
21
|
+
Column(
|
|
22
|
+
modifier = Modifier
|
|
23
|
+
.fillMaxSize()
|
|
24
|
+
.designToken(
|
|
25
|
+
tokens = listOf("PaddingPage"),
|
|
26
|
+
resolved = mapOf("padding" to "16dp"),
|
|
27
|
+
)
|
|
28
|
+
.padding(__THEME_PREFIX__Tokens.PaddingPage),
|
|
29
|
+
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
30
|
+
horizontalAlignment = Alignment.Start,
|
|
31
|
+
) {
|
|
32
|
+
Text(
|
|
33
|
+
text = "Profile",
|
|
34
|
+
style = MaterialTheme.typography.headlineMedium,
|
|
35
|
+
modifier = Modifier.semantics { testTag = "profile_title" },
|
|
36
|
+
)
|
|
37
|
+
Text(
|
|
38
|
+
text = "This is a stub screen. Wire it up like the Home feature.",
|
|
39
|
+
style = MaterialTheme.typography.bodyMedium,
|
|
40
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/DesignToken.kt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.theme
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.Modifier
|
|
4
|
+
import androidx.compose.ui.semantics.SemanticsPropertyKey
|
|
5
|
+
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
|
|
6
|
+
import androidx.compose.ui.semantics.semantics
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The resolved design-token payload a component self-reports into the semantics tree.
|
|
10
|
+
*
|
|
11
|
+
* [tokens] are the declared token names (e.g. "RadiusCard"); [resolved] maps a facet
|
|
12
|
+
* (e.g. "radius", "color") to its concrete resolved value (e.g. "16dp", "#FFFFFFFF").
|
|
13
|
+
* This makes the semantics tree design-system-aware, not just geometry — which is what
|
|
14
|
+
* the create-cmp inspector reads to catch token drift. Only uses semantics + Modifier,
|
|
15
|
+
* so it adds no dependencies.
|
|
16
|
+
*/
|
|
17
|
+
data class DesignTokenInfo(val tokens: List<String>, val resolved: Map<String, String>)
|
|
18
|
+
|
|
19
|
+
val DesignTokenKey = SemanticsPropertyKey<DesignTokenInfo>("DesignToken")
|
|
20
|
+
|
|
21
|
+
var SemanticsPropertyReceiver.designToken by DesignTokenKey
|
|
22
|
+
|
|
23
|
+
/** Attaches the resolved design-token payload to a node's semantics. */
|
|
24
|
+
fun Modifier.designToken(tokens: List<String>, resolved: Map<String, String>): Modifier =
|
|
25
|
+
this.semantics { designToken = DesignTokenInfo(tokens, resolved) }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.theme
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
4
|
+
import androidx.compose.material3.Shapes
|
|
5
|
+
import androidx.compose.ui.unit.dp
|
|
6
|
+
|
|
7
|
+
val __THEME_PREFIX__Shapes = Shapes(
|
|
8
|
+
small = RoundedCornerShape(14.dp),
|
|
9
|
+
medium = RoundedCornerShape(16.dp),
|
|
10
|
+
large = RoundedCornerShape(999.dp),
|
|
11
|
+
extraLarge = RoundedCornerShape(24.dp),
|
|
12
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.theme
|
|
2
|
+
|
|
3
|
+
import androidx.compose.material3.MaterialTheme
|
|
4
|
+
import androidx.compose.material3.lightColorScheme
|
|
5
|
+
import androidx.compose.runtime.Composable
|
|
6
|
+
import androidx.compose.ui.graphics.Color
|
|
7
|
+
|
|
8
|
+
// Seed design tokens. Replace these with your own brand palette — every screen reads
|
|
9
|
+
// colours from here (or from MaterialTheme.colorScheme), never as raw hex.
|
|
10
|
+
object __THEME_PREFIX__Colors {
|
|
11
|
+
val Primary = Color(0xFF0A2540)
|
|
12
|
+
val OnPrimary = Color(0xFFFFFFFF)
|
|
13
|
+
val Accent = Color(0xFF00E676)
|
|
14
|
+
val OnAccent = Color(0xFF0A2540)
|
|
15
|
+
val Secondary = Color(0xFF00B96B)
|
|
16
|
+
val Error = Color(0xFFDC2626)
|
|
17
|
+
val Success = Color(0xFF16A34A)
|
|
18
|
+
val Warning = Color(0xFFF59E0B)
|
|
19
|
+
val Info = Color(0xFF2563EB)
|
|
20
|
+
val Background = Color(0xFFF7F9FC)
|
|
21
|
+
val Surface = Color(0xFFFFFFFF)
|
|
22
|
+
val SurfaceVariant = Color(0xFFE8EDF3)
|
|
23
|
+
val OnSurface = Color(0xFF1A1A1A)
|
|
24
|
+
val OnSurfaceVariant = Color(0xFF6B7280)
|
|
25
|
+
val Outline = Color(0xFF9CA3AF)
|
|
26
|
+
val OutlineVariant = Color(0xFFE5E7EB)
|
|
27
|
+
val Divider = Color(0xFFE5E7EB)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private val __THEME_PREFIX__ColorScheme = lightColorScheme(
|
|
31
|
+
primary = __THEME_PREFIX__Colors.Primary,
|
|
32
|
+
onPrimary = __THEME_PREFIX__Colors.OnPrimary,
|
|
33
|
+
secondary = __THEME_PREFIX__Colors.Secondary,
|
|
34
|
+
onSecondary = __THEME_PREFIX__Colors.OnPrimary,
|
|
35
|
+
tertiary = __THEME_PREFIX__Colors.Accent,
|
|
36
|
+
onTertiary = __THEME_PREFIX__Colors.OnAccent,
|
|
37
|
+
error = __THEME_PREFIX__Colors.Error,
|
|
38
|
+
background = __THEME_PREFIX__Colors.Background,
|
|
39
|
+
surface = __THEME_PREFIX__Colors.Surface,
|
|
40
|
+
surfaceVariant = __THEME_PREFIX__Colors.SurfaceVariant,
|
|
41
|
+
onBackground = __THEME_PREFIX__Colors.OnSurface,
|
|
42
|
+
onSurface = __THEME_PREFIX__Colors.OnSurface,
|
|
43
|
+
onSurfaceVariant = __THEME_PREFIX__Colors.OnSurfaceVariant,
|
|
44
|
+
outline = __THEME_PREFIX__Colors.Outline,
|
|
45
|
+
outlineVariant = __THEME_PREFIX__Colors.OutlineVariant,
|
|
46
|
+
inverseSurface = __THEME_PREFIX__Colors.Primary,
|
|
47
|
+
// Kill M3's tonal overlay so dialogs/menus/sheets stay on the design-system surface.
|
|
48
|
+
surfaceTint = Color.Transparent,
|
|
49
|
+
surfaceContainerLowest = __THEME_PREFIX__Colors.Surface,
|
|
50
|
+
surfaceContainerLow = __THEME_PREFIX__Colors.Surface,
|
|
51
|
+
surfaceContainer = __THEME_PREFIX__Colors.Surface,
|
|
52
|
+
surfaceContainerHigh = __THEME_PREFIX__Colors.Surface,
|
|
53
|
+
surfaceContainerHighest = __THEME_PREFIX__Colors.SurfaceVariant,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@Composable
|
|
57
|
+
fun __THEME_PREFIX__Theme(content: @Composable () -> Unit) {
|
|
58
|
+
MaterialTheme(
|
|
59
|
+
colorScheme = __THEME_PREFIX__ColorScheme,
|
|
60
|
+
typography = remember__THEME_PREFIX__Typography(),
|
|
61
|
+
shapes = __THEME_PREFIX__Shapes,
|
|
62
|
+
content = content,
|
|
63
|
+
)
|
|
64
|
+
}
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Tokens.kt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.theme
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.unit.dp
|
|
4
|
+
|
|
5
|
+
object __THEME_PREFIX__Tokens {
|
|
6
|
+
val ElevationCard = 2.dp
|
|
7
|
+
val ElevationModal = 8.dp
|
|
8
|
+
|
|
9
|
+
val PaddingPage = 16.dp
|
|
10
|
+
val PaddingCard = 16.dp
|
|
11
|
+
val GapCard = 12.dp
|
|
12
|
+
|
|
13
|
+
val BottomNavHeight = 72.dp
|
|
14
|
+
|
|
15
|
+
val RadiusCard = 16.dp
|
|
16
|
+
val RadiusPill = 999.dp
|
|
17
|
+
val RadiusModal = 24.dp
|
|
18
|
+
val RadiusInput = 14.dp
|
|
19
|
+
}
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.theme
|
|
2
|
+
|
|
3
|
+
import androidx.compose.material3.Typography
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.ui.text.TextStyle
|
|
6
|
+
import androidx.compose.ui.text.font.FontFamily
|
|
7
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
8
|
+
import androidx.compose.ui.unit.sp
|
|
9
|
+
import org.jetbrains.compose.resources.Font
|
|
10
|
+
import __PACKAGE__.generated.resources.DMSans_Bold
|
|
11
|
+
import __PACKAGE__.generated.resources.DMSans_Medium
|
|
12
|
+
import __PACKAGE__.generated.resources.DMSans_Regular
|
|
13
|
+
import __PACKAGE__.generated.resources.DMSans_SemiBold
|
|
14
|
+
import __PACKAGE__.generated.resources.Res
|
|
15
|
+
|
|
16
|
+
// DM Sans font family — weights 400/500/600/700 bundled in composeResources/font/.
|
|
17
|
+
val DmSansFontFamily: FontFamily
|
|
18
|
+
@Composable get() = FontFamily(
|
|
19
|
+
Font(Res.font.DMSans_Regular, weight = FontWeight.Normal),
|
|
20
|
+
Font(Res.font.DMSans_Medium, weight = FontWeight.Medium),
|
|
21
|
+
Font(Res.font.DMSans_SemiBold, weight = FontWeight.SemiBold),
|
|
22
|
+
Font(Res.font.DMSans_Bold, weight = FontWeight.Bold),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@Composable
|
|
26
|
+
fun remember__THEME_PREFIX__Typography(): Typography {
|
|
27
|
+
val dmSans = DmSansFontFamily
|
|
28
|
+
return Typography(
|
|
29
|
+
displayLarge = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.Bold, fontSize = 32.sp),
|
|
30
|
+
headlineMedium = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.SemiBold, fontSize = 24.sp),
|
|
31
|
+
titleMedium = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.SemiBold, fontSize = 18.sp),
|
|
32
|
+
bodyLarge = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.Normal, fontSize = 16.sp),
|
|
33
|
+
bodyMedium = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.Normal, fontSize = 14.sp),
|
|
34
|
+
labelSmall = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.Medium, fontSize = 12.sp),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
package __PACKAGE__.core.connectivity
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
4
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
5
|
+
|
|
6
|
+
// Desktop dev-client: treat the workstation as always online.
|
|
7
|
+
actual class NetworkMonitor actual constructor(context: Any?) {
|
|
8
|
+
actual val isOnline: StateFlow<Boolean> = MutableStateFlow(true)
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package __PACKAGE__.data.local
|
|
2
|
+
|
|
3
|
+
import androidx.room.Room
|
|
4
|
+
import androidx.room.RoomDatabase
|
|
5
|
+
import java.io.File
|
|
6
|
+
|
|
7
|
+
// Room KMP on the JVM uses the same BundledSQLiteDriver as iOS (set in buildDatabase). The
|
|
8
|
+
// dev-client keeps its cache in the OS temp dir — disposable by design, like the dev loop itself.
|
|
9
|
+
actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
|
|
10
|
+
val dbDir = File(System.getProperty("java.io.tmpdir"), "__PACKAGE__-dev-client")
|
|
11
|
+
dbDir.mkdirs()
|
|
12
|
+
return Room.databaseBuilder<AppDatabase>(name = File(dbDir, "app.db").absolutePath)
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package __PACKAGE__.di
|
|
2
|
+
|
|
3
|
+
import __PACKAGE__.core.connectivity.NetworkMonitor
|
|
4
|
+
// >>> cmp:feature room
|
|
5
|
+
import __PACKAGE__.data.local.AppDatabase
|
|
6
|
+
import __PACKAGE__.data.local.buildDatabase
|
|
7
|
+
// <<< cmp:feature room
|
|
8
|
+
import org.koin.core.context.startKoin
|
|
9
|
+
import org.koin.dsl.module
|
|
10
|
+
|
|
11
|
+
// Desktop (JVM) DI bindings for the dev-client window.
|
|
12
|
+
//
|
|
13
|
+
// No Firebase here — the dev-client runs fully offline. The example feature's data source
|
|
14
|
+
// (ItemRepositoryImpl, bound in appModules) is already an in-memory implementation serving the
|
|
15
|
+
// same sample data on every platform, so nothing needs swapping out of the box. When you add a
|
|
16
|
+
// real remote-backed repository, bind its desktop fake in THIS module so the dev-client keeps
|
|
17
|
+
// working without a backend:
|
|
18
|
+
//
|
|
19
|
+
// single<MyRepository> { InMemoryMyRepository() } // shadows the remote binding on desktop
|
|
20
|
+
val desktopModule = module {
|
|
21
|
+
// >>> cmp:feature room
|
|
22
|
+
single<AppDatabase> { buildDatabase() }
|
|
23
|
+
// <<< cmp:feature room
|
|
24
|
+
single { NetworkMonitor(null) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Mirrors AppApplication (Android) / KoinHelper (iOS) — Koin start for the desktop entry point.
|
|
28
|
+
fun initDesktopKoin() {
|
|
29
|
+
startKoin {
|
|
30
|
+
modules(desktopModule, *appModules.toTypedArray())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package __PACKAGE__
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.unit.DpSize
|
|
4
|
+
import androidx.compose.ui.unit.dp
|
|
5
|
+
import androidx.compose.ui.window.Window
|
|
6
|
+
import androidx.compose.ui.window.application
|
|
7
|
+
import androidx.compose.ui.window.rememberWindowState
|
|
8
|
+
import __PACKAGE__.di.initDesktopKoin
|
|
9
|
+
|
|
10
|
+
// Desktop dev-client: the shared commonMain UI in a live, clickable JVM window, sized like a
|
|
11
|
+
// phone. This is the daily dev loop — run it with Compose Hot Reload and watch edits land
|
|
12
|
+
// without restarting:
|
|
13
|
+
//
|
|
14
|
+
// ./gradlew :composeApp:hotRunDesktop --auto # hot reload, auto-reload on save
|
|
15
|
+
// ./gradlew :composeApp:run # plain run (any JDK, no hot reload)
|
|
16
|
+
//
|
|
17
|
+
// See docs/dev-client.md for the full loop.
|
|
18
|
+
fun main() {
|
|
19
|
+
initDesktopKoin()
|
|
20
|
+
application {
|
|
21
|
+
Window(
|
|
22
|
+
onCloseRequest = ::exitApplication,
|
|
23
|
+
title = "__APP_NAME__ dev-client",
|
|
24
|
+
state = rememberWindowState(size = DpSize(411.dp, 891.dp)),
|
|
25
|
+
) {
|
|
26
|
+
App()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package __PACKAGE__
|
|
2
|
+
|
|
3
|
+
import __PACKAGE__.core.connectivity.NetworkMonitor
|
|
4
|
+
// >>> cmp:feature room
|
|
5
|
+
import __PACKAGE__.data.local.AppDatabase
|
|
6
|
+
import __PACKAGE__.data.local.buildDatabase
|
|
7
|
+
// <<< cmp:feature room
|
|
8
|
+
// >>> cmp:feature firebase
|
|
9
|
+
import __PACKAGE__.data.remote.FIREBASE_FUNCTIONS_REGION
|
|
10
|
+
import dev.gitlive.firebase.Firebase
|
|
11
|
+
import dev.gitlive.firebase.auth.auth
|
|
12
|
+
import dev.gitlive.firebase.firestore.firestore
|
|
13
|
+
import dev.gitlive.firebase.functions.functions
|
|
14
|
+
import dev.gitlive.firebase.storage.storage
|
|
15
|
+
// <<< cmp:feature firebase
|
|
16
|
+
import __PACKAGE__.di.appModules
|
|
17
|
+
import org.koin.core.context.startKoin
|
|
18
|
+
import org.koin.dsl.module
|
|
19
|
+
|
|
20
|
+
// >>> cmp:feature firebase
|
|
21
|
+
// Debug/QA: point GitLive Firebase at the local emulators. The iOS simulator shares the host
|
|
22
|
+
// network, so 127.0.0.1 reaches the emulators directly. Requires FirebaseApp.configure() to
|
|
23
|
+
// have run first (done in iOSApp.swift AppDelegate).
|
|
24
|
+
private fun configureFirebaseEmulators() {
|
|
25
|
+
val host = "127.0.0.1"
|
|
26
|
+
runCatching {
|
|
27
|
+
Firebase.auth.useEmulator(host, 9099)
|
|
28
|
+
Firebase.firestore.useEmulator(host, 8080)
|
|
29
|
+
Firebase.functions(FIREBASE_FUNCTIONS_REGION).useEmulator(host, 5001)
|
|
30
|
+
Firebase.storage.useEmulator(host, 9199)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// <<< cmp:feature firebase
|
|
34
|
+
|
|
35
|
+
fun initKoin() {
|
|
36
|
+
// >>> cmp:feature firebase
|
|
37
|
+
configureFirebaseEmulators()
|
|
38
|
+
// <<< cmp:feature firebase
|
|
39
|
+
startKoin {
|
|
40
|
+
modules(
|
|
41
|
+
module {
|
|
42
|
+
// >>> cmp:feature room
|
|
43
|
+
single<AppDatabase> { buildDatabase() }
|
|
44
|
+
// <<< cmp:feature room
|
|
45
|
+
single { NetworkMonitor(null) }
|
|
46
|
+
},
|
|
47
|
+
*appModules.toTypedArray()
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
package __PACKAGE__
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.window.ComposeUIViewController
|
|
4
|
+
|
|
5
|
+
// Compose Multiplatform iOS entry point, bridged from ContentView.swift via ComposeView.
|
|
6
|
+
// CMP 1.10: the iOS accessibility tree syncs automatically, so Compose testTag values are
|
|
7
|
+
// exposed to XCUITest/Appium as accessibilityIdentifier without extra config.
|
|
8
|
+
fun MainViewController() = ComposeUIViewController {
|
|
9
|
+
App()
|
|
10
|
+
}
|
package/template/composeApp/src/iosMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package __PACKAGE__.core.connectivity
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
4
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
5
|
+
import kotlinx.coroutines.flow.asStateFlow
|
|
6
|
+
import platform.Network.nw_path_get_status
|
|
7
|
+
import platform.Network.nw_path_monitor_create
|
|
8
|
+
import platform.Network.nw_path_monitor_set_queue
|
|
9
|
+
import platform.Network.nw_path_monitor_set_update_handler
|
|
10
|
+
import platform.Network.nw_path_monitor_start
|
|
11
|
+
import platform.Network.nw_path_status_satisfied
|
|
12
|
+
import platform.darwin.DISPATCH_QUEUE_PRIORITY_DEFAULT
|
|
13
|
+
import platform.darwin.dispatch_get_global_queue
|
|
14
|
+
|
|
15
|
+
actual class NetworkMonitor actual constructor(context: Any?) {
|
|
16
|
+
|
|
17
|
+
// Kotlin/Native exposes the C nw_path_monitor API (the Obj-C NWPathMonitor wrapper isn't bridged).
|
|
18
|
+
private val monitor = nw_path_monitor_create()
|
|
19
|
+
private val _isOnline = MutableStateFlow(false)
|
|
20
|
+
actual val isOnline: StateFlow<Boolean> = _isOnline.asStateFlow()
|
|
21
|
+
|
|
22
|
+
init {
|
|
23
|
+
nw_path_monitor_set_update_handler(monitor) { path ->
|
|
24
|
+
_isOnline.value = nw_path_get_status(path) == nw_path_status_satisfied
|
|
25
|
+
}
|
|
26
|
+
nw_path_monitor_set_queue(
|
|
27
|
+
monitor,
|
|
28
|
+
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(), 0u)
|
|
29
|
+
)
|
|
30
|
+
nw_path_monitor_start(monitor)
|
|
31
|
+
}
|
|
32
|
+
}
|