Compare commits
No commits in common. "c5a401c1e5808531beb20581ea898e7a994e2791" and "67ffa975cb5bab5c00823e4d8fd51822d142219d" have entirely different histories.
c5a401c1e5
...
67ffa975cb
11 changed files with 105 additions and 240 deletions
|
@ -62,6 +62,7 @@ android {
|
|||
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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,12 +141,15 @@ fun InstancesCard(rentedInstancesCount: Int) {
|
|||
modifier = Modifier.width(12.dp)
|
||||
)
|
||||
Text(
|
||||
text = rentedInstancesCount.toString(),
|
||||
text = rentedInstances.size.toString(),
|
||||
fontSize = 22.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun balanceCardColor(balance: Double): Color {
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in a new issue