Compare commits

..

No commits in common. "c5a401c1e5808531beb20581ea898e7a994e2791" and "67ffa975cb5bab5c00823e4d8fd51822d142219d" have entirely different histories.

11 changed files with 105 additions and 240 deletions

View file

@ -62,6 +62,7 @@ android {
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)

View file

@ -115,10 +115,10 @@ class DashboardViewModel(
fun toggleInstance(instance: RentedInstance) {
val deferred =
if (instance.isRunning()) {
vastApi.stopInstance(instance.rentalId)
} else {
if (instance.status == "running") {
vastApi.startInstance(instance.rentalId)
} else {
vastApi.stopInstance(instance.rentalId)
}
viewModelScope.launch {
@ -144,4 +144,5 @@ class DashboardViewModel(
}
} // TODO once again these methods share some code and more probably will so why not move the shared stuff
// OR not refresh but refresh only instances or even better don't refresh instances but delete or edit that one
}

View file

@ -72,21 +72,34 @@ fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
horizontalArrangement = Arrangement.Center
) {
// balance card
BalanceCard(balance = user.credit, balanceWarning = user.balanceThreshold)
Card(
modifier = Modifier
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = balanceCardColor(user.credit)
)
) {
Row(
modifier = Modifier.padding(16.dp, 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(id = R.drawable.baseline_monetization_on_24),
contentDescription = stringResource(id = R.string.balance)
)
Spacer(
modifier = Modifier.width(12.dp)
)
Text(
text = "$%.2f".format(user.credit),
fontSize = 22.sp,
color = balanceColor(user.credit, user.balanceThreshold)
)
}
}
// time card
if (rentedInstances.isNotEmpty())
RemainingTimeCard(remainingTime = remainingTime)
// instances
InstancesCard(rentedInstancesCount = rentedInstances.size)
}
}
}
}
@Composable
fun RemainingTimeCard(remainingTime: Int) {
Card(
modifier = Modifier
.padding(16.dp)
@ -109,40 +122,8 @@ fun RemainingTimeCard(remainingTime: Int) {
)
}
}
}
@Composable
fun BalanceCard(balance: Double, balanceWarning: Double) {
Card(
modifier = Modifier
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = balanceCardColor(balance)
)
) {
Row(
modifier = Modifier.padding(16.dp, 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(id = R.drawable.baseline_monetization_on_24),
contentDescription = stringResource(id = R.string.balance)
)
Spacer(
modifier = Modifier.width(12.dp)
)
Text(
text = "$%.2f".format(balance),
fontSize = 22.sp,
color = balanceColor(balance, balanceWarning)
)
}
}
}
@Composable
fun InstancesCard(rentedInstancesCount: Int) {
// instances
Card(
modifier = Modifier
.padding(16.dp)
@ -160,11 +141,14 @@ fun InstancesCard(rentedInstancesCount: Int) {
modifier = Modifier.width(12.dp)
)
Text(
text = rentedInstancesCount.toString(),
text = rentedInstances.size.toString(),
fontSize = 22.sp
)
}
}
}
}
}
}
@Composable

View file

@ -2,14 +2,13 @@ package eu.m724.vastapp.activity.dashboard.screen
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ContextualFlowRow
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -19,17 +18,14 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -79,19 +75,15 @@ fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
)
}
} else {
Column(
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.no_instances)
)
Text(
text = stringResource(id = R.string.rent_on_website)
)
}
}
}
@ -107,34 +99,13 @@ fun RentedInstanceCard(
deleteButtonClick: (RentedInstance) -> Unit,
) {
val instance by remember(rentedInstance) { derivedStateOf { rentedInstance.instance } }
val dialogOpen = remember { mutableStateOf(false) }
if (dialogOpen.value) {
InstanceDeleteDialog(
instance = rentedInstance,
onConfirm = {
dialogOpen.value = false
deleteButtonClick(rentedInstance)
},
onClose = { dialogOpen.value = false }
)
}
val label by remember(instance) { derivedStateOf {
rentedInstance.label ?: instance.machine.gpu.model
} }
Card(modifier = modifier) {
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
.padding(8.dp)
) {
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly // TODO I think the label is too low
) {
Text(rentedInstance.getName(), fontSize = 22.sp)
Text(rentedInstance.status, fontSize = 14.sp)
}
Row(modifier = Modifier.padding(8.dp)) {
Text(label, fontSize = 22.sp)
Spacer(modifier = Modifier.weight(1f))
Column(
@ -144,7 +115,7 @@ fun RentedInstanceCard(
Button(
modifier = Modifier.size(24.dp),
contentPadding = PaddingValues(0.dp),
onClick = { dialogOpen.value = true },
onClick = { deleteButtonClick(rentedInstance) },
) {
Icon(
modifier = Modifier.size(16.dp),
@ -159,9 +130,9 @@ fun RentedInstanceCard(
modifier = Modifier.size(24.dp),
contentPadding = PaddingValues(0.dp),
onClick = { actionButtonClick(rentedInstance) },
enabled = !rentedInstance.isChangingState()
enabled = rentedInstance.status == rentedInstance.targetStatus
) {
if (rentedInstance.isRunning()) {
if (rentedInstance.status == "running") {
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(id = R.drawable.baseline_stop_24),
@ -206,52 +177,9 @@ fun RentedInstanceCard(
Spacer(modifier = Modifier.height(6.dp))
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Details about instance ${rentedInstance.getName()}"
contentDescription = "Details about instance $label"
)
}
}
}
}
@Composable
fun InstanceDeleteDialog(
instance: RentedInstance,
onConfirm: () -> Unit,
onClose: () -> Unit,
) {
AlertDialog(
onDismissRequest = { onClose() },
confirmButton = {
TextButton(
onClick = { onConfirm() }
) {
Text("Confirm")
}
},
dismissButton = {
TextButton(
onClick = { onClose() }
) {
Text("Dismiss")
}
},
title = {
Text(
text = stringResource(
id = R.string.instance_confirm_delete
)
)
},
text = {
Text(
text = stringResource(
id = R.string.instance_confirm_delete_text,
instance.rentalId,
instance.getName()
)
)
},
)
}

View file

@ -60,6 +60,8 @@ import eu.m724.vastapp.activity.dashboard.DashboardActivity
import eu.m724.vastapp.ui.theme.VastappTheme
import eu.m724.vastapp.vastai.data.User
import kotlinx.coroutines.launch
import org.chromium.net.CronetEngine
import java.util.concurrent.Executors
class LoginActivity : ComponentActivity() {
private lateinit var dashboardLauncher: ActivityResultLauncher<Intent>
@ -71,25 +73,21 @@ class LoginActivity : ComponentActivity() {
ActivityResultContracts.StartActivityForResult()
) { _ -> finish() } // TODO re-login here
val loginViewModel = LoginViewModel(application)
val executor = Executors.newSingleThreadExecutor()
val cronetEngine = CronetEngine.Builder(baseContext).build()
val loginViewModel = LoginViewModel(application, cronetEngine, executor)
// load api key if saved
loginViewModel.init()
// handle login errors
loginViewModel.error.observe(this) { errorMessage ->
if (errorMessage != null) {
Toast.makeText(baseContext, errorMessage, Toast.LENGTH_SHORT).show()
}
}
// handle loading error
val loadingError = intent.getStringExtra("error")
if (loadingError != null) {
Toast.makeText(baseContext, loadingError, Toast.LENGTH_SHORT).show()
}
// load app if it's time
lifecycleScope.launch { // TODO I was suggested not to launch an activity from a lifecycle scope
loginViewModel.uiState.collect { state ->
if (state is LoginUiState.Success) {

View file

@ -5,17 +5,19 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import eu.m724.vastapp.BuildConfig
import eu.m724.vastapp.VastApplication
import eu.m724.vastapp.vastai.Account
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.chromium.net.CronetEngine
import java.util.concurrent.Executor
class LoginViewModel(
application: Application
application: Application,
private val cronetEngine: CronetEngine,
private val executor: Executor
) : AndroidViewModel(application) {
private val _uiState: MutableStateFlow<LoginUiState> =
MutableStateFlow(LoginUiState.Idle)
@ -30,19 +32,13 @@ class LoginViewModel(
private val application = getApplication<VastApplication>()
fun init() {
_apiKey.value = application.loadKey() ?: BuildConfig.VASTAI_KEY
}
fun tryLogin() {
application.vastApi.setApiKey(apiKey.value)
val userDeferred = application.vastApi.getUser()
viewModelScope.launch {
try {
val user = userDeferred.await()
application.submitKey(apiKey.value) // TODO toggle for this
application.account = Account(user)
_uiState.update { LoginUiState.Success(user) }
} catch (e: Exception) {
_uiState.update { LoginUiState.Idle }

View file

@ -56,7 +56,7 @@ class VastApi(
val deferred = CompletableDeferred<Unit>()
val request = requestMaker.buildRequest(
"/instances/$rentalId/", // THIS NEEDS A / AT THE END
"/instances/$rentalId",
JsonUrlRequestCallback(
onSuccess = {
if (it.getBoolean("success"))
@ -77,7 +77,7 @@ class VastApi(
val deferred = CompletableDeferred<Unit>()
val request = requestMaker.buildRequest(
"/instances/$rentalId/", // THIS NEEDS A / AT THE END
"/instances/$rentalId",
JsonUrlRequestCallback(
onSuccess = {
deferred.complete(Unit)
@ -97,7 +97,7 @@ class VastApi(
val deferred = CompletableDeferred<Unit>()
val request = requestMaker.buildRequest(
"/instances/$rentalId/", // THIS NEEDS A / AT THE END
"/instances/$rentalId",
JsonUrlRequestCallback(
onSuccess = {
deferred.complete(Unit)

View file

@ -25,8 +25,7 @@ open class StringUrlRequestCallback(
}
override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {
val size = info?.allHeaders?.get("content-length")?.get(0)?.toIntOrNull()
request?.read(ByteBuffer.allocateDirect(size ?: 100000))
request?.read(ByteBuffer.allocateDirect(102400))
}
override fun onReadCompleted(
@ -37,19 +36,13 @@ open class StringUrlRequestCallback(
byteBuffer?.clear()
request?.read(byteBuffer)
stringResponse.append(
Charsets.UTF_8.newDecoder()
.onUnmappableCharacter(CodingErrorAction.IGNORE)
.decode(byteBuffer)
)
stringResponse.append(Charsets.UTF_8.newDecoder().onUnmappableCharacter(CodingErrorAction.IGNORE).decode(byteBuffer))
}
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
if (info != null) {
val body = stringResponse.toString()
val statusCode = info.httpStatusCode
println(info.httpStatusCode.toString() + ": " + stringResponse.substring(0, stringResponse.length))
println(info.allHeaders.toString())
if (statusCode == 200) {
try {
@ -59,7 +52,7 @@ open class StringUrlRequestCallback(
}
} else if (statusCode >= 500) {
onFailure(ServerError(statusCode, body))
} else if (statusCode == 401) {
} else if (statusCode == 403) {
onFailure(UnauthorizedException(body))
} else {
onFailure(ClientException(body))

View file

@ -72,34 +72,4 @@ data class RentedInstance(
)
}
}
/**
* gpu model or label if set
* @return instance name
*/
fun getName(): String {
return label ?: instance.machine.gpu.model
}
/**
* returns whether the instance is running
* this is true also if the instance is stopping
* vice versa, false if stopped or starting
* @return is the instance running
*/
fun isRunning(): Boolean {
return status == "running"
}
fun isStopping(): Boolean {
return status == "running" && targetStatus == "stopped"
}
fun isStarting(): Boolean {
return status == "exited" && targetStatus == "running"
}
fun isChangingState(): Boolean {
return isStopping() || isStarting()
}
}

View file

@ -4,7 +4,7 @@
<string name="title_activity_login">Logowanie</string>
<string name="nav_dashboard">Kokpit</string>
<string name="nav_billing">Płatności</string>
<string name="nav_instances">Instancje</string>
<string name="nav_instances">Maszyny</string>
<string name="nav_help">Pomoc</string>
<string name="balance">Bilans</string>
<string name="greeting">Witaj %1$s!</string>
@ -27,10 +27,7 @@
<string name="termux_not_configured">Termux nie jest skonfigurowany pod działanie z innymi aplikacjami.</string>
<string name="termux_open_instructions">Otwórz instrukcje na github.com</string>
<string name="termux_error">Wystąpił błąd:</string>
<string name="no_instances">Nie wynajmujesz żadnych instancji</string>
<string name="no_instances">Nie wynajmujesz żadnych maszyn</string>
<string name="webview_todo">(kiedyś to będzie tutaj)</string>
<string name="termux_no_ssh">Brakuje klienta SSH na Termux</string>
<string name="rent_on_website">Jeszcze nie możesz wynajmować z tej aplikacji</string>
<string name="instance_confirm_delete">Potwierdź usunięcie</string>
<string name="instance_confirm_delete_text">Instancja #%1$d (%2$s)</string>
</resources>

View file

@ -31,7 +31,4 @@
<string name="webview_todo">(this will be a webview)</string>
<string name="no_instances">You are not renting any instances</string>
<string name="termux_no_ssh">Missing SSH client on Termux</string>
<string name="rent_on_website">You can\'t rent from this app yet</string>
<string name="instance_confirm_delete">Confirm deletion</string>
<string name="instance_confirm_delete_text">Instance #%1$d (%2$s)</string>
</resources>