Compare commits

..

No commits in common. "2a61d22de97d0f37aca91517dbcad5bd552b801b" and "a7f0e5da8c6fb3750f1908159b14dd13d2f4aa93" have entirely different histories.

16 changed files with 167 additions and 245 deletions

View file

@ -21,7 +21,6 @@ android {
}
// don't forget to add another "s this is counter intuitive I know but not my fault
buildConfigField("Boolean", "AUTO_LOGIN", "true")
buildConfigField("String", "VASTAI_KEY", "\"\"")
buildConfigField("String", "VASIAI_API_ENDPOINT", "\"https://cloud.vast.ai/api/v0\"")
}

View file

@ -12,6 +12,7 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Vastapp"
tools:targetApi="34">

View file

@ -16,6 +16,7 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -23,11 +24,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
@ -40,12 +44,14 @@ 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@ -63,6 +69,7 @@ import eu.m724.vastapp.vastai.data.User
import kotlinx.coroutines.launch
import org.chromium.net.CronetEngine
import java.util.concurrent.Executors
import kotlin.random.Random
class LoginActivity : ComponentActivity() {
private lateinit var dashboardLauncher: ActivityResultLauncher<Intent>
@ -92,7 +99,6 @@ class LoginActivity : ComponentActivity() {
}
}
if (BuildConfig.AUTO_LOGIN)
loginViewModel.loadKey()
enableEdgeToEdge()
@ -107,17 +113,11 @@ class LoginActivity : ComponentActivity() {
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
val loading by loginViewModel.fullscreenLoading.collectAsState()
if (loading) {
CircularProgressIndicator()
} else {
LoginApp(loginViewModel)
}
}
}
}
}
}
@ -135,11 +135,17 @@ class LoginActivity : ComponentActivity() {
fun LoginApp(loginViewModel: LoginViewModel) {
val uiState by loginViewModel.uiState.collectAsState()
val loginErrorMessage by loginViewModel.error.observeAsState() // TODO put this in uiState
val apiKey by loginViewModel.apiKey.collectAsState()
val isIdle by remember(uiState) { derivedStateOf { uiState !is LoginUiState.Loading } }
var apiKey by rememberSaveable { mutableStateOf(BuildConfig.VASTAI_KEY) }
var advancedOpen by rememberSaveable { mutableStateOf(false) }
val transition = updateTransition(targetState = advancedOpen, label = "Advanced Menu Transition")
val arrowRotation by transition.animateFloat(label = "Advanced Menu Arrow Rotation") { state ->
if (state) 180f else 0f
}
Column(
modifier = Modifier
.width(300.dp)
@ -147,25 +153,38 @@ fun LoginApp(loginViewModel: LoginViewModel) {
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
KeyTextField(
TextField(
modifier = Modifier.fillMaxWidth(),
enabled = isIdle,
apiKey = apiKey,
onValueChange = { loginViewModel.onApiKeyChange(it) },
rainbowText = uiState is LoginUiState.Success,
value = apiKey,
onValueChange = { apiKey = it },
label = { Text(text = stringResource(id = R.string.api_key)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
textStyle = if (uiState is LoginUiState.Success) rainbowTextStyle() else LocalTextStyle.current,
singleLine = true,
isError = loginErrorMessage != null
)
Spacer(modifier = Modifier.height(10.dp))
Row {
AdvancedOptionsButton(
TextButton(
enabled = isIdle,
onClick = { advancedOpen = !advancedOpen },
isOpen = advancedOpen
onClick = {
advancedOpen = !advancedOpen
}
) {
Text(text = stringResource(id = R.string.advanced_options))
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.rotate(arrowRotation)
)
}
Spacer(modifier = Modifier.weight(1f))
Button(
enabled = isIdle,
onClick = {
loginViewModel.tryLogin()
loginViewModel.tryLogin(apiKey)
}
) {
if (uiState is LoginUiState.Loading) {
@ -175,62 +194,12 @@ fun LoginApp(loginViewModel: LoginViewModel) {
}
}
}
AnimatedVisibility(visible = advancedOpen) {
AdvancedOptions()
}
}
}
@Composable
fun KeyTextField(
enabled: Boolean,
apiKey: String,
onValueChange: (String) -> Unit,
rainbowText: Boolean,
isError: Boolean
) {
TextField(
modifier = Modifier.fillMaxWidth(),
label = { Text(text = stringResource(id = R.string.api_key)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
textStyle = if (rainbowText) rainbowTextStyle() else LocalTextStyle.current,
singleLine = true,
enabled = enabled,
value = apiKey,
onValueChange = onValueChange,
isError = isError
)
}
@Composable
fun AdvancedOptionsButton(
enabled: Boolean,
onClick: () -> Unit,
isOpen: Boolean
) {
val transition = updateTransition(targetState = isOpen, label = "Advanced Menu Transition")
val arrowRotation by transition.animateFloat(label = "Advanced Menu Arrow Rotation") { state ->
if (state) 180f else 0f
}
TextButton(
enabled = enabled,
onClick = onClick
) {
Text(text = stringResource(id = R.string.advanced_options))
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.rotate(arrowRotation)
)
}
}
@Composable
fun rainbowTextStyle(): TextStyle {
return LocalTextStyle.current.copy(brush = Brush.linearGradient(colors = listOf(
@ -245,6 +214,119 @@ fun rainbowTextStyle(): TextStyle {
}
@Composable
fun AdvancedOptions() {
FunGame()
fun AdvancedOptions() { // TODO put this in viewmodel
var checked by rememberSaveable { mutableStateOf(true) }
var clicks by rememberSaveable { mutableIntStateOf(0) }
var checkboxLabel by rememberSaveable { mutableIntStateOf(R.string.login_checkbox) }
var mathPassing by rememberSaveable { mutableStateOf(true) }
val clickMessages = mapOf(
20 to R.string.login_checkbox_20
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
) {
Text(text = stringResource(id = R.string.no_options))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
enabled = mathPassing,
checked = mathPassing && checked,
onCheckedChange = {
checked = it
clicks++
if (clicks in clickMessages) {
checkboxLabel = clickMessages[clicks]!!
}
}
)
Text(text = stringResource(id = checkboxLabel))
}
AnimatedVisibility(visible = mathPassing) {
MathProblem(onFail = {
mathPassing = false
checkboxLabel = R.string.login_checkbox_angry
}, onPass = {
checked = !checked
})
}
}
}
@Composable
fun MathProblem(onFail: () -> Unit, onPass: () -> Unit) {
var solved by rememberSaveable { mutableStateOf(false) }
val n1 = Random.nextInt(-10, 20)
val n2 = Random.nextInt(-10, 20)
val plus = Random.nextBoolean()
val correct = Random.nextInt(0, 3)
val solution = if (plus) n1 + n2 else n1 - n2
val solutions = mutableListOf(solution, solution, solution)
if (correct != 0) solutions[0] += Random.nextInt(5, 11)
if (correct != 1) solutions[1] -= Random.nextInt(1, 3)
if (correct != 2) solutions[2] -= Random.nextInt(5, 11)
if (solved) {
MathProblem(onFail, onPass)
} else {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
MathProblemLabel(n1 = n1, n2 = n2, sign = if (plus) "+" else "-")
NumberButton(onClick = {
if (correct == 0) {
solved = true
onPass()
} else {
onFail()
}
}, number = solutions[0])
NumberButton(onClick = {
if (correct == 1) {
solved = true
onPass()
} else {
onFail()
}
}, number = solutions[1])
NumberButton(onClick = {
if (correct == 2) {
solved = true
onPass()
} else {
onFail()
}
}, number = solutions[2])
}
}
}
@Composable
fun MathProblemLabel(n1: Int, n2: Int, sign: String) {
Text("$n1 $sign $n2 =")
}
@Composable
fun NumberButton(onClick: () -> Unit, number: Int) {
Button(
modifier = Modifier
.clip(CircleShape)
.size(30.dp),
contentPadding = PaddingValues(0.dp),
onClick = onClick
) {
Text(number.toString())
}
}

View file

@ -11,7 +11,6 @@ import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.chromium.net.CronetEngine
import java.util.concurrent.Executor
@ -26,47 +25,31 @@ class LoginViewModel(
_uiState.asStateFlow()
private val _error = MutableLiveData<String?>(null)
val error: LiveData<String?> = _error
private val _apiKey = MutableStateFlow<String>("")
var apiKey: StateFlow<String> = _apiKey.asStateFlow()
private val _fullscreenLoading = MutableStateFlow<Boolean>(false)
var fullscreenLoading: StateFlow<Boolean> = _fullscreenLoading.asStateFlow()
val error: LiveData<String?> = _error // TODO put this in uistate
private val applicationContext = getApplication<Application>().applicationContext
private val sharedPreferences = applicationContext.getSharedPreferences("login", Context.MODE_PRIVATE)
private fun saveKey() {
with (sharedPreferences.edit()) {
putString("apiKey", apiKey.value) // TODO encrypt
apply()
}
}
fun loadKey() {
val apiKey = sharedPreferences.getString("apiKey", null)
if (apiKey != null) {
_apiKey.value = apiKey
_fullscreenLoading.value = true
tryLogin()
tryLogin(apiKey)
}
}
fun tryLogin() {
val apiKey = apiKey.value
fun tryLogin(apiKey: String) {
val vastApi = VastApi(apiKey, cronetEngine, executor)
val request = vastApi.buildRequest(
ApiRoute.SHOW_USER,
UserUrlRequestCallback({ user ->
saveKey() // TODO toggle for this
with (sharedPreferences.edit()) {
putString("apiKey", apiKey) // TODO encrypt
apply()
} // TODO toggle for this
_uiState.value = LoginUiState.Success(user)
}, { apiFailure ->
_uiState.value = LoginUiState.Idle
_fullscreenLoading.value = false
_error.postValue(apiFailure.errorMessage)
})
)
@ -74,8 +57,4 @@ class LoginViewModel(
_uiState.value = LoginUiState.Loading
request.start()
}
fun onApiKeyChange(apiKey: String) {
_apiKey.update { apiKey }
}
}

View file

@ -1,144 +0,0 @@
package eu.m724.vastapp.activity.login
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.m724.vastapp.R
import kotlin.random.Random
@Composable
fun FunGame() {
var checked by rememberSaveable { mutableStateOf(true) }
var clicks by rememberSaveable { mutableIntStateOf(0) }
var checkboxLabel by rememberSaveable { mutableIntStateOf(R.string.login_checkbox) }
var mathPassing by rememberSaveable { mutableStateOf(true) }
val clickMessages = mapOf(
20 to R.string.login_checkbox_20
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
) {
Text(text = stringResource(id = R.string.no_options))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
enabled = mathPassing,
checked = mathPassing && checked,
onCheckedChange = {
checked = it
clicks++
if (clicks in clickMessages) {
checkboxLabel = clickMessages[clicks]!!
}
}
)
Text(text = stringResource(id = checkboxLabel))
}
AnimatedVisibility(visible = mathPassing) {
MathProblem(onFail = {
mathPassing = false
checkboxLabel = R.string.login_checkbox_angry
}, onPass = {
checked = !checked
})
}
}
}
@Composable
fun MathProblem(onFail: () -> Unit, onPass: () -> Unit) {
var solved by rememberSaveable { mutableStateOf(false) }
val n1 = Random.nextInt(-10, 20)
val n2 = Random.nextInt(-10, 20)
val plus = Random.nextBoolean()
val correct = Random.nextInt(0, 3)
val solution = if (plus) n1 + n2 else n1 - n2
val solutions = mutableListOf(solution, solution, solution)
if (correct != 0) solutions[0] += Random.nextInt(5, 11)
if (correct != 1) solutions[1] -= Random.nextInt(1, 3)
if (correct != 2) solutions[2] -= Random.nextInt(5, 11)
if (solved) {
MathProblem(onFail, onPass)
} else {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
MathProblemLabel(n1 = n1, n2 = n2, sign = if (plus) "+" else "-")
NumberButton(onClick = {
if (correct == 0) {
solved = true
onPass()
} else {
onFail()
}
}, number = solutions[0])
NumberButton(onClick = {
if (correct == 1) {
solved = true
onPass()
} else {
onFail()
}
}, number = solutions[1])
NumberButton(onClick = {
if (correct == 2) {
solved = true
onPass()
} else {
onFail()
}
}, number = solutions[2])
}
}
}
@Composable
fun MathProblemLabel(n1: Int, n2: Int, sign: String) {
Text("$n1 $sign $n2 =")
}
@Composable
fun NumberButton(onClick: () -> Unit, number: Int) {
Button(
modifier = Modifier
.clip(CircleShape)
.size(30.dp),
contentPadding = PaddingValues(0.dp),
onClick = onClick
) {
Text(number.toString())
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB