From c785b73c3c6d325bacca385d083ba6408f9dd081 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Thu, 25 Jul 2024 13:17:45 +0200 Subject: [PATCH] androidify --- app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 4 +- .../java/eu/m724/vastapp/DashboardActivity.kt | 128 ------------------ .../activity/dashboard/DashboardActivity.kt | 113 ++++++++++++++++ .../activity/dashboard/DashboardUiState.kt | 9 ++ .../activity/dashboard/DashboardViewModel.kt | 45 ++++++ .../activity/dashboard/screen/Billing.kt | 12 ++ .../dashboard/screen}/Dashboard.kt | 78 +++++++++-- .../activity/dashboard/screen/Instances.kt | 43 ++++++ .../{ => activity/dashboard/screen}/Screen.kt | 6 +- .../{ => activity/login}/LoginActivity.kt | 78 +++++------ .../vastapp/activity/login/LoginUiState.kt | 22 +++ .../vastapp/activity/login/LoginViewModel.kt | 43 ++++++ .../java/eu/m724/vastapp/dashboard/Billing.kt | 4 - .../eu/m724/vastapp/dashboard/Instances.kt | 20 --- .../vastapp/vastai/{api => }/ApiFailure.kt | 5 +- .../java/eu/m724/vastapp/vastai/ApiRoute.kt | 6 + .../java/eu/m724/vastapp/vastai/VastApi.kt | 44 ++++++ .../vastai/api/InstancesUrlRequestCallback.kt | 62 +++++++++ .../vastai/api/UserUrlRequestCallback.kt | 5 +- gradle/libs.versions.toml | 2 + 21 files changed, 515 insertions(+), 217 deletions(-) delete mode 100644 app/src/main/java/eu/m724/vastapp/DashboardActivity.kt create mode 100644 app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardActivity.kt create mode 100644 app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardUiState.kt create mode 100644 app/src/main/java/eu/m724/vastapp/activity/dashboard/DashboardViewModel.kt create mode 100644 app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Billing.kt rename app/src/main/java/eu/m724/vastapp/{dashboard => activity/dashboard/screen}/Dashboard.kt (65%) create mode 100644 app/src/main/java/eu/m724/vastapp/activity/dashboard/screen/Instances.kt rename app/src/main/java/eu/m724/vastapp/{ => activity/dashboard/screen}/Screen.kt (81%) rename app/src/main/java/eu/m724/vastapp/{ => activity/login}/LoginActivity.kt (84%) create mode 100644 app/src/main/java/eu/m724/vastapp/activity/login/LoginUiState.kt create mode 100644 app/src/main/java/eu/m724/vastapp/activity/login/LoginViewModel.kt delete mode 100644 app/src/main/java/eu/m724/vastapp/dashboard/Billing.kt delete mode 100644 app/src/main/java/eu/m724/vastapp/dashboard/Instances.kt rename app/src/main/java/eu/m724/vastapp/vastai/{api => }/ApiFailure.kt (51%) create mode 100644 app/src/main/java/eu/m724/vastapp/vastai/ApiRoute.kt create mode 100644 app/src/main/java/eu/m724/vastapp/vastai/VastApi.kt create mode 100644 app/src/main/java/eu/m724/vastapp/vastai/api/InstancesUrlRequestCallback.kt 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..