super ultra mega refactoring
- now extending Application - using that Application to store data like user or instances - completed loading screen * made loading seamless - and made other stuff work with that
This commit is contained in:
parent
9574ed496d
commit
ae4912fd29
19 changed files with 395 additions and 106 deletions
|
@ -14,7 +14,19 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Vastapp"
|
android:theme="@style/Theme.Vastapp"
|
||||||
|
android:name=".VastApplication"
|
||||||
tools:targetApi="34">
|
tools:targetApi="34">
|
||||||
|
<activity
|
||||||
|
android:name=".activity.dashboard.loading.LoadingActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/title_activity_loading"
|
||||||
|
android:theme="@style/Theme.Vastapp">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.termux.TermuxSshActivity"
|
android:name=".activity.termux.TermuxSshActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
@ -30,11 +42,6 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/title_activity_login"
|
android:label="@string/title_activity_login"
|
||||||
android:theme="@style/Theme.Vastapp">
|
android:theme="@style/Theme.Vastapp">
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
38
app/src/main/java/eu/m724/vastapp/VastApplication.kt
Normal file
38
app/src/main/java/eu/m724/vastapp/VastApplication.kt
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package eu.m724.vastapp
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import eu.m724.vastapp.vastai.Account
|
||||||
|
import eu.m724.vastapp.vastai.VastApi
|
||||||
|
import org.chromium.net.CronetEngine
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class VastApplication : Application() {
|
||||||
|
private lateinit var cronetEngine: CronetEngine
|
||||||
|
lateinit var vastApi: VastApi // TODO maybe make private?
|
||||||
|
|
||||||
|
private lateinit var loginSharedPreferences: SharedPreferences
|
||||||
|
|
||||||
|
var account: Account? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
cronetEngine = CronetEngine.Builder(baseContext)
|
||||||
|
.enableBrotli(true)
|
||||||
|
.build() // http3 is not supported on cloud.vast.ai
|
||||||
|
vastApi = VastApi("", cronetEngine, Executors.newSingleThreadExecutor())
|
||||||
|
loginSharedPreferences = applicationContext.getSharedPreferences("login", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitKey(apiKey: String) {
|
||||||
|
with (loginSharedPreferences.edit()) {
|
||||||
|
putString("apiKey", apiKey) // TODO encrypt
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadKey(): String? {
|
||||||
|
return loginSharedPreferences.getString("apiKey", null)
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,26 +36,18 @@ import eu.m724.vastapp.activity.dashboard.screen.HelpScreen
|
||||||
import eu.m724.vastapp.activity.dashboard.screen.InstancesScreen
|
import eu.m724.vastapp.activity.dashboard.screen.InstancesScreen
|
||||||
import eu.m724.vastapp.activity.dashboard.screen.Screen
|
import eu.m724.vastapp.activity.dashboard.screen.Screen
|
||||||
import eu.m724.vastapp.ui.theme.VastappTheme
|
import eu.m724.vastapp.ui.theme.VastappTheme
|
||||||
import eu.m724.vastapp.vastai.VastApi
|
|
||||||
import eu.m724.vastapp.vastai.data.User
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.chromium.net.CronetEngine
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class DashboardActivity : ComponentActivity() {
|
class DashboardActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val user = intent.getParcelableExtra<User>("user")!! // TODO null check
|
val dashboardViewModel = DashboardViewModel(application)
|
||||||
|
if (intent.getBooleanExtra("direct", false).not()) {
|
||||||
val executor = Executors.newSingleThreadExecutor()
|
dashboardViewModel.refresh(this)
|
||||||
val cronetEngine = CronetEngine.Builder(baseContext).enableBrotli(true).build()
|
}
|
||||||
val vastApi = VastApi(user.apiKey, cronetEngine, executor) // TODO use that from login activity
|
|
||||||
|
|
||||||
val dashboardViewModel = DashboardViewModel(user, vastApi)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
dashboardViewModel.refresh(this@DashboardActivity)
|
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
dashboardViewModel.refreshError.collect {
|
dashboardViewModel.refreshError.collect {
|
||||||
it.forEach { errorMsg ->
|
it.forEach { errorMsg ->
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
package eu.m724.vastapp.activity.dashboard
|
package eu.m724.vastapp.activity.dashboard
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import eu.m724.vastapp.R
|
import eu.m724.vastapp.R
|
||||||
|
import eu.m724.vastapp.VastApplication
|
||||||
import eu.m724.vastapp.activity.Opener
|
import eu.m724.vastapp.activity.Opener
|
||||||
import eu.m724.vastapp.activity.PermissionChecker
|
import eu.m724.vastapp.activity.PermissionChecker
|
||||||
import eu.m724.vastapp.vastai.ApiRoute
|
import eu.m724.vastapp.vastai.ApiRoute
|
||||||
import eu.m724.vastapp.vastai.VastApi
|
|
||||||
import eu.m724.vastapp.vastai.api.InstancesUrlRequestCallback
|
import eu.m724.vastapp.vastai.api.InstancesUrlRequestCallback
|
||||||
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
|
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
|
||||||
import eu.m724.vastapp.vastai.data.RentedInstance
|
import eu.m724.vastapp.vastai.data.RentedInstance
|
||||||
import eu.m724.vastapp.vastai.data.User
|
|
||||||
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
|
||||||
|
@ -21,30 +21,24 @@ import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
|
||||||
class DashboardViewModel(
|
class DashboardViewModel(
|
||||||
initialUser: User,
|
application: Application
|
||||||
private val vastApi: VastApi
|
) : AndroidViewModel(application) { // TODO do something with the user
|
||||||
) : ViewModel() { // TODO do something with the user
|
|
||||||
|
|
||||||
private val _uiState: MutableStateFlow<DashboardUiState> =
|
private val _uiState: MutableStateFlow<DashboardUiState> =
|
||||||
MutableStateFlow(DashboardUiState(0))
|
MutableStateFlow(DashboardUiState(0))
|
||||||
val uiState: StateFlow<DashboardUiState> =
|
val uiState: StateFlow<DashboardUiState> =
|
||||||
_uiState.asStateFlow()
|
_uiState.asStateFlow()
|
||||||
|
|
||||||
private val _rentedInstances: MutableStateFlow<List<RentedInstance>> = MutableStateFlow(emptyList())
|
|
||||||
val rentedInstances: StateFlow<List<RentedInstance>> = _rentedInstances.asStateFlow()
|
|
||||||
|
|
||||||
private val _user: MutableStateFlow<User> = MutableStateFlow(initialUser)
|
|
||||||
val user: StateFlow<User> = _user.asStateFlow()
|
|
||||||
|
|
||||||
private val _remainingTime: MutableStateFlow<Int> = MutableStateFlow(-1)
|
|
||||||
var remainingTime: StateFlow<Int> = _remainingTime.asStateFlow()
|
|
||||||
|
|
||||||
private val _refreshError: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
|
private val _refreshError: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||||
val refreshError: StateFlow<List<String>> = _refreshError.asStateFlow()
|
val refreshError: StateFlow<List<String>> = _refreshError.asStateFlow()
|
||||||
|
|
||||||
private val _termuxAvailable: MutableStateFlow<Int> = MutableStateFlow(0)
|
private val _termuxAvailable: MutableStateFlow<Int> = MutableStateFlow(0)
|
||||||
val termuxAvailable: StateFlow<Int> = _termuxAvailable.asStateFlow()
|
val termuxAvailable: StateFlow<Int> = _termuxAvailable.asStateFlow()
|
||||||
|
|
||||||
|
private val application = getApplication<VastApplication>()
|
||||||
|
private val vastApi = this.application.vastApi
|
||||||
|
val account = this.application.account!!
|
||||||
|
|
||||||
fun refresh(activity: ComponentActivity) {
|
fun refresh(activity: ComponentActivity) {
|
||||||
_uiState.value = _uiState.value.copy(refreshing = 2)
|
_uiState.value = _uiState.value.copy(refreshing = 2)
|
||||||
_refreshError.value = emptyList()
|
_refreshError.value = emptyList()
|
||||||
|
@ -52,7 +46,7 @@ class DashboardViewModel(
|
||||||
val userRequest = vastApi.buildRequest(
|
val userRequest = vastApi.buildRequest(
|
||||||
ApiRoute.SHOW_USER,
|
ApiRoute.SHOW_USER,
|
||||||
UserUrlRequestCallback({ newUser ->
|
UserUrlRequestCallback({ newUser ->
|
||||||
_user.value = newUser
|
account.updateUser(newUser)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(refreshing = it.refreshing - 1) // TODO I don't like how this looks
|
it.copy(refreshing = it.refreshing - 1) // TODO I don't like how this looks
|
||||||
}
|
}
|
||||||
|
@ -67,33 +61,17 @@ class DashboardViewModel(
|
||||||
val instancesRequest = vastApi.buildRequest(
|
val instancesRequest = vastApi.buildRequest(
|
||||||
ApiRoute.GET_INSTANCES,
|
ApiRoute.GET_INSTANCES,
|
||||||
InstancesUrlRequestCallback({ instances ->
|
InstancesUrlRequestCallback({ instances ->
|
||||||
_rentedInstances.value = instances // TODO better way?
|
account.updateRentedInstances(instances)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(refreshing = it.refreshing - 1)
|
it.copy(refreshing = it.refreshing - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instances.isEmpty()) { // TODO move this
|
|
||||||
_remainingTime.value = -1
|
|
||||||
} else {
|
|
||||||
var totalDph = 0.0
|
|
||||||
|
|
||||||
instances.forEach {
|
|
||||||
if (it.status == "running")
|
|
||||||
totalDph += it.instance.pricing.dphTotal!!
|
|
||||||
else
|
|
||||||
totalDph += it.instance.pricing.dphTotal!! - it.instance.pricing.dphBase
|
|
||||||
// TODO make this ideal
|
|
||||||
}
|
|
||||||
|
|
||||||
_remainingTime.value = (user.value.credit / totalDph * 3600).toInt()
|
|
||||||
}
|
|
||||||
}, { apiFailure ->
|
}, { apiFailure ->
|
||||||
_refreshError.update { it + apiFailure.errorMessage!! }
|
_refreshError.update { it + apiFailure.errorMessage!! }
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(refreshing = it.refreshing - 1)
|
it.copy(refreshing = it.refreshing - 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
) // TODO move all that refreshing to some shared place
|
||||||
|
|
||||||
userRequest.start()
|
userRequest.start()
|
||||||
instancesRequest.start()
|
instancesRequest.start()
|
||||||
|
@ -111,7 +89,7 @@ class DashboardViewModel(
|
||||||
}
|
}
|
||||||
} else -1 // not available
|
} else -1 // not available
|
||||||
|
|
||||||
// TODO I don't like this function especially the last line
|
// TODO I don't like this function especially the last line. I think it should be moved to application
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SdCardPath")
|
@SuppressLint("SdCardPath")
|
||||||
|
@ -123,7 +101,7 @@ class DashboardViewModel(
|
||||||
PermissionChecker.requestIfNoPermission(
|
PermissionChecker.requestIfNoPermission(
|
||||||
"com.termux.permission.RUN_COMMAND",
|
"com.termux.permission.RUN_COMMAND",
|
||||||
activity, 0
|
activity, 0
|
||||||
) { granted, asked ->
|
) { granted, _ ->
|
||||||
if (granted) {
|
if (granted) {
|
||||||
val arguments = arrayOf(
|
val arguments = arrayOf(
|
||||||
"/data/data/com.termux/files/usr/bin/ssh",
|
"/data/data/com.termux/files/usr/bin/ssh",
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package eu.m724.vastapp.activity.dashboard.loading
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import eu.m724.vastapp.R
|
||||||
|
import eu.m724.vastapp.activity.dashboard.DashboardActivity
|
||||||
|
import eu.m724.vastapp.activity.dashboard.loading.ui.theme.VastappTheme
|
||||||
|
import eu.m724.vastapp.activity.login.LoginActivity
|
||||||
|
|
||||||
|
class LoadingActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val viewModel = LoadingViewModel(application) { result ->
|
||||||
|
println(result.loggedIn)
|
||||||
|
val intent =
|
||||||
|
if (result.loggedIn) {
|
||||||
|
Intent(this, DashboardActivity::class.java)
|
||||||
|
.putExtra("direct", true)
|
||||||
|
} else {
|
||||||
|
Intent(this, LoginActivity::class.java)
|
||||||
|
.putExtra("error", result.error!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.init()
|
||||||
|
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
VastappTheme {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||||
|
contentDescription = "app icon"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package eu.m724.vastapp.activity.dashboard.loading
|
||||||
|
|
||||||
|
data class LoadingResult(
|
||||||
|
val loggedIn: Boolean,
|
||||||
|
val error: String?
|
||||||
|
)
|
|
@ -0,0 +1,54 @@
|
||||||
|
package eu.m724.vastapp.activity.dashboard.loading
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import eu.m724.vastapp.VastApplication
|
||||||
|
import eu.m724.vastapp.vastai.Account
|
||||||
|
import eu.m724.vastapp.vastai.ApiRoute
|
||||||
|
import eu.m724.vastapp.vastai.api.InstancesUrlRequestCallback
|
||||||
|
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
|
||||||
|
|
||||||
|
class LoadingViewModel(
|
||||||
|
application: Application,
|
||||||
|
private val onEnded: (LoadingResult) -> Unit,
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
|
private val application = application as VastApplication
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
val vastApi = application.vastApi
|
||||||
|
val apiKey = application.loadKey()
|
||||||
|
|
||||||
|
if (apiKey != null) {
|
||||||
|
vastApi.apiKey = apiKey
|
||||||
|
|
||||||
|
val request = vastApi.buildRequest(
|
||||||
|
ApiRoute.SHOW_USER,
|
||||||
|
UserUrlRequestCallback({ user ->
|
||||||
|
application.account = Account(user)
|
||||||
|
loadInstances()
|
||||||
|
}, { apiFailure ->
|
||||||
|
onEnded(LoadingResult(false, apiFailure.errorMessage))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
request.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadInstances() {
|
||||||
|
val vastApi = application.vastApi
|
||||||
|
|
||||||
|
val instancesRequest = vastApi.buildRequest(
|
||||||
|
ApiRoute.GET_INSTANCES,
|
||||||
|
InstancesUrlRequestCallback({ instances ->
|
||||||
|
application.account!!.updateRentedInstances(instances)
|
||||||
|
onEnded(LoadingResult(true, null))
|
||||||
|
}, { apiFailure ->
|
||||||
|
// TODO I don't know what to do yet
|
||||||
|
onEnded(LoadingResult(true, null))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
instancesRequest.start()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package eu.m724.vastapp.activity.dashboard.loading.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val Purple80 = Color(0xFFD0BCFF)
|
||||||
|
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
|
val Pink80 = Color(0xFFEFB8C8)
|
||||||
|
|
||||||
|
val Purple40 = Color(0xFF6650a4)
|
||||||
|
val PurpleGrey40 = Color(0xFF625b71)
|
||||||
|
val Pink40 = Color(0xFF7D5260)
|
|
@ -0,0 +1,58 @@
|
||||||
|
package eu.m724.vastapp.activity.dashboard.loading.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = Purple80,
|
||||||
|
secondary = PurpleGrey80,
|
||||||
|
tertiary = Pink80
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LightColorScheme = lightColorScheme(
|
||||||
|
primary = Purple40,
|
||||||
|
secondary = PurpleGrey40,
|
||||||
|
tertiary = Pink40
|
||||||
|
|
||||||
|
/* Other default colors to override
|
||||||
|
background = Color(0xFFFFFBFE),
|
||||||
|
surface = Color(0xFFFFFBFE),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.White,
|
||||||
|
onTertiary = Color.White,
|
||||||
|
onBackground = Color(0xFF1C1B1F),
|
||||||
|
onSurface = Color(0xFF1C1B1F),
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VastappTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
// Dynamic color is available on Android 12+
|
||||||
|
dynamicColor: Boolean = true,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
darkTheme -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package eu.m724.vastapp.activity.dashboard.loading.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
// Set of Material typography styles to start with
|
||||||
|
val Typography = Typography(
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
/* Other default text styles to override
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
|
@ -29,7 +29,7 @@ import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||||
fun BillingScreen(dashboardViewModel: DashboardViewModel) {
|
fun BillingScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
val uiState by dashboardViewModel.uiState.collectAsState()
|
val uiState by dashboardViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val user by dashboardViewModel.user.collectAsState()
|
val user by dashboardViewModel.account.user.collectAsState()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
|
@ -44,9 +44,9 @@ fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val uiState by dashboardViewModel.uiState.collectAsState()
|
val uiState by dashboardViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val user by dashboardViewModel.user.collectAsState()
|
val user by dashboardViewModel.account.user.collectAsState()
|
||||||
val rentedInstances by dashboardViewModel.rentedInstances.collectAsState()
|
val rentedInstances by dashboardViewModel.account.rentedInstances.collectAsState()
|
||||||
val remainingTime by dashboardViewModel.remainingTime.collectAsState()
|
val remainingTime by dashboardViewModel.account.remainingTime.collectAsState()
|
||||||
val isRefreshing by remember(uiState) { derivedStateOf { uiState.refreshing > 0 } }
|
val isRefreshing by remember(uiState) { derivedStateOf { uiState.refreshing > 0 } }
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
|
@ -42,7 +42,7 @@ fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
val activity = LocalContext.current as ComponentActivity
|
val activity = LocalContext.current as ComponentActivity
|
||||||
|
|
||||||
val uiState by dashboardViewModel.uiState.collectAsState()
|
val uiState by dashboardViewModel.uiState.collectAsState()
|
||||||
val rentedInstances by dashboardViewModel.rentedInstances.collectAsState()
|
val rentedInstances by dashboardViewModel.account.rentedInstances.collectAsState()
|
||||||
val termuxAvailable by dashboardViewModel.termuxAvailable.collectAsState()
|
val termuxAvailable by dashboardViewModel.termuxAvailable.collectAsState()
|
||||||
|
|
||||||
// TODO actually get instances
|
// TODO actually get instances
|
||||||
|
|
|
@ -55,7 +55,6 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import eu.m724.vastapp.BuildConfig
|
|
||||||
import eu.m724.vastapp.R
|
import eu.m724.vastapp.R
|
||||||
import eu.m724.vastapp.activity.dashboard.DashboardActivity
|
import eu.m724.vastapp.activity.dashboard.DashboardActivity
|
||||||
import eu.m724.vastapp.ui.theme.VastappTheme
|
import eu.m724.vastapp.ui.theme.VastappTheme
|
||||||
|
@ -84,6 +83,11 @@ class LoginActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val loadingError = intent.getStringExtra("error")
|
||||||
|
if (loadingError != null) {
|
||||||
|
Toast.makeText(baseContext, loadingError, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launch { // TODO I was suggested not to launch an activity from a lifecycle scope
|
lifecycleScope.launch { // TODO I was suggested not to launch an activity from a lifecycle scope
|
||||||
loginViewModel.uiState.collect { state ->
|
loginViewModel.uiState.collect { state ->
|
||||||
if (state is LoginUiState.Success) {
|
if (state is LoginUiState.Success) {
|
||||||
|
@ -92,9 +96,6 @@ class LoginActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.AUTO_LOGIN)
|
|
||||||
loginViewModel.loadKey()
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
VastappTheme {
|
VastappTheme {
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package eu.m724.vastapp.activity.login
|
package eu.m724.vastapp.activity.login
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import eu.m724.vastapp.VastApplication
|
||||||
import eu.m724.vastapp.vastai.ApiRoute
|
import eu.m724.vastapp.vastai.ApiRoute
|
||||||
import eu.m724.vastapp.vastai.VastApi
|
|
||||||
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
|
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
|
||||||
|
@ -34,35 +33,16 @@ class LoginViewModel(
|
||||||
private val _fullscreenLoading = MutableStateFlow<Boolean>(false)
|
private val _fullscreenLoading = MutableStateFlow<Boolean>(false)
|
||||||
var fullscreenLoading: StateFlow<Boolean> = _fullscreenLoading.asStateFlow()
|
var fullscreenLoading: StateFlow<Boolean> = _fullscreenLoading.asStateFlow()
|
||||||
|
|
||||||
private val applicationContext = getApplication<Application>().applicationContext
|
private val application = getApplication<VastApplication>()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun tryLogin() {
|
fun tryLogin() {
|
||||||
val apiKey = apiKey.value
|
val vastApi = application.vastApi
|
||||||
|
vastApi.apiKey = apiKey.value
|
||||||
|
|
||||||
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
|
application.submitKey(apiKey.value) // TODO toggle for this
|
||||||
_uiState.value = LoginUiState.Success(user)
|
_uiState.value = LoginUiState.Success(user)
|
||||||
}, { apiFailure ->
|
}, { apiFailure ->
|
||||||
_uiState.value = LoginUiState.Idle
|
_uiState.value = LoginUiState.Idle
|
||||||
|
|
49
app/src/main/java/eu/m724/vastapp/vastai/Account.kt
Normal file
49
app/src/main/java/eu/m724/vastapp/vastai/Account.kt
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package eu.m724.vastapp.vastai
|
||||||
|
|
||||||
|
import eu.m724.vastapp.vastai.data.RentedInstance
|
||||||
|
import eu.m724.vastapp.vastai.data.User
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
class Account(
|
||||||
|
initialUser: User
|
||||||
|
) {
|
||||||
|
private val _rentedInstances: MutableStateFlow<List<RentedInstance>> = MutableStateFlow(emptyList())
|
||||||
|
val rentedInstances: StateFlow<List<RentedInstance>> = _rentedInstances.asStateFlow()
|
||||||
|
|
||||||
|
private val _user: MutableStateFlow<User> = MutableStateFlow(initialUser)
|
||||||
|
val user: StateFlow<User> = _user.asStateFlow()
|
||||||
|
|
||||||
|
private val _remainingTime: MutableStateFlow<Int> = MutableStateFlow(-1)
|
||||||
|
var remainingTime: StateFlow<Int> = _remainingTime.asStateFlow()
|
||||||
|
|
||||||
|
fun updateUser(user: User) {
|
||||||
|
_user.value = user
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateRentedInstances(rentedInstances: List<RentedInstance>) { // TODO better way?
|
||||||
|
_rentedInstances.value = rentedInstances
|
||||||
|
calculateRemainingTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun calculateRemainingTime() {
|
||||||
|
val rentedInstances = rentedInstances.value
|
||||||
|
|
||||||
|
if (rentedInstances.isEmpty()) {
|
||||||
|
_remainingTime.value = -1
|
||||||
|
} else {
|
||||||
|
var totalDph = 0.0
|
||||||
|
|
||||||
|
rentedInstances.forEach {
|
||||||
|
if (it.status == "running")
|
||||||
|
totalDph += it.instance.pricing.dphTotal!!
|
||||||
|
else
|
||||||
|
totalDph += it.instance.pricing.dphTotal!! - it.instance.pricing.dphBase
|
||||||
|
// TODO make this ideal
|
||||||
|
}
|
||||||
|
|
||||||
|
_remainingTime.value = (user.value.credit / totalDph * 3600).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,14 @@
|
||||||
package eu.m724.vastapp.vastai
|
package eu.m724.vastapp.vastai
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import eu.m724.vastapp.BuildConfig
|
import eu.m724.vastapp.BuildConfig
|
||||||
import org.chromium.net.CronetEngine
|
import org.chromium.net.CronetEngine
|
||||||
import org.chromium.net.UrlRequest
|
import org.chromium.net.UrlRequest
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
class VastApi(
|
class VastApi(
|
||||||
val apiKey: String,
|
var apiKey: String, // TODO make private?
|
||||||
val cronetEngine: CronetEngine,
|
private val cronetEngine: CronetEngine,
|
||||||
val executor: Executor
|
private val executor: Executor
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* build an api request
|
* build an api request
|
||||||
|
|
|
@ -43,8 +43,10 @@ class UserUrlRequestCallback(
|
||||||
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
|
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
|
||||||
println(stringResponse) // TODO don't do that
|
println(stringResponse) // TODO don't do that
|
||||||
if (info?.httpStatusCode == 200) {
|
if (info?.httpStatusCode == 200) {
|
||||||
|
try {
|
||||||
val jsonResponse = JSONObject(stringResponse.toString())
|
val jsonResponse = JSONObject(stringResponse.toString())
|
||||||
onSuccess(User(
|
onSuccess(
|
||||||
|
User(
|
||||||
id = jsonResponse.getString("id"),
|
id = jsonResponse.getString("id"),
|
||||||
username = jsonResponse.getString("username"),
|
username = jsonResponse.getString("username"),
|
||||||
email = jsonResponse.getString("email"),
|
email = jsonResponse.getString("email"),
|
||||||
|
@ -52,7 +54,12 @@ class UserUrlRequestCallback(
|
||||||
credit = jsonResponse.getDouble("credit"),
|
credit = jsonResponse.getDouble("credit"),
|
||||||
balanceThreshold = jsonResponse.getDouble("balance_threshold"),
|
balanceThreshold = jsonResponse.getDouble("balance_threshold"),
|
||||||
balanceThresholdEnabled = jsonResponse.getBoolean("balance_threshold_enabled"),
|
balanceThresholdEnabled = jsonResponse.getBoolean("balance_threshold_enabled"),
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onFailure(ApiFailure(e.message))
|
||||||
|
println("API response error: $stringResponse")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onFailure(ApiFailure("${info?.httpStatusCode} ${info?.httpStatusText}"))
|
onFailure(ApiFailure("${info?.httpStatusCode} ${info?.httpStatusText}"))
|
||||||
println("API error: $stringResponse")
|
println("API error: $stringResponse")
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">vast.app</string>
|
<string name="app_name" translatable="false">vast.app</string>
|
||||||
<string name="title_activity_dashboard">Dashboard</string>
|
<string name="title_activity_dashboard">Dashboard</string>
|
||||||
<string name="title_activity_login" translatable="false">vast.app</string>
|
<string name="title_activity_login">Login</string>
|
||||||
|
<string name="title_activity_loading" translatable="false">vast.app</string>
|
||||||
<string name="nav_dashboard">Dashboard</string>
|
<string name="nav_dashboard">Dashboard</string>
|
||||||
<string name="nav_billing">Billing</string>
|
<string name="nav_billing">Billing</string>
|
||||||
<string name="nav_instances">Instances</string>
|
<string name="nav_instances">Instances</string>
|
||||||
|
@ -21,14 +22,13 @@
|
||||||
<string name="login_checkbox_angry">checkbox is angry</string>
|
<string name="login_checkbox_angry">checkbox is angry</string>
|
||||||
<string name="no_options">none yet sorry</string>
|
<string name="no_options">none yet sorry</string>
|
||||||
<string name="command_permission_denied">If you change your mind, do so from settings</string>
|
<string name="command_permission_denied">If you change your mind, do so from settings</string>
|
||||||
<string name="no_termux">Termuxn\'t</string>
|
<string name="title_activity_termux_ssh">Termux Error</string>
|
||||||
<string name="title_activity_termux_ssh">TermuxSshActivity</string>
|
|
||||||
<string name="termux_no_ssh">No ssh client on termux, install dropbear or openssh package</string>
|
<string name="termux_no_ssh">No ssh client on termux, install dropbear or openssh package</string>
|
||||||
<string name="copied_to_clipboard">Copied command to clipboard</string>
|
<string name="copied_to_clipboard">Copied command to clipboard</string>
|
||||||
<string name="termux_install_dropbear">Install Dropbear with:</string>
|
<string name="termux_install_dropbear">Install Dropbear with:</string>
|
||||||
<string name="open_termux">Open Termux</string>
|
<string name="open_termux">Open Termux</string>
|
||||||
<string name="termux_not_configured">Termux is not configured for usage with other apps.</string>
|
<string name="termux_not_configured">Termux is not configured for usage with other apps.</string>
|
||||||
<string name="termux_open_instructions">Open instructions on github.com</string>
|
<string name="termux_open_instructions">Open instructions on github.com</string>
|
||||||
<string name="termux_error">An error occured:</string>
|
<string name="termux_error">An error occurred:</string>
|
||||||
<string name="webview_todo">(this will be a webview)</string>
|
<string name="webview_todo">(this will be a webview)</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue