diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index aaa5e6b..fb3d025 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7827d93..3410faa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,12 +14,12 @@
android:enableOnBackInvokedCallback="true"
tools:targetApi="34">
diff --git a/app/src/main/java/eu/m724/vastapp/DashboardActivity.kt b/app/src/main/java/eu/m724/vastapp/DashboardActivity.kt
deleted file mode 100644
index 4f7ef7d..0000000
--- a/app/src/main/java/eu/m724/vastapp/DashboardActivity.kt
+++ /dev/null
@@ -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")!! // 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()
- ) {
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardActivity.kt b/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardActivity.kt
new file mode 100644
index 0000000..48d09d4
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardActivity.kt
@@ -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")!! // 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, 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)) }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardUiState.kt b/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardUiState.kt
new file mode 100644
index 0000000..6befee9
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardUiState.kt
@@ -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?
+) { }
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardViewModel.kt b/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardViewModel.kt
new file mode 100644
index 0000000..ac45fce
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardViewModel.kt
@@ -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 =
+ MutableStateFlow(DashboardUiState(false, _user, null))
+ val uiState: StateFlow =
+ _uiState.asStateFlow()
+
+ private val _navigationEvent = MutableSharedFlow()
+ val navigationEvent: SharedFlow = _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)
+ })
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Billing.kt b/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Billing.kt
new file mode 100644
index 0000000..9dced3c
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Billing.kt
@@ -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) {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/dashboard/Dashboard.kt b/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Dashboard.kt
similarity index 65%
rename from app/src/main/java/eu/m724/vastapp/dashboard/Dashboard.kt
rename to app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Dashboard.kt
index d37e4d4..3e41b0c 100644
--- a/app/src/main/java/eu/m724/vastapp/dashboard/Dashboard.kt
+++ b/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Dashboard.kt
@@ -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)
+ ) {
+
+ }
}
}
diff --git a/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Instances.kt b/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Instances.kt
new file mode 100644
index 0000000..8be9036
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Instances.kt
@@ -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))
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/Screen.kt b/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Screen.kt
similarity index 81%
rename from app/src/main/java/eu/m724/vastapp/Screen.kt
rename to app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Screen.kt
index e4ca9f4..214c6f5 100644
--- a/app/src/main/java/eu/m724/vastapp/Screen.kt
+++ b/app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Screen.kt
@@ -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)
diff --git a/app/src/main/java/eu/m724/vastapp/LoginActivity.kt b/app/src/main/java/eu/m724/vastapp/activity/login/LoginActivity.kt
similarity index 84%
rename from app/src/main/java/eu/m724/vastapp/LoginActivity.kt
rename to app/src/main/java/eu/m724/vastapp/activity/login/LoginActivity.kt
index 6da9ac4..4752b7c 100644
--- a/app/src/main/java/eu/m724/vastapp/LoginActivity.kt
+++ b/app/src/main/java/eu/m724/vastapp/activity/login/LoginActivity.kt
@@ -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
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")
diff --git a/app/src/main/java/eu/m724/vastapp/activity/login/LoginUiState.kt b/app/src/main/java/eu/m724/vastapp/activity/login/LoginUiState.kt
new file mode 100644
index 0000000..edefd24
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/activity/login/LoginUiState.kt
@@ -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
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/activity/login/LoginViewModel.kt b/app/src/main/java/eu/m724/vastapp/activity/login/LoginViewModel.kt
new file mode 100644
index 0000000..f952600
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/activity/login/LoginViewModel.kt
@@ -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 =
+ MutableStateFlow(LoginUiState.Idle)
+ val uiState: StateFlow =
+ _uiState.asStateFlow()
+
+ private val _error = MutableLiveData(null)
+ val error: LiveData = _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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/dashboard/Billing.kt b/app/src/main/java/eu/m724/vastapp/dashboard/Billing.kt
deleted file mode 100644
index ff7fa1f..0000000
--- a/app/src/main/java/eu/m724/vastapp/dashboard/Billing.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package eu.m724.vastapp.dashboard
-
-class Billing {
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/dashboard/Instances.kt b/app/src/main/java/eu/m724/vastapp/dashboard/Instances.kt
deleted file mode 100644
index bdd6774..0000000
--- a/app/src/main/java/eu/m724/vastapp/dashboard/Instances.kt
+++ /dev/null
@@ -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() {
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/vastai/api/ApiFailure.kt b/app/src/main/java/eu/m724/vastapp/vastai/ApiFailure.kt
similarity index 51%
rename from app/src/main/java/eu/m724/vastapp/vastai/api/ApiFailure.kt
rename to app/src/main/java/eu/m724/vastapp/vastai/ApiFailure.kt
index faa0e6f..15d6a76 100644
--- a/app/src/main/java/eu/m724/vastapp/vastai/api/ApiFailure.kt
+++ b/app/src/main/java/eu/m724/vastapp/vastai/ApiFailure.kt
@@ -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 */
diff --git a/app/src/main/java/eu/m724/vastapp/vastai/ApiRoute.kt b/app/src/main/java/eu/m724/vastapp/vastai/ApiRoute.kt
new file mode 100644
index 0000000..0fc7571
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/vastai/ApiRoute.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/vastai/VastApi.kt b/app/src/main/java/eu/m724/vastapp/vastai/VastApi.kt
new file mode 100644
index 0000000..9edb9a5
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/vastai/VastApi.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/vastapp/vastai/api/InstancesUrlRequestCallback.kt b/app/src/main/java/eu/m724/vastapp/vastai/api/InstancesUrlRequestCallback.kt
new file mode 100644
index 0000000..0a54fb5
--- /dev/null
+++ b/app/src/main/java/eu/m724/vastapp/vastai/api/InstancesUrlRequestCallback.kt
@@ -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) -> 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()
+
+ val instancesJson = jsonResponse.getJSONArray("instances")
+ for (i in 0..