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 // 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,6 +12,7 @@
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,6 +16,7 @@ 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
@ -23,11 +24,14 @@ 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
@ -40,12 +44,14 @@ 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
@ -63,6 +69,7 @@ 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>
@ -92,8 +99,7 @@ class LoginActivity : ComponentActivity() {
} }
} }
if (BuildConfig.AUTO_LOGIN) loginViewModel.loadKey()
loginViewModel.loadKey()
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
@ -107,13 +113,7 @@ class LoginActivity : ComponentActivity() {
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
val loading by loginViewModel.fullscreenLoading.collectAsState() LoginApp(loginViewModel)
if (loading) {
CircularProgressIndicator()
} else {
LoginApp(loginViewModel)
}
} }
} }
} }
@ -135,11 +135,17 @@ 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)
@ -147,25 +153,38 @@ fun LoginApp(loginViewModel: LoginViewModel) {
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
KeyTextField( TextField(
modifier = Modifier.fillMaxWidth(),
enabled = isIdle, enabled = isIdle,
apiKey = apiKey, value = apiKey,
onValueChange = { loginViewModel.onApiKeyChange(it) }, onValueChange = { apiKey = it },
rainbowText = uiState is LoginUiState.Success, 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 isError = loginErrorMessage != null
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Row { Row {
AdvancedOptionsButton( TextButton(
enabled = isIdle, enabled = isIdle,
onClick = { advancedOpen = !advancedOpen }, onClick = {
isOpen = advancedOpen 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)) Spacer(modifier = Modifier.weight(1f))
Button( Button(
enabled = isIdle, enabled = isIdle,
onClick = { onClick = {
loginViewModel.tryLogin() loginViewModel.tryLogin(apiKey)
} }
) { ) {
if (uiState is LoginUiState.Loading) { if (uiState is LoginUiState.Loading) {
@ -175,62 +194,12 @@ 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(
@ -245,6 +214,119 @@ fun rainbowTextStyle(): TextStyle {
} }
@Composable @Composable
fun AdvancedOptions() { fun AdvancedOptions() { // TODO put this in viewmodel
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,7 +11,6 @@ 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
@ -26,47 +25,31 @@ class LoginViewModel(
_uiState.asStateFlow() _uiState.asStateFlow()
private val _error = MutableLiveData<String?>(null) private val _error = MutableLiveData<String?>(null)
val error: LiveData<String?> = _error val error: LiveData<String?> = _error // TODO put this in uistate
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) {
_apiKey.value = apiKey tryLogin(apiKey)
_fullscreenLoading.value = true
tryLogin()
} }
} }
fun tryLogin() { fun tryLogin(apiKey: String) {
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 ->
saveKey() // TODO toggle for this with (sharedPreferences.edit()) {
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)
}) })
) )
@ -74,8 +57,4 @@ class LoginViewModel(
_uiState.value = LoginUiState.Loading _uiState.value = LoginUiState.Loading
request.start() 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