androidify
This commit is contained in:
parent
99d16d56c8
commit
c785b73c3c
21 changed files with 515 additions and 217 deletions
|
@ -20,7 +20,9 @@ android {
|
|||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
// don't forget to add another "s this is counter intuitive I know but not my fault
|
||||
buildConfigField("String", "VASTAI_KEY", "null")
|
||||
buildConfigField("String", "VASIAI_API_ENDPOINT", "\"https://console.vast.ai/api/v0\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -67,6 +69,7 @@ dependencies {
|
|||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.runtime.livedata)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
|
|
@ -14,12 +14,12 @@
|
|||
android:enableOnBackInvokedCallback="true"
|
||||
tools:targetApi="34">
|
||||
<activity
|
||||
android:name=".DashboardActivity"
|
||||
android:name=".activity.dashboard.DashboardActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_dashboard"
|
||||
android:theme="@style/Theme.Vastapp" />
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:name=".activity.login.LoginActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_login"
|
||||
android:theme="@style/Theme.Vastapp">
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
package eu.m724.vastapp
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavOptionsBuilder
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import eu.m724.vastapp.dashboard.DashboardScreen
|
||||
import eu.m724.vastapp.dashboard.InstancesScreen
|
||||
import eu.m724.vastapp.ui.theme.VastappTheme
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class DashboardActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val user = intent.getParcelableExtra<User>("user")!! // TODO null check
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
VastappTheme {
|
||||
val items = listOf(
|
||||
Screen.Dashboard,
|
||||
Screen.Instances,
|
||||
Screen.Billing
|
||||
)
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
|
||||
items.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
||||
onClick = {
|
||||
navController.navigate(screen.route) {
|
||||
// Pop up to the start destination of the graph to
|
||||
// avoid building up a large stack of destinations
|
||||
// on the back stack as users select items
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
// Avoid multiple copies of the same destination when
|
||||
// reselecting the same item
|
||||
launchSingleTop = true
|
||||
// Restore state when reselecting a previously selected item
|
||||
restoreState = true
|
||||
|
||||
}
|
||||
},
|
||||
icon = { Icon(screen.icon, contentDescription = stringResource(screen.resourceId)) },
|
||||
label = { Text(text = stringResource(screen.resourceId)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(modifier = Modifier.padding(innerPadding)) {
|
||||
|
||||
NavHost(navController = navController, startDestination = "dashboard") {
|
||||
composable("dashboard") { DashboardScreen(user = user) }
|
||||
composable("instances") { InstancesScreen() }
|
||||
composable("billing") { BillingScreen(user = user) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BillingScreen(user: User) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center) {
|
||||
Text("You have $%.2f".format(user.credit), fontSize = 28.sp)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package eu.m724.vastapp.activity.dashboard
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import eu.m724.vastapp.activity.dashboard.screen.Screen
|
||||
import eu.m724.vastapp.activity.dashboard.screen.BillingScreen
|
||||
import eu.m724.vastapp.activity.dashboard.screen.DashboardScreen
|
||||
import eu.m724.vastapp.activity.dashboard.screen.InstancesScreen
|
||||
import eu.m724.vastapp.ui.theme.VastappTheme
|
||||
import eu.m724.vastapp.vastai.VastApi
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
import org.chromium.net.CronetEngine
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class DashboardActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val user = intent.getParcelableExtra<User>("user")!! // TODO null check
|
||||
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val cronetEngine = CronetEngine.Builder(baseContext).build()
|
||||
val vastApi = VastApi(user.apiKey, cronetEngine, executor) // TODO use that from login activity
|
||||
|
||||
val dashboardViewModel = DashboardViewModel(user, vastApi)
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
VastappTheme {
|
||||
val items = listOf(
|
||||
Screen.Dashboard,
|
||||
Screen.Instances,
|
||||
Screen.Billing
|
||||
)
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
MyNavigationBar(items, navController = navController)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(modifier = Modifier.padding(innerPadding)) {
|
||||
NavHost(navController = navController, startDestination = "dashboard") {
|
||||
composable("dashboard") { DashboardScreen(dashboardViewModel) }
|
||||
composable("instances") { InstancesScreen(dashboardViewModel) }
|
||||
composable("billing") { BillingScreen(dashboardViewModel) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MyNavigationBar(items: List<Screen>, navController: NavHostController) {
|
||||
NavigationBar {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
|
||||
items.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
||||
onClick = {
|
||||
navController.navigate(screen.route) {
|
||||
// Pop up to the start destination of the graph to
|
||||
// avoid building up a large stack of destinations
|
||||
// on the back stack as users select items
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
// Avoid multiple copies of the same destination when
|
||||
// reselecting the same item
|
||||
launchSingleTop = true
|
||||
// Restore state when reselecting a previously selected item
|
||||
restoreState = true
|
||||
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
screen.icon,
|
||||
contentDescription = stringResource(screen.resourceId)
|
||||
)
|
||||
},
|
||||
label = { Text(text = stringResource(screen.resourceId)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package eu.m724.vastapp.activity.dashboard
|
||||
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
|
||||
data class DashboardUiState(
|
||||
val isRefreshing: Boolean,
|
||||
val user: User,
|
||||
val error: String?
|
||||
) { }
|
|
@ -0,0 +1,45 @@
|
|||
package eu.m724.vastapp.activity.dashboard
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import eu.m724.vastapp.activity.login.LoginUiState
|
||||
import eu.m724.vastapp.vastai.ApiRoute
|
||||
import eu.m724.vastapp.vastai.VastApi
|
||||
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DashboardViewModel(private val _user: User, val vastApi: VastApi) : ViewModel() {
|
||||
private val _uiState: MutableStateFlow<DashboardUiState> =
|
||||
MutableStateFlow(DashboardUiState(false, _user, null))
|
||||
val uiState: StateFlow<DashboardUiState> =
|
||||
_uiState.asStateFlow()
|
||||
|
||||
private val _navigationEvent = MutableSharedFlow<String>()
|
||||
val navigationEvent: SharedFlow<String> = _navigationEvent.asSharedFlow()
|
||||
|
||||
fun navigateTo(route: String) {
|
||||
viewModelScope.launch {
|
||||
_navigationEvent.emit(route)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshData() {
|
||||
val request = vastApi.buildRequest(
|
||||
ApiRoute.SHOW_USER,
|
||||
UserUrlRequestCallback({ user ->
|
||||
_uiState.value = _uiState.value.copy(isRefreshing = false, user = user)
|
||||
}, { apiFailure ->
|
||||
_uiState.value = _uiState.value.copy(isRefreshing = false, error = apiFailure.errorMessage)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package eu.m724.vastapp.activity.dashboard.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||
|
||||
class Billing {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BillingScreen(dashboardViewModel: DashboardViewModel) {
|
||||
|
||||
}
|
|
@ -1,20 +1,14 @@
|
|||
package eu.m724.vastapp.dashboard
|
||||
package eu.m724.vastapp.activity.dashboard.screen
|
||||
|
||||
import android.widget.Space
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.ShoppingCart
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -28,20 +22,20 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.modifier.modifierLocalMapOf
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.FontScaling
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.m724.vastapp.R
|
||||
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
import org.json.JSONObject
|
||||
|
||||
class Dashboard {
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun DashboardScreen(user: User) {
|
||||
fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
||||
var balance by rememberSaveable { mutableDoubleStateOf(user.credit) }
|
||||
var remainingTime by rememberSaveable { mutableIntStateOf(0) }
|
||||
|
||||
|
@ -114,6 +108,70 @@ fun DashboardScreen(user: User) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.width(340.dp)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.weight(1f),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = balanceCardColor(balance)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.baseline_account_balance_wallet_24),
|
||||
contentDescription = "Balance"
|
||||
)
|
||||
Spacer(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "$%.2f".format(balance),
|
||||
fontSize = 22.sp,
|
||||
color = balanceColor(balance, user.balanceThreshold)
|
||||
)
|
||||
}
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.baseline_access_time_filled_24),
|
||||
contentDescription = "Remaining time"
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = formatTime(remainingTime),
|
||||
fontSize = 22.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.width(340.dp)
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package eu.m724.vastapp.activity.dashboard.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||
import org.json.JSONObject
|
||||
|
||||
class Instances {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
||||
Column {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InstanceCard(instance: JSONObject, modifier: Modifier = Modifier) {
|
||||
Card(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = instance.getString("id"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewInstanceCard() {
|
||||
val instance = JSONObject()
|
||||
instance.put("id", 3423941)
|
||||
InstanceCard(instance = instance, modifier = Modifier.size(300.dp))
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
package eu.m724.vastapp
|
||||
package eu.m724.vastapp.activity.dashboard.screen
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.Menu
|
||||
import androidx.compose.material.icons.outlined.ShoppingCart
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import eu.m724.vastapp.R
|
||||
|
||||
sealed class Screen(val route: String, @StringRes val resourceId: Int, val icon: ImageVector) {
|
||||
object Dashboard : Screen("dashboard", R.string.nav_dashboard, Icons.Outlined.Home)
|
|
@ -1,6 +1,5 @@
|
|||
package eu.m724.vastapp
|
||||
package eu.m724.vastapp.activity.login
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
|
@ -37,9 +36,13 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
|
@ -50,20 +53,17 @@ import androidx.compose.ui.draw.rotate
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import eu.m724.vastapp.BuildConfig
|
||||
import eu.m724.vastapp.activity.dashboard.DashboardActivity
|
||||
import eu.m724.vastapp.ui.theme.VastappTheme
|
||||
import eu.m724.vastapp.vastai.api.ApiFailure
|
||||
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
import kotlinx.coroutines.launch
|
||||
import org.chromium.net.CronetEngine
|
||||
import org.chromium.net.UrlRequest
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.random.Random
|
||||
|
||||
class LoginActivity : ComponentActivity() {
|
||||
private val executor: Executor = Executors.newSingleThreadExecutor()
|
||||
private lateinit var cronetEngine: CronetEngine
|
||||
private lateinit var dashboardLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -73,9 +73,23 @@ class LoginActivity : ComponentActivity() {
|
|||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result -> finish() } // TODO re-login here
|
||||
|
||||
cronetEngine = CronetEngine.Builder(applicationContext).build()
|
||||
val tryLogin: (String, (ApiFailure) -> Unit) -> Unit =
|
||||
{ apiKey, onFailure -> tryLogin(apiKey, { user -> loadApp(user) }, onFailure) }
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val cronetEngine = CronetEngine.Builder(baseContext).build()
|
||||
val loginViewModel = LoginViewModel(cronetEngine, executor)
|
||||
|
||||
loginViewModel.error.observe(this) { errorMessage ->
|
||||
if (errorMessage != null) {
|
||||
Toast.makeText(baseContext, errorMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch { // TODO I was suggested not to launch an activity from a lifecyclescope
|
||||
loginViewModel.uiState.collect { state ->
|
||||
if (state is LoginUiState.Success) {
|
||||
loadApp(state.user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
|
@ -89,7 +103,7 @@ class LoginActivity : ComponentActivity() {
|
|||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
LoginApp(applicationContext, tryLogin)
|
||||
LoginApp(loginViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,25 +119,16 @@ class LoginActivity : ComponentActivity() {
|
|||
finish()
|
||||
}
|
||||
|
||||
private fun tryLogin(apiKey: String, onSuccess: (User) -> Unit, onFailure: (ApiFailure) -> Unit) {
|
||||
val requestBuilder = cronetEngine.newUrlRequestBuilder(
|
||||
"https://console.vast.ai/api/v0/users/current",
|
||||
UserUrlRequestCallback(onSuccess, onFailure),
|
||||
executor
|
||||
).addHeader("Authorization", "Bearer $apiKey")
|
||||
|
||||
val request: UrlRequest = requestBuilder.build()
|
||||
request.start()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginApp(context: Context, tryLogin: (String, (ApiFailure) -> Unit) -> Unit) {
|
||||
fun LoginApp(loginViewModel: LoginViewModel) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val uiState by loginViewModel.uiState.collectAsState()
|
||||
val loginErrorMessage by loginViewModel.error.observeAsState() // TODO put this in uistate
|
||||
val isIdle by remember(uiState) { derivedStateOf { uiState !is LoginUiState.Loading } }
|
||||
|
||||
var apiKey by rememberSaveable { mutableStateOf(BuildConfig.VASTAI_KEY ?: "") }
|
||||
var loading by rememberSaveable { mutableStateOf(false) }
|
||||
var advancedOpen by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val transition = updateTransition(targetState = advancedOpen, label = "Advanced Menu Transition")
|
||||
|
@ -131,15 +136,6 @@ fun LoginApp(context: Context, tryLogin: (String, (ApiFailure) -> Unit) -> Unit)
|
|||
if (state) 180f else 0f
|
||||
}
|
||||
|
||||
val login: () -> Unit = {
|
||||
tryLogin(apiKey) { apiFailure ->
|
||||
loading = false
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(context, apiFailure.errorMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.width(300.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
|
@ -147,18 +143,19 @@ fun LoginApp(context: Context, tryLogin: (String, (ApiFailure) -> Unit) -> Unit)
|
|||
) {
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !loading,
|
||||
enabled = isIdle,
|
||||
value = apiKey,
|
||||
onValueChange = { apiKey = it },
|
||||
label = { Text(text = "API key") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
singleLine = true
|
||||
singleLine = true,
|
||||
isError = loginErrorMessage != null
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Row {
|
||||
TextButton(
|
||||
enabled = !loading,
|
||||
enabled = isIdle,
|
||||
onClick = {
|
||||
advancedOpen = !advancedOpen
|
||||
}
|
||||
|
@ -172,13 +169,12 @@ fun LoginApp(context: Context, tryLogin: (String, (ApiFailure) -> Unit) -> Unit)
|
|||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Button(
|
||||
enabled = !loading,
|
||||
enabled = isIdle,
|
||||
onClick = {
|
||||
loading = true
|
||||
login()
|
||||
loginViewModel.tryLogin(apiKey)
|
||||
}
|
||||
) {
|
||||
if (loading) {
|
||||
if (uiState is LoginUiState.Loading) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
Text("Log in")
|
|
@ -0,0 +1,22 @@
|
|||
package eu.m724.vastapp.activity.login
|
||||
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
|
||||
sealed interface LoginUiState {
|
||||
/**
|
||||
* when nothing is being done
|
||||
* you should get the error
|
||||
*/
|
||||
data object Idle : LoginUiState
|
||||
|
||||
/**
|
||||
* when we're currently logging in
|
||||
*/
|
||||
data object Loading : LoginUiState
|
||||
|
||||
/**
|
||||
* called when logged in
|
||||
* @param user the logged in user
|
||||
*/
|
||||
data class Success(val user: User) : LoginUiState
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package eu.m724.vastapp.activity.login
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import eu.m724.vastapp.vastai.ApiFailure
|
||||
import eu.m724.vastapp.vastai.ApiRoute
|
||||
import eu.m724.vastapp.vastai.VastApi
|
||||
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.chromium.net.CronetEngine
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class LoginViewModel(val cronetEngine: CronetEngine, val executor: Executor) : ViewModel() {
|
||||
private val _uiState: MutableStateFlow<LoginUiState> =
|
||||
MutableStateFlow(LoginUiState.Idle)
|
||||
val uiState: StateFlow<LoginUiState> =
|
||||
_uiState.asStateFlow()
|
||||
|
||||
private val _error = MutableLiveData<String?>(null)
|
||||
val error: LiveData<String?> = _error // TODO put this in uistate
|
||||
|
||||
fun tryLogin(apiKey: String) {
|
||||
val vastApi = VastApi(apiKey, cronetEngine, executor)
|
||||
val request = vastApi.buildRequest(
|
||||
ApiRoute.SHOW_USER,
|
||||
UserUrlRequestCallback({ user ->
|
||||
_uiState.value = LoginUiState.Success(user)
|
||||
}, { apiFailure ->
|
||||
_uiState.value = LoginUiState.Idle
|
||||
_error.value = apiFailure.errorMessage
|
||||
})
|
||||
)
|
||||
|
||||
_uiState.value = LoginUiState.Loading
|
||||
request.start()
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
package eu.m724.vastapp.dashboard
|
||||
|
||||
class Billing {
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package eu.m724.vastapp.dashboard
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
|
||||
class Instances {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InstancesScreen() {
|
||||
Column {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Instance() {
|
||||
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
package eu.m724.vastapp.vastai.api
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
package eu.m724.vastapp.vastai
|
||||
|
||||
data class ApiFailure(
|
||||
/** user friendly error message */
|
6
app/src/main/java/eu/m724/vastapp/vastai/ApiRoute.kt
Normal file
6
app/src/main/java/eu/m724/vastapp/vastai/ApiRoute.kt
Normal file
|
@ -0,0 +1,6 @@
|
|||
package eu.m724.vastapp.vastai
|
||||
|
||||
enum class ApiRoute(val path: String, val method: String) {
|
||||
SHOW_USER("/users/current", "GET"),
|
||||
GET_INSTANCES("/instances", "GET")
|
||||
}
|
44
app/src/main/java/eu/m724/vastapp/vastai/VastApi.kt
Normal file
44
app/src/main/java/eu/m724/vastapp/vastai/VastApi.kt
Normal file
|
@ -0,0 +1,44 @@
|
|||
package eu.m724.vastapp.vastai
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import eu.m724.vastapp.BuildConfig
|
||||
import org.chromium.net.CronetEngine
|
||||
import org.chromium.net.UrlRequest
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class VastApi(
|
||||
val apiKey: String,
|
||||
val cronetEngine: CronetEngine,
|
||||
val executor: Executor
|
||||
) {
|
||||
/**
|
||||
* build an api request
|
||||
* don't forget to call .start() on the returned [UrlRequest]
|
||||
*
|
||||
* @param endpoint the endpoint path starting with a slash like /users/current
|
||||
* @param callback any callback for example [UserUrlRequestCallback]
|
||||
* @return an [UrlRequest] you must .start() yourself
|
||||
*/
|
||||
fun buildRequest(endpoint: String, callback: UrlRequest.Callback): UrlRequest {
|
||||
val requestBuilder = cronetEngine.newUrlRequestBuilder(
|
||||
BuildConfig.VASIAI_API_ENDPOINT + endpoint,
|
||||
callback,
|
||||
executor
|
||||
).addHeader("Authorization", "Bearer $apiKey")
|
||||
|
||||
return requestBuilder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* build an api request
|
||||
* don't forget to call .start() on the returned [UrlRequest]
|
||||
*
|
||||
* @param apiRoute the api route
|
||||
* @param callback any callback for example [UserUrlRequestCallback]
|
||||
* @return an [UrlRequest] you must .start() yourself
|
||||
*/
|
||||
fun buildRequest(apiRoute: ApiRoute, callback: UrlRequest.Callback): UrlRequest {
|
||||
return buildRequest(apiRoute.path, callback)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package eu.m724.vastapp.vastai.api
|
||||
|
||||
import eu.m724.vastapp.vastai.ApiFailure
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
import org.chromium.net.CronetException
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import org.json.JSONObject
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.CodingErrorAction
|
||||
|
||||
class InstancesUrlRequestCallback(
|
||||
val onSuccess: (List<JSONObject>) -> Unit,
|
||||
val onFailure: (ApiFailure) -> Unit
|
||||
) : UrlRequest.Callback() {
|
||||
|
||||
private val stringResponse = StringBuilder()
|
||||
|
||||
override fun onRedirectReceived(
|
||||
request: UrlRequest?,
|
||||
info: UrlResponseInfo?,
|
||||
newLocationUrl: String?
|
||||
) {
|
||||
request?.followRedirect()
|
||||
}
|
||||
|
||||
override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {
|
||||
request?.read(ByteBuffer.allocateDirect(102400))
|
||||
}
|
||||
|
||||
override fun onReadCompleted(
|
||||
request: UrlRequest?,
|
||||
info: UrlResponseInfo?,
|
||||
byteBuffer: ByteBuffer?
|
||||
) {
|
||||
byteBuffer?.clear()
|
||||
request?.read(byteBuffer)
|
||||
|
||||
stringResponse.append(Charsets.UTF_8.newDecoder().onUnmappableCharacter(CodingErrorAction.IGNORE).decode(byteBuffer))
|
||||
}
|
||||
|
||||
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
|
||||
if (info?.httpStatusCode == 200) {
|
||||
val jsonResponse = JSONObject(stringResponse.toString())
|
||||
val instances = ArrayList<JSONObject>()
|
||||
|
||||
val instancesJson = jsonResponse.getJSONArray("instances")
|
||||
for (i in 0..<instancesJson.length()) {
|
||||
instances.add(instancesJson.getJSONObject(i))
|
||||
}
|
||||
|
||||
onSuccess(instances) // TODO make it better
|
||||
} else {
|
||||
onFailure(ApiFailure("${info?.httpStatusCode} ${info?.httpStatusText}"))
|
||||
println("API error: $stringResponse")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailed(request: UrlRequest?, info: UrlResponseInfo?, error: CronetException?) {
|
||||
onFailure(ApiFailure("Network error: ${error?.message ?: "Unknown"}"))
|
||||
}
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
package eu.m724.vastapp.vastai.api
|
||||
|
||||
import eu.m724.vastapp.vastai.ApiFailure
|
||||
import eu.m724.vastapp.vastai.data.User
|
||||
import org.chromium.net.CronetException
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.WritableByteChannel
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.CodingErrorAction
|
||||
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ composeBom = "2024.06.00"
|
|||
playServicesCronet = "18.1.0"
|
||||
navigationCompose = "2.7.7"
|
||||
secretsGradlePlugin = "2.0.1"
|
||||
runtimeLivedata = "1.6.8"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
|
@ -33,6 +34,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
|
|||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
play-services-cronet = { module = "com.google.android.gms:play-services-cronet", version.ref = "playServicesCronet" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
Loading…
Reference in a new issue