Compare commits

...

4 commits

Author SHA1 Message Date
2a61d22de9
add themed icon and remove unnecessary icon files 2024-08-04 10:09:09 +02:00
18dea0beb1
make loading seamless 2024-08-04 09:36:07 +02:00
5b27c8783e
login refactoring 2024-08-04 09:25:31 +02:00
fff16ca0fb
move fun stuff in login activity to another file
no clutter yano amirite
2024-08-04 08:47:33 +02:00
16 changed files with 245 additions and 167 deletions

View file

@ -21,6 +21,7 @@ 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,7 +12,6 @@
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,7 +16,6 @@ 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
@ -24,14 +23,11 @@ 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
@ -44,14 +40,12 @@ 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
@ -69,7 +63,6 @@ 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>
@ -99,7 +92,8 @@ class LoginActivity : ComponentActivity() {
}
}
loginViewModel.loadKey()
if (BuildConfig.AUTO_LOGIN)
loginViewModel.loadKey()
enableEdgeToEdge()
setContent {
@ -113,7 +107,13 @@ class LoginActivity : ComponentActivity() {
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LoginApp(loginViewModel)
val loading by loginViewModel.fullscreenLoading.collectAsState()
if (loading) {
CircularProgressIndicator()
} else {
LoginApp(loginViewModel)
}
}
}
}
@ -135,17 +135,11 @@ 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)
@ -153,38 +147,25 @@ fun LoginApp(loginViewModel: LoginViewModel) {
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier.fillMaxWidth(),
KeyTextField(
enabled = isIdle,
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,
apiKey = apiKey,
onValueChange = { loginViewModel.onApiKeyChange(it) },
rainbowText = uiState is LoginUiState.Success,
isError = loginErrorMessage != null
)
Spacer(modifier = Modifier.height(10.dp))
Row {
TextButton(
AdvancedOptionsButton(
enabled = isIdle,
onClick = {
advancedOpen = !advancedOpen
}
) {
Text(text = stringResource(id = R.string.advanced_options))
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.rotate(arrowRotation)
)
}
onClick = { advancedOpen = !advancedOpen },
isOpen = advancedOpen
)
Spacer(modifier = Modifier.weight(1f))
Button(
enabled = isIdle,
onClick = {
loginViewModel.tryLogin(apiKey)
loginViewModel.tryLogin()
}
) {
if (uiState is LoginUiState.Loading) {
@ -194,12 +175,62 @@ 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(
@ -214,119 +245,6 @@ fun rainbowTextStyle(): TextStyle {
}
@Composable
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())
}
fun AdvancedOptions() {
FunGame()
}

View file

@ -11,6 +11,7 @@ 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
@ -25,31 +26,47 @@ class LoginViewModel(
_uiState.asStateFlow()
private val _error = MutableLiveData<String?>(null)
val error: LiveData<String?> = _error // TODO put this in uistate
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()
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) {
tryLogin(apiKey)
_apiKey.value = apiKey
_fullscreenLoading.value = true
tryLogin()
}
}
fun tryLogin(apiKey: String) {
fun tryLogin() {
val apiKey = apiKey.value
val vastApi = VastApi(apiKey, cronetEngine, executor)
val request = vastApi.buildRequest(
ApiRoute.SHOW_USER,
UserUrlRequestCallback({ user ->
with (sharedPreferences.edit()) {
putString("apiKey", apiKey) // TODO encrypt
apply()
} // TODO toggle for this
saveKey() // TODO toggle for this
_uiState.value = LoginUiState.Success(user)
}, { apiFailure ->
_uiState.value = LoginUiState.Idle
_fullscreenLoading.value = false
_error.postValue(apiFailure.errorMessage)
})
)
@ -57,4 +74,8 @@ class LoginViewModel(
_uiState.value = LoginUiState.Loading
request.start()
}
fun onApiKeyChange(apiKey: String) {
_apiKey.update { apiKey }
}
}

View file

@ -0,0 +1,144 @@
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

@ -1,5 +0,0 @@
<?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.

Before

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB