expo-dev-launcher 5.2.0-canary-20250701-6a945c5 → 5.2.0-canary-20250709-136b77f
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/CHANGELOG.md +9 -0
- package/android/build.gradle +2 -2
- package/android/src/debug/java/expo/modules/devlauncher/compose/BindingView.kt +2 -14
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt +13 -23
- package/android/src/debug/java/expo/modules/devlauncher/compose/HomeViewModel.kt +84 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ProfileViewModel.kt +30 -44
- package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/Accordion.kt +1 -1
- package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/Asyncimage.kt +40 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Home.kt +23 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/{screens → routes}/Profile.kt +8 -2
- package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Settings.kt +16 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/screens/{Home.kt → HomeScreen.kt} +18 -10
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountAvatar.kt +37 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountSelector.kt +42 -9
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AppHeader.kt +26 -18
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/BottomTabBar.kt +3 -3
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/RunningAppCard.kt +25 -26
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/SectionHeader.kt +6 -16
- package/android/src/debug/java/expo/modules/devlauncher/launcher/DevLauncherActivity.kt +36 -51
- package/android/src/debug/java/expo/modules/devlauncher/services/DependencyInjection.kt +37 -3
- package/android/src/debug/java/expo/modules/devlauncher/services/HttpClientService.kt +9 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/ImageLoaderService.kt +69 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/PackagerService.kt +1 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/SessionService.kt +90 -3
- package/ios/Assets.xcassets/branch-icon.imageset/Contents.json +56 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light@2x.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light@3x.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon@2x.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon@3x.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/Contents.json +56 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon-light.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon-light@2x.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon-light@3x.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon@2x.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon@3x.png +0 -0
- package/ios/EXDevLauncherController.m +2 -4
- package/ios/SwiftUI/Data.swift +34 -0
- package/ios/SwiftUI/DevLauncherViewModel.swift +24 -15
- package/ios/SwiftUI/DevLauncherViews.swift +3 -11
- package/ios/SwiftUI/GraphQL/APIClient.swift +11 -6
- package/ios/SwiftUI/GraphQL/Models.swift +12 -0
- package/ios/SwiftUI/GraphQL/Queries.swift +3 -15
- package/ios/SwiftUI/HomeTabView.swift +5 -3
- package/ios/SwiftUI/SettingsTabView.swift +4 -4
- package/ios/SwiftUI/UpdatesTab/NotSignedInView.swift +32 -0
- package/ios/SwiftUI/UpdatesTab/NotUsingUpdatesView.swift +38 -0
- package/ios/SwiftUI/UpdatesTab/UpdateRow.swift +76 -0
- package/ios/SwiftUI/UpdatesTab/UpdateUtils.swift +5 -0
- package/ios/SwiftUI/UpdatesTab/UpdatesListView.swift +196 -0
- package/ios/SwiftUI/UpdatesTab/UpdatesTabView.swift +37 -0
- package/package.json +4 -4
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherAction.kt +0 -7
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherViewModel.kt +0 -43
- package/ios/SwiftUI/ExtensionsTabView.swift +0 -68
- /package/android/src/debug/java/expo/modules/devlauncher/compose/screens/{Settings.kt → SettingsScreen.kt} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
### 🐛 Bug fixes
|
|
12
12
|
|
|
13
13
|
- [iOS] Fix missing CDP headers when using static frameworks. ([#37448](https://github.com/expo/expo/pull/37448) by [@alanjhughes](https://github.com/alanjhughes))
|
|
14
|
+
- [Android] Use same strings in UI as iOS. ([#37786](https://github.com/expo/expo/pull/37786) by [@douglowder](https://github.com/douglowder))
|
|
14
15
|
|
|
15
16
|
### 💡 Others
|
|
16
17
|
|
|
@@ -20,6 +21,14 @@
|
|
|
20
21
|
|
|
21
22
|
- Added support for React Native 0.80.x. ([#37400](https://github.com/expo/expo/pull/37400) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
|
22
23
|
|
|
24
|
+
## 5.1.16 - 2025-07-03
|
|
25
|
+
|
|
26
|
+
_This version does not introduce any user-facing changes._
|
|
27
|
+
|
|
28
|
+
## 5.1.15 - 2025-07-02
|
|
29
|
+
|
|
30
|
+
_This version does not introduce any user-facing changes._
|
|
31
|
+
|
|
23
32
|
## 5.1.14 - 2025-06-26
|
|
24
33
|
|
|
25
34
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -20,13 +20,13 @@ expoModule {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
group = "host.exp.exponent"
|
|
23
|
-
version = "5.2.0-canary-
|
|
23
|
+
version = "5.2.0-canary-20250709-136b77f"
|
|
24
24
|
|
|
25
25
|
android {
|
|
26
26
|
namespace "expo.modules.devlauncher"
|
|
27
27
|
defaultConfig {
|
|
28
28
|
versionCode 9
|
|
29
|
-
versionName "5.2.0-canary-
|
|
29
|
+
versionName "5.2.0-canary-20250709-136b77f"
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
buildTypes {
|
|
@@ -2,28 +2,16 @@ package expo.modules.devlauncher.compose
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.widget.LinearLayout
|
|
5
|
-
import androidx.compose.runtime.collectAsState
|
|
6
|
-
import androidx.compose.runtime.getValue
|
|
7
|
-
import androidx.compose.runtime.remember
|
|
8
5
|
import androidx.compose.ui.platform.ComposeView
|
|
9
|
-
import expo.modules.devmenu.AppInfo
|
|
10
6
|
import expo.modules.devmenu.compose.theme.AppTheme
|
|
11
7
|
|
|
12
|
-
class BindingView(context: Context
|
|
8
|
+
class BindingView(context: Context) : LinearLayout(context) {
|
|
13
9
|
init {
|
|
14
10
|
addView(
|
|
15
11
|
ComposeView(context).apply {
|
|
16
12
|
setContent {
|
|
17
13
|
AppTheme {
|
|
18
|
-
|
|
19
|
-
val nativeAppInfo = remember { AppInfo.getNativeAppInfo(context) }
|
|
20
|
-
DevLauncherBottomTabsNavigator(
|
|
21
|
-
DevLauncherState(
|
|
22
|
-
appName = nativeAppInfo.appName,
|
|
23
|
-
runningPackagers = runningPackager,
|
|
24
|
-
onAction = viewModel::onAction
|
|
25
|
-
)
|
|
26
|
-
)
|
|
14
|
+
DevLauncherBottomTabsNavigator()
|
|
27
15
|
}
|
|
28
16
|
}
|
|
29
17
|
}
|
package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt
CHANGED
|
@@ -14,21 +14,15 @@ import androidx.navigation.compose.NavHost
|
|
|
14
14
|
import androidx.navigation.compose.composable
|
|
15
15
|
import androidx.navigation.compose.rememberNavController
|
|
16
16
|
import expo.modules.devlauncher.compose.primitives.DefaultScaffold
|
|
17
|
-
import expo.modules.devlauncher.compose.
|
|
18
|
-
import expo.modules.devlauncher.compose.
|
|
19
|
-
import expo.modules.devlauncher.compose.
|
|
17
|
+
import expo.modules.devlauncher.compose.routes.Home
|
|
18
|
+
import expo.modules.devlauncher.compose.routes.HomeRoute
|
|
19
|
+
import expo.modules.devlauncher.compose.routes.ProfileRoute
|
|
20
|
+
import expo.modules.devlauncher.compose.routes.Settings
|
|
21
|
+
import expo.modules.devlauncher.compose.routes.SettingsRoute
|
|
20
22
|
import expo.modules.devlauncher.compose.ui.BottomTabBar
|
|
21
23
|
import expo.modules.devlauncher.compose.ui.Full
|
|
22
24
|
import expo.modules.devlauncher.compose.ui.rememberBottomSheetState
|
|
23
|
-
import expo.modules.devlauncher.services.PackagerInfo
|
|
24
25
|
import expo.modules.devmenu.compose.theme.Theme
|
|
25
|
-
import kotlinx.serialization.Serializable
|
|
26
|
-
|
|
27
|
-
@Serializable
|
|
28
|
-
object Home
|
|
29
|
-
|
|
30
|
-
@Serializable
|
|
31
|
-
object Settings
|
|
32
26
|
|
|
33
27
|
@Composable
|
|
34
28
|
fun DefaultScreenContainer(
|
|
@@ -52,16 +46,8 @@ data class Tab(
|
|
|
52
46
|
val screen: Any
|
|
53
47
|
)
|
|
54
48
|
|
|
55
|
-
data class DevLauncherState(
|
|
56
|
-
val appName: String = "BareExpo",
|
|
57
|
-
val runningPackagers: Set<PackagerInfo> = emptySet<PackagerInfo>(),
|
|
58
|
-
val onAction: DevLauncherActionHandler = {}
|
|
59
|
-
)
|
|
60
|
-
|
|
61
49
|
@Composable
|
|
62
|
-
fun DevLauncherBottomTabsNavigator(
|
|
63
|
-
state: DevLauncherState
|
|
64
|
-
) {
|
|
50
|
+
fun DevLauncherBottomTabsNavigator() {
|
|
65
51
|
val navController = rememberNavController()
|
|
66
52
|
val bottomSheetState = rememberBottomSheetState()
|
|
67
53
|
|
|
@@ -78,10 +64,14 @@ fun DevLauncherBottomTabsNavigator(
|
|
|
78
64
|
ExitTransition.None
|
|
79
65
|
}
|
|
80
66
|
) {
|
|
81
|
-
composable<Home> {
|
|
82
|
-
|
|
67
|
+
composable<Home> {
|
|
68
|
+
HomeRoute(onProfileClick = { bottomSheetState.jumpTo(Full) })
|
|
69
|
+
}
|
|
70
|
+
composable<Settings> {
|
|
71
|
+
SettingsRoute()
|
|
72
|
+
}
|
|
83
73
|
}
|
|
84
74
|
}
|
|
85
75
|
|
|
86
|
-
|
|
76
|
+
ProfileRoute(bottomSheetState)
|
|
87
77
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import androidx.compose.runtime.mutableStateOf
|
|
5
|
+
import androidx.core.net.toUri
|
|
6
|
+
import androidx.lifecycle.ViewModel
|
|
7
|
+
import androidx.lifecycle.viewModelScope
|
|
8
|
+
import expo.modules.devlauncher.DevLauncherController
|
|
9
|
+
import expo.modules.devlauncher.MeQuery
|
|
10
|
+
import expo.modules.devlauncher.services.HttpClientService
|
|
11
|
+
import expo.modules.devlauncher.services.PackagerInfo
|
|
12
|
+
import expo.modules.devlauncher.services.PackagerService
|
|
13
|
+
import expo.modules.devlauncher.services.SessionService
|
|
14
|
+
import expo.modules.devlauncher.services.UserState
|
|
15
|
+
import expo.modules.devlauncher.services.inject
|
|
16
|
+
import kotlinx.coroutines.flow.launchIn
|
|
17
|
+
import kotlinx.coroutines.flow.onEach
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
|
|
20
|
+
sealed interface HomeAction {
|
|
21
|
+
class OpenApp(val url: String) : HomeAction
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
typealias HomeActionHandler = (HomeAction) -> Unit
|
|
25
|
+
|
|
26
|
+
data class HomeState(
|
|
27
|
+
val appName: String = "BareExpo",
|
|
28
|
+
val runningPackagers: Set<PackagerInfo> = emptySet(),
|
|
29
|
+
val currentAccount: MeQuery.Account? = null
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
class HomeViewModel() : ViewModel() {
|
|
33
|
+
val httpClientService = inject<HttpClientService>()
|
|
34
|
+
val devLauncherController = inject<DevLauncherController>()
|
|
35
|
+
val sessionService = inject<SessionService>()
|
|
36
|
+
|
|
37
|
+
val packagerService = PackagerService(httpClientService.httpClient, viewModelScope)
|
|
38
|
+
|
|
39
|
+
private var _state = mutableStateOf(
|
|
40
|
+
HomeState(
|
|
41
|
+
runningPackagers = packagerService.runningPackagers.value,
|
|
42
|
+
currentAccount = when (val userState = sessionService.user.value) {
|
|
43
|
+
UserState.Fetching, UserState.LoggedOut -> null
|
|
44
|
+
is UserState.LoggedIn -> userState.selectedAccount
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
val state
|
|
50
|
+
get() = _state.value
|
|
51
|
+
|
|
52
|
+
init {
|
|
53
|
+
packagerService.runningPackagers.onEach { newPackagers ->
|
|
54
|
+
_state.value = _state.value.copy(
|
|
55
|
+
runningPackagers = newPackagers
|
|
56
|
+
)
|
|
57
|
+
}.launchIn(viewModelScope)
|
|
58
|
+
|
|
59
|
+
sessionService.user.onEach { newUser ->
|
|
60
|
+
when (newUser) {
|
|
61
|
+
UserState.Fetching, UserState.LoggedOut -> _state.value = _state.value.copy(
|
|
62
|
+
currentAccount = null
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
is UserState.LoggedIn -> _state.value = _state.value.copy(
|
|
66
|
+
currentAccount = newUser.selectedAccount
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}.launchIn(viewModelScope)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fun onAction(action: HomeAction) {
|
|
73
|
+
when (action) {
|
|
74
|
+
is HomeAction.OpenApp ->
|
|
75
|
+
viewModelScope.launch {
|
|
76
|
+
try {
|
|
77
|
+
devLauncherController.loadApp(action.url.toUri(), mainActivity = null)
|
|
78
|
+
} catch (e: Exception) {
|
|
79
|
+
Log.e("DevLauncher", "Failed to open app: ${action.url}", e)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -2,39 +2,39 @@ package expo.modules.devlauncher.compose
|
|
|
2
2
|
|
|
3
3
|
import androidx.lifecycle.ViewModel
|
|
4
4
|
import androidx.lifecycle.viewModelScope
|
|
5
|
-
import expo.modules.devlauncher.services.ApolloClientService
|
|
6
5
|
import expo.modules.devlauncher.services.SessionService
|
|
6
|
+
import expo.modules.devlauncher.services.UserState
|
|
7
7
|
import expo.modules.devlauncher.services.inject
|
|
8
|
-
import kotlinx.coroutines.Dispatchers
|
|
9
8
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
10
9
|
import kotlinx.coroutines.flow.asStateFlow
|
|
11
10
|
import kotlinx.coroutines.flow.launchIn
|
|
12
11
|
import kotlinx.coroutines.flow.onEach
|
|
13
12
|
import kotlinx.coroutines.flow.update
|
|
14
|
-
import kotlinx.coroutines.launch
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
data class Account(
|
|
15
|
+
val id: String,
|
|
16
|
+
val name: String,
|
|
17
|
+
val avatar: String?,
|
|
18
|
+
val isSelected: Boolean = false
|
|
19
|
+
)
|
|
21
20
|
|
|
21
|
+
sealed interface ProfileState {
|
|
22
22
|
class LoggedIn(
|
|
23
|
-
val isLoading: Boolean = true,
|
|
24
23
|
val accounts: List<Account> = emptyList()
|
|
25
24
|
) : ProfileState
|
|
26
25
|
|
|
26
|
+
object Fetching : ProfileState
|
|
27
27
|
object LoggedOut : ProfileState
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
class ProfileViewModel : ViewModel() {
|
|
31
31
|
sealed interface Action {
|
|
32
32
|
class LogIn(val sessionSecret: String) : Action
|
|
33
|
+
class SwitchAccount(val account: Account) : Action
|
|
33
34
|
object SignOut : Action
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
val
|
|
37
|
-
val apolloClientService = inject<ApolloClientService>()
|
|
37
|
+
val session = inject<SessionService>()
|
|
38
38
|
|
|
39
39
|
private val _state = MutableStateFlow<ProfileState>(ProfileState.LoggedOut)
|
|
40
40
|
|
|
@@ -42,54 +42,40 @@ class ProfileViewModel : ViewModel() {
|
|
|
42
42
|
get() = _state.asStateFlow()
|
|
43
43
|
|
|
44
44
|
init {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
_state.update {
|
|
49
|
-
|
|
45
|
+
session.user.onEach { newUserState ->
|
|
46
|
+
when (newUserState) {
|
|
47
|
+
UserState.LoggedOut -> _state.update { ProfileState.LoggedOut }
|
|
48
|
+
UserState.Fetching -> _state.update { ProfileState.Fetching }
|
|
49
|
+
is UserState.LoggedIn -> _state.update {
|
|
50
|
+
ProfileState.LoggedIn(
|
|
51
|
+
accounts = newUserState.data.meUserActor?.accounts?.map { account ->
|
|
52
|
+
Account(
|
|
53
|
+
id = account.id,
|
|
54
|
+
name = account.name,
|
|
55
|
+
avatar = account.ownerUserActor?.profilePhoto,
|
|
56
|
+
isSelected = account.id == newUserState.selectedAccount?.id
|
|
57
|
+
)
|
|
58
|
+
} ?: emptyList()
|
|
59
|
+
)
|
|
50
60
|
}
|
|
51
|
-
} else {
|
|
52
|
-
_state.update { ProfileState.LoggedOut }
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
.launchIn(viewModelScope)
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
private fun fetchMe() {
|
|
59
|
-
viewModelScope.launch(Dispatchers.IO) {
|
|
60
|
-
val me = apolloClientService.fetchMe()
|
|
61
|
-
_state.update { prevState ->
|
|
62
|
-
// User logged out in the meantime, we can ignore the result
|
|
63
|
-
if (prevState is ProfileState.LoggedOut) {
|
|
64
|
-
return@update prevState
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
val accounts = me.data?.meUserActor?.accounts?.map {
|
|
68
|
-
ProfileState.Account(
|
|
69
|
-
name = it.name,
|
|
70
|
-
avatar = it.ownerUserActor?.profilePhoto
|
|
71
|
-
)
|
|
72
|
-
} ?: emptyList()
|
|
73
|
-
|
|
74
|
-
ProfileState.LoggedIn(
|
|
75
|
-
isLoading = false,
|
|
76
|
-
accounts = accounts
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
66
|
fun onAction(action: Action) {
|
|
83
67
|
when (action) {
|
|
84
68
|
is Action.LogIn -> {
|
|
85
|
-
|
|
69
|
+
session.setSession(
|
|
86
70
|
Session(action.sessionSecret)
|
|
87
71
|
)
|
|
88
72
|
}
|
|
89
73
|
|
|
90
74
|
Action.SignOut -> {
|
|
91
|
-
|
|
75
|
+
session.setSession(null)
|
|
92
76
|
}
|
|
77
|
+
|
|
78
|
+
is Action.SwitchAccount -> session.switchAccount(action.account.id)
|
|
93
79
|
}
|
|
94
80
|
}
|
|
95
81
|
}
|
|
@@ -91,7 +91,7 @@ fun Accordion(
|
|
|
91
91
|
@Composable
|
|
92
92
|
@Preview(showBackground = true, heightDp = 200)
|
|
93
93
|
fun AccordionVariantPreview() {
|
|
94
|
-
Accordion(text = "Enter URL") {
|
|
94
|
+
Accordion(text = "Enter URL manually") {
|
|
95
95
|
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac nisl interdum, mattis purus a, consequat ipsum. Aliquam sem mauris, egestas a elit a, lacinia efficitur nisi. Maecenas scelerisque erat nisi, ac interdum mauris volutpat vel. Proin sed lectus at purus interdum porta. Ut mollis feugiat dignissim.")
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.primitives
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.Image
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
6
|
+
import androidx.compose.runtime.getValue
|
|
7
|
+
import androidx.compose.runtime.mutableStateOf
|
|
8
|
+
import androidx.compose.runtime.remember
|
|
9
|
+
import androidx.compose.runtime.rememberCoroutineScope
|
|
10
|
+
import androidx.compose.runtime.setValue
|
|
11
|
+
import androidx.compose.ui.graphics.ImageBitmap
|
|
12
|
+
import androidx.compose.ui.graphics.asImageBitmap
|
|
13
|
+
import expo.modules.devlauncher.services.ImageLoaderService
|
|
14
|
+
import expo.modules.devlauncher.services.inject
|
|
15
|
+
import kotlinx.coroutines.launch
|
|
16
|
+
|
|
17
|
+
@Composable
|
|
18
|
+
fun AsyncImage(
|
|
19
|
+
url: String
|
|
20
|
+
) {
|
|
21
|
+
val imageLoaderService = inject<ImageLoaderService>()
|
|
22
|
+
val scope = rememberCoroutineScope()
|
|
23
|
+
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
|
|
24
|
+
|
|
25
|
+
LaunchedEffect(url) {
|
|
26
|
+
scope.launch {
|
|
27
|
+
val image = imageLoaderService.loadImage(url)
|
|
28
|
+
if (image != null) {
|
|
29
|
+
imageBitmap = image.asImageBitmap()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
imageBitmap?.let {
|
|
35
|
+
Image(
|
|
36
|
+
bitmap = it,
|
|
37
|
+
contentDescription = url
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.routes
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
5
|
+
import expo.modules.devlauncher.compose.DefaultScreenContainer
|
|
6
|
+
import expo.modules.devlauncher.compose.HomeViewModel
|
|
7
|
+
import expo.modules.devlauncher.compose.screens.HomeScreen
|
|
8
|
+
import kotlinx.serialization.Serializable
|
|
9
|
+
|
|
10
|
+
@Serializable
|
|
11
|
+
object Home
|
|
12
|
+
|
|
13
|
+
@Composable
|
|
14
|
+
fun HomeRoute(onProfileClick: () -> Unit) {
|
|
15
|
+
DefaultScreenContainer {
|
|
16
|
+
val viewModel = viewModel<HomeViewModel>()
|
|
17
|
+
HomeScreen(
|
|
18
|
+
state = viewModel.state,
|
|
19
|
+
onAction = viewModel::onAction,
|
|
20
|
+
onProfileClick = onProfileClick
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
}
|
package/android/src/debug/java/expo/modules/devlauncher/compose/{screens → routes}/Profile.kt
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package expo.modules.devlauncher.compose.
|
|
1
|
+
package expo.modules.devlauncher.compose.routes
|
|
2
2
|
|
|
3
3
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
4
4
|
import androidx.compose.runtime.Composable
|
|
@@ -18,7 +18,7 @@ import expo.modules.devlauncher.compose.ui.ProfileLayout
|
|
|
18
18
|
import expo.modules.devlauncher.compose.ui.SignUp
|
|
19
19
|
|
|
20
20
|
@Composable
|
|
21
|
-
fun
|
|
21
|
+
fun ProfileRoute(
|
|
22
22
|
bottomSheetState: ModalBottomSheetState,
|
|
23
23
|
viewModel: ProfileViewModel = viewModel()
|
|
24
24
|
) {
|
|
@@ -44,6 +44,9 @@ fun Profile(
|
|
|
44
44
|
accounts = state.accounts,
|
|
45
45
|
onSignOut = {
|
|
46
46
|
viewModel.onAction(ProfileViewModel.Action.SignOut)
|
|
47
|
+
},
|
|
48
|
+
onClick = { account ->
|
|
49
|
+
viewModel.onAction(ProfileViewModel.Action.SwitchAccount(account))
|
|
47
50
|
}
|
|
48
51
|
)
|
|
49
52
|
}
|
|
@@ -58,6 +61,9 @@ fun Profile(
|
|
|
58
61
|
}
|
|
59
62
|
)
|
|
60
63
|
}
|
|
64
|
+
|
|
65
|
+
ProfileState.Fetching -> {
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.routes
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import expo.modules.devlauncher.compose.DefaultScreenContainer
|
|
5
|
+
import expo.modules.devlauncher.compose.screens.SettingsScreen
|
|
6
|
+
import kotlinx.serialization.Serializable
|
|
7
|
+
|
|
8
|
+
@Serializable
|
|
9
|
+
object Settings
|
|
10
|
+
|
|
11
|
+
@Composable
|
|
12
|
+
fun SettingsRoute() {
|
|
13
|
+
DefaultScreenContainer {
|
|
14
|
+
SettingsScreen()
|
|
15
|
+
}
|
|
16
|
+
}
|
package/android/src/debug/java/expo/modules/devlauncher/compose/screens/{Home.kt → HomeScreen.kt}
RENAMED
|
@@ -19,8 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|
|
19
19
|
import com.composeunstyled.Button
|
|
20
20
|
import com.composeunstyled.TextField
|
|
21
21
|
import expo.modules.devlauncher.R
|
|
22
|
-
import expo.modules.devlauncher.compose.
|
|
23
|
-
import expo.modules.devlauncher.compose.
|
|
22
|
+
import expo.modules.devlauncher.compose.HomeAction
|
|
23
|
+
import expo.modules.devlauncher.compose.HomeState
|
|
24
24
|
import expo.modules.devlauncher.compose.primitives.Accordion
|
|
25
25
|
import expo.modules.devlauncher.compose.ui.AppHeader
|
|
26
26
|
import expo.modules.devlauncher.compose.ui.RunningAppCard
|
|
@@ -33,10 +33,18 @@ import expo.modules.devmenu.compose.primitives.Text
|
|
|
33
33
|
import expo.modules.devmenu.compose.theme.Theme
|
|
34
34
|
|
|
35
35
|
@Composable
|
|
36
|
-
fun HomeScreen(
|
|
36
|
+
fun HomeScreen(
|
|
37
|
+
state: HomeState,
|
|
38
|
+
onAction: (HomeAction) -> Unit,
|
|
39
|
+
onProfileClick: () -> Unit
|
|
40
|
+
) {
|
|
37
41
|
Column {
|
|
38
42
|
ScreenHeaderContainer(modifier = Modifier.padding(Theme.spacing.medium)) {
|
|
39
|
-
AppHeader(
|
|
43
|
+
AppHeader(
|
|
44
|
+
appName = state.appName,
|
|
45
|
+
currentAccount = state.currentAccount,
|
|
46
|
+
onProfileClick = onProfileClick
|
|
47
|
+
)
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
Column(
|
|
@@ -49,7 +57,7 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
|
|
|
49
57
|
Spacer(Theme.spacing.small)
|
|
50
58
|
|
|
51
59
|
SectionHeader(
|
|
52
|
-
"Development",
|
|
60
|
+
"Development servers",
|
|
53
61
|
leftIcon = {
|
|
54
62
|
Image(
|
|
55
63
|
painter = painterResource(R.drawable._expodevclientcomponents_assets_terminalicon),
|
|
@@ -73,12 +81,12 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
|
|
|
73
81
|
RunningAppCard(
|
|
74
82
|
appIp = packager.url
|
|
75
83
|
) {
|
|
76
|
-
|
|
84
|
+
onAction(HomeAction.OpenApp(packager.url))
|
|
77
85
|
}
|
|
78
86
|
Divider()
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
Accordion("Enter URL", initialState = false) {
|
|
89
|
+
Accordion("Enter URL manually", initialState = false) {
|
|
82
90
|
val url = remember { mutableStateOf("") }
|
|
83
91
|
|
|
84
92
|
Column {
|
|
@@ -89,7 +97,7 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
|
|
|
89
97
|
onValueChange = { newValue ->
|
|
90
98
|
url.value = newValue
|
|
91
99
|
},
|
|
92
|
-
placeholder = "http://10.0.2.2:
|
|
100
|
+
placeholder = "http://10.0.2.2:8081",
|
|
93
101
|
textStyle = Theme.typography.medium.font,
|
|
94
102
|
maxLines = 1,
|
|
95
103
|
modifier = Modifier
|
|
@@ -109,7 +117,7 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
|
|
|
109
117
|
Spacer(Theme.spacing.tiny)
|
|
110
118
|
|
|
111
119
|
Button(onClick = {
|
|
112
|
-
|
|
120
|
+
onAction(HomeAction.OpenApp(url.value))
|
|
113
121
|
}, modifier = Modifier.fillMaxWidth()) {
|
|
114
122
|
Row(modifier = Modifier.padding(vertical = Theme.spacing.small)) {
|
|
115
123
|
Text("Connect")
|
|
@@ -130,5 +138,5 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
|
|
|
130
138
|
@Preview(showBackground = true)
|
|
131
139
|
@Composable
|
|
132
140
|
fun HomeScreenPreview() {
|
|
133
|
-
HomeScreen(state =
|
|
141
|
+
HomeScreen(state = HomeState(), onAction = {}, onProfileClick = {})
|
|
134
142
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.ui
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.padding
|
|
4
|
+
import androidx.compose.foundation.layout.size
|
|
5
|
+
import androidx.compose.runtime.Composable
|
|
6
|
+
import androidx.compose.ui.Modifier
|
|
7
|
+
import androidx.compose.ui.res.painterResource
|
|
8
|
+
import androidx.compose.ui.unit.Dp
|
|
9
|
+
import com.composables.core.Icon
|
|
10
|
+
import expo.modules.devlauncher.R
|
|
11
|
+
import expo.modules.devlauncher.compose.primitives.AsyncImage
|
|
12
|
+
import expo.modules.devmenu.compose.primitives.RoundedSurface
|
|
13
|
+
import expo.modules.devmenu.compose.theme.Theme
|
|
14
|
+
|
|
15
|
+
@Composable
|
|
16
|
+
fun AccountAvatar(
|
|
17
|
+
url: String?,
|
|
18
|
+
size: Dp = Theme.sizing.icon.medium,
|
|
19
|
+
modifier: Modifier = Modifier
|
|
20
|
+
) {
|
|
21
|
+
RoundedSurface(
|
|
22
|
+
borderRadius = Theme.sizing.borderRadius.full,
|
|
23
|
+
modifier = Modifier.size(size).then(modifier)
|
|
24
|
+
) {
|
|
25
|
+
if (url != null) {
|
|
26
|
+
AsyncImage(
|
|
27
|
+
url = url
|
|
28
|
+
)
|
|
29
|
+
} else {
|
|
30
|
+
Icon(
|
|
31
|
+
painterResource(R.drawable._expodevclientcomponents_assets_buildingicon),
|
|
32
|
+
contentDescription = "Avatar",
|
|
33
|
+
modifier = Modifier.padding(Theme.spacing.micro)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,27 +1,60 @@
|
|
|
1
1
|
package expo.modules.devlauncher.compose.ui
|
|
2
2
|
|
|
3
3
|
import androidx.compose.foundation.layout.Column
|
|
4
|
+
import androidx.compose.foundation.layout.padding
|
|
4
5
|
import androidx.compose.runtime.Composable
|
|
5
|
-
import
|
|
6
|
-
import
|
|
6
|
+
import androidx.compose.ui.Modifier
|
|
7
|
+
import androidx.compose.ui.res.painterResource
|
|
8
|
+
import com.composables.core.Icon
|
|
9
|
+
import com.composeunstyled.Button
|
|
10
|
+
import expo.modules.devlauncher.R
|
|
11
|
+
import expo.modules.devlauncher.compose.Account
|
|
12
|
+
import expo.modules.devmenu.compose.primitives.Divider
|
|
7
13
|
import expo.modules.devmenu.compose.primitives.RoundedSurface
|
|
14
|
+
import expo.modules.devmenu.compose.primitives.RowLayout
|
|
8
15
|
import expo.modules.devmenu.compose.primitives.Spacer
|
|
16
|
+
import expo.modules.devmenu.compose.primitives.Text
|
|
9
17
|
import expo.modules.devmenu.compose.theme.Theme
|
|
10
|
-
import expo.modules.devmenu.compose.ui.MenuButton
|
|
11
18
|
|
|
12
19
|
@Composable
|
|
13
20
|
fun AccountSelector(
|
|
14
|
-
accounts: List<
|
|
21
|
+
accounts: List<Account>,
|
|
22
|
+
onClick: (Account) -> Unit = {},
|
|
15
23
|
onSignOut: () -> Unit = {}
|
|
16
24
|
) {
|
|
17
25
|
Column {
|
|
18
26
|
RoundedSurface {
|
|
19
27
|
Column {
|
|
20
|
-
for (account in accounts) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
28
|
+
for ((index, account) in accounts.withIndex()) {
|
|
29
|
+
Button(
|
|
30
|
+
onClick = { onClick(account) },
|
|
31
|
+
enabled = !account.isSelected
|
|
32
|
+
) {
|
|
33
|
+
val avatar = @Composable {
|
|
34
|
+
RoundedSurface(borderRadius = Theme.sizing.borderRadius.full) {
|
|
35
|
+
AccountAvatar(
|
|
36
|
+
account.avatar
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
RowLayout(
|
|
41
|
+
modifier = Modifier.padding(Theme.spacing.small),
|
|
42
|
+
leftComponent = avatar,
|
|
43
|
+
rightComponent = {
|
|
44
|
+
if (account.isSelected) {
|
|
45
|
+
Icon(
|
|
46
|
+
painterResource(R.drawable._expodevclientcomponents_assets_checkicon),
|
|
47
|
+
contentDescription = "Checked"
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
) {
|
|
52
|
+
Text(account.name)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (index < accounts.size - 1) {
|
|
56
|
+
Divider()
|
|
57
|
+
}
|
|
25
58
|
}
|
|
26
59
|
}
|
|
27
60
|
}
|