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 // 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", "VASTAI_KEY", "\"\"")
buildConfigField("String", "VASIAI_API_ENDPOINT", "\"https://cloud.vast.ai/api/v0\"") buildConfigField("String", "VASIAI_API_ENDPOINT", "\"https://cloud.vast.ai/api/v0\"")
} }

View file

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

View file

@ -16,7 +16,6 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
@ -44,14 +40,12 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -69,7 +63,6 @@ import eu.m724.vastapp.vastai.data.User
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.chromium.net.CronetEngine import org.chromium.net.CronetEngine
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.random.Random
class LoginActivity : ComponentActivity() { class LoginActivity : ComponentActivity() {
private lateinit var dashboardLauncher: ActivityResultLauncher<Intent> private lateinit var dashboardLauncher: ActivityResultLauncher<Intent>
@ -99,7 +92,8 @@ class LoginActivity : ComponentActivity() {
} }
} }
loginViewModel.loadKey() if (BuildConfig.AUTO_LOGIN)
loginViewModel.loadKey()
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
@ -113,7 +107,13 @@ class LoginActivity : ComponentActivity() {
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally 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) { fun LoginApp(loginViewModel: LoginViewModel) {
val uiState by loginViewModel.uiState.collectAsState() val uiState by loginViewModel.uiState.collectAsState()
val loginErrorMessage by loginViewModel.error.observeAsState() // TODO put this in uiState 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 } } val isIdle by remember(uiState) { derivedStateOf { uiState !is LoginUiState.Loading } }
var apiKey by rememberSaveable { mutableStateOf(BuildConfig.VASTAI_KEY) }
var advancedOpen by rememberSaveable { mutableStateOf(false) } 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( Column(
modifier = Modifier modifier = Modifier
.width(300.dp) .width(300.dp)
@ -153,38 +147,25 @@ fun LoginApp(loginViewModel: LoginViewModel) {
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
TextField( KeyTextField(
modifier = Modifier.fillMaxWidth(),
enabled = isIdle, enabled = isIdle,
value = apiKey, apiKey = apiKey,
onValueChange = { apiKey = it }, onValueChange = { loginViewModel.onApiKeyChange(it) },
label = { Text(text = stringResource(id = R.string.api_key)) }, rainbowText = uiState is LoginUiState.Success,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
textStyle = if (uiState is LoginUiState.Success) rainbowTextStyle() else LocalTextStyle.current,
singleLine = true,
isError = loginErrorMessage != null isError = loginErrorMessage != null
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Row { Row {
TextButton( AdvancedOptionsButton(
enabled = isIdle, enabled = isIdle,
onClick = { onClick = { advancedOpen = !advancedOpen },
advancedOpen = !advancedOpen isOpen = 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)) Spacer(modifier = Modifier.weight(1f))
Button( Button(
enabled = isIdle, enabled = isIdle,
onClick = { onClick = {
loginViewModel.tryLogin(apiKey) loginViewModel.tryLogin()
} }
) { ) {
if (uiState is LoginUiState.Loading) { if (uiState is LoginUiState.Loading) {
@ -194,12 +175,62 @@ fun LoginApp(loginViewModel: LoginViewModel) {
} }
} }
} }
AnimatedVisibility(visible = advancedOpen) { AnimatedVisibility(visible = advancedOpen) {
AdvancedOptions() 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 @Composable
fun rainbowTextStyle(): TextStyle { fun rainbowTextStyle(): TextStyle {
return LocalTextStyle.current.copy(brush = Brush.linearGradient(colors = listOf( return LocalTextStyle.current.copy(brush = Brush.linearGradient(colors = listOf(
@ -214,119 +245,6 @@ fun rainbowTextStyle(): TextStyle {
} }
@Composable @Composable
fun AdvancedOptions() { // TODO put this in viewmodel fun AdvancedOptions() {
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

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