Compare commits
No commits in common. "9d0401c0f6e70f250e81200e40571e1ce1453d0d" and "3ae52c6638b61a29a768b2fcbf7d840e5a49f3f2" have entirely different histories.
9d0401c0f6
...
3ae52c6638
20 changed files with 192 additions and 581 deletions
|
@ -8,19 +8,14 @@
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
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:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Vastapp"
|
android:theme="@style/Theme.Vastapp"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
tools:targetApi="34">
|
tools:targetApi="34">
|
||||||
<activity
|
|
||||||
android:name=".activity.termux.TermuxSshActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/title_activity_termux_ssh"
|
|
||||||
android:theme="@style/Theme.Vastapp" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.dashboard.DashboardActivity"
|
android:name=".activity.dashboard.DashboardActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
package eu.m724.vastapp.activity
|
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
|
|
||||||
class Opener {
|
|
||||||
companion object {
|
|
||||||
fun openUrl(url: String, activity: ComponentActivity) {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
||||||
activity.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openApp(packageName: String, activity: ComponentActivity) {
|
|
||||||
val intent = activity.packageManager.getLaunchIntentForPackage(packageName)
|
|
||||||
activity.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copyToClipboard(text: String, label: String, context: Context) {
|
|
||||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clipData = ClipData.newPlainText(label, text)
|
|
||||||
clipboardManager.setPrimaryClip(clipData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,75 +7,62 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
|
||||||
class PermissionChecker {
|
class PermissionChecker(private val context: Context) {
|
||||||
companion object {
|
/**
|
||||||
|
* check if the app has a permission
|
||||||
|
* @param permission the permission
|
||||||
|
* @return whether the app has the permission? obviously
|
||||||
|
*/
|
||||||
|
fun hasPermission(permission: String): Boolean {
|
||||||
|
return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if the app has a permission
|
* check if a permission exists or if the app is installed
|
||||||
* @param permission the permission
|
* @param permission the permission
|
||||||
* @param context application context
|
* @return if the permission exists
|
||||||
* @return whether the app has the permission? obviously
|
*/
|
||||||
*/
|
fun permissionExists(permission: String): Boolean {
|
||||||
fun hasPermission(permission: String, context: Context): Boolean {
|
try {
|
||||||
return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
|
context.packageManager.getPermissionInfo(permission, 0)
|
||||||
}
|
return true
|
||||||
|
} catch (e: NameNotFoundException) {
|
||||||
/**
|
return false
|
||||||
* check if a permission exists or if the app is installed
|
|
||||||
* @param permission the permission
|
|
||||||
* @param packageManager usually from application context
|
|
||||||
* @return if the permission exists
|
|
||||||
*/
|
|
||||||
fun permissionExists(permission: String, packageManager: PackageManager): Boolean {
|
|
||||||
try {
|
|
||||||
packageManager.getPermissionInfo(permission, 0)
|
|
||||||
return true
|
|
||||||
} catch (e: NameNotFoundException) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param permission the permission
|
|
||||||
* @param activity the activity you're calling from
|
|
||||||
* @return if the permission can be asked for, that is if the user didn't check "don't ask again"
|
|
||||||
*/
|
|
||||||
fun canAskForPermission(permission: String, activity: ComponentActivity): Boolean {
|
|
||||||
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* request a permission if that permission is not granted
|
|
||||||
* @param permission the permission
|
|
||||||
* @param activity the activity you're calling from
|
|
||||||
* @param requestCode if set, calls requestPermissions, so the callback is not called and you must handle it in the activity
|
|
||||||
* @param callback an Unit, the first boolean is whether the permission is granted and the second one is whether we asked for it
|
|
||||||
*/
|
|
||||||
fun requestIfNoPermission(permission: String, activity: ComponentActivity, requestCode: Int? = null, callback: (Boolean, Boolean) -> Unit) {
|
|
||||||
val available = canAskForPermission(permission, activity)
|
|
||||||
|
|
||||||
if (hasPermission(permission, activity.applicationContext)) {
|
|
||||||
callback(true, false)
|
|
||||||
} else if (available) { // no permission but can request
|
|
||||||
requestPermission(permission, activity, requestCode) { callback(it, true) }
|
|
||||||
} else { // no permission and can't request
|
|
||||||
callback(false, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO should this be private? I mean it doesn't check for other stuff so it's a waste to register an activity if we don't have to
|
|
||||||
private fun requestPermission(permission: String, activity: ComponentActivity, requestCode: Int? = null, callback: (Boolean) -> Unit) {
|
|
||||||
if (requestCode != null) {
|
|
||||||
activity.requestPermissions(
|
|
||||||
arrayOf(permission),
|
|
||||||
requestCode
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
activity.registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission(),
|
|
||||||
callback
|
|
||||||
).launch(permission)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param permission the permission
|
||||||
|
* @param activity the activity you're calling from
|
||||||
|
* @return if the permission can be asked for, that is if the user didn't check "don't ask again"
|
||||||
|
*/
|
||||||
|
fun canAskForPermission(permission: String, activity: ComponentActivity): Boolean {
|
||||||
|
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* request a permission if that permission is not granted
|
||||||
|
* @param permission the permission
|
||||||
|
* @param activity the activity you're calling from
|
||||||
|
* @param callback an Unit, the first boolean is whether the permission is granted and the second one is whether we asked for it
|
||||||
|
*/
|
||||||
|
fun requestIfNoPermission(permission: String, activity: ComponentActivity, callback: (Boolean, Boolean) -> Unit) {
|
||||||
|
val available = canAskForPermission(permission, activity)
|
||||||
|
|
||||||
|
if (hasPermission(permission)) {
|
||||||
|
callback(true, false)
|
||||||
|
} else if (available) { // no permission but can request
|
||||||
|
requestPermission(permission, activity) { callback(it, true) }
|
||||||
|
} else { // no permission and can't request
|
||||||
|
callback(false, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO should this be private? I mean it doesn't check for other stuff so it's a waste to register an activity if we don't have to
|
||||||
|
private fun requestPermission(permission: String, activity: ComponentActivity, callback: (Boolean) -> Unit) {
|
||||||
|
activity.registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission(),
|
||||||
|
callback
|
||||||
|
).launch(permission)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -49,13 +49,12 @@ class DashboardActivity : ComponentActivity() {
|
||||||
val user = intent.getParcelableExtra<User>("user")!! // TODO null check
|
val user = intent.getParcelableExtra<User>("user")!! // TODO null check
|
||||||
|
|
||||||
val executor = Executors.newSingleThreadExecutor()
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
val cronetEngine = CronetEngine.Builder(baseContext).enableBrotli(true).build()
|
val cronetEngine = CronetEngine.Builder(baseContext).build()
|
||||||
val vastApi = VastApi(user.apiKey, cronetEngine, executor) // TODO use that from login activity
|
val vastApi = VastApi(user.apiKey, cronetEngine, executor) // TODO use that from login activity
|
||||||
|
|
||||||
val dashboardViewModel = DashboardViewModel(user, vastApi)
|
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 ->
|
||||||
|
|
|
@ -2,4 +2,5 @@ package eu.m724.vastapp.activity.dashboard
|
||||||
|
|
||||||
data class DashboardUiState(
|
data class DashboardUiState(
|
||||||
val refreshing: Int = 0
|
val refreshing: Int = 0
|
||||||
)
|
) {
|
||||||
|
}
|
|
@ -1,22 +1,11 @@
|
||||||
package eu.m724.vastapp.activity.dashboard
|
package eu.m724.vastapp.activity.dashboard
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import eu.m724.vastapp.R
|
|
||||||
import eu.m724.vastapp.activity.Opener
|
|
||||||
import eu.m724.vastapp.activity.PermissionChecker
|
|
||||||
import eu.m724.vastapp.activity.termux.TermuxSshActivity
|
|
||||||
import eu.m724.vastapp.vastai.ApiRoute
|
import eu.m724.vastapp.vastai.ApiRoute
|
||||||
import eu.m724.vastapp.vastai.VastApi
|
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.Instance
|
||||||
import eu.m724.vastapp.vastai.data.User
|
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
|
||||||
|
@ -24,18 +13,14 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
|
||||||
class DashboardViewModel(
|
class DashboardViewModel(initialUser: User, private val vastApi: VastApi) : ViewModel() { // TODO do something with the user
|
||||||
private val initialUser: User,
|
|
||||||
private val vastApi: VastApi
|
|
||||||
) : 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())
|
private val _rentedInstances: MutableStateFlow<List<Instance>> = MutableStateFlow(emptyList())
|
||||||
val rentedInstances: StateFlow<List<RentedInstance>> = _rentedInstances.asStateFlow()
|
val rentedInstances: StateFlow<List<Instance>> = _rentedInstances.asStateFlow()
|
||||||
|
|
||||||
private val _user: MutableStateFlow<User> = MutableStateFlow(initialUser)
|
private val _user: MutableStateFlow<User> = MutableStateFlow(initialUser)
|
||||||
val user: StateFlow<User> = _user.asStateFlow()
|
val user: StateFlow<User> = _user.asStateFlow()
|
||||||
|
@ -43,10 +28,7 @@ class DashboardViewModel(
|
||||||
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)
|
fun refresh() {
|
||||||
val termuxAvailable: StateFlow<Int> = _termuxAvailable.asStateFlow()
|
|
||||||
|
|
||||||
fun refresh(activity: ComponentActivity) {
|
|
||||||
_uiState.value = _uiState.value.copy(refreshing = 2)
|
_uiState.value = _uiState.value.copy(refreshing = 2)
|
||||||
_refreshError.value = emptyList()
|
_refreshError.value = emptyList()
|
||||||
|
|
||||||
|
@ -83,77 +65,7 @@ class DashboardViewModel(
|
||||||
userRequest.start()
|
userRequest.start()
|
||||||
instancesRequest.start()
|
instancesRequest.start()
|
||||||
|
|
||||||
val context = activity.applicationContext
|
|
||||||
|
|
||||||
_termuxAvailable.value =
|
|
||||||
if (PermissionChecker.permissionExists("com.termux.permission.RUN_COMMAND", context.packageManager)) {
|
|
||||||
if (PermissionChecker.hasPermission("com.termux.permission.RUN_COMMAND", context)) {
|
|
||||||
1 // available and permitted
|
|
||||||
} else {
|
|
||||||
if (PermissionChecker.canAskForPermission("com.termux.permission.RUN_COMMAND", activity)) {
|
|
||||||
0 // available but no permission
|
|
||||||
} else -1 // not available because permission denied
|
|
||||||
}
|
|
||||||
} 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
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sshButtonClick(activity: ComponentActivity, rentedInstance: RentedInstance) {
|
|
||||||
val sshCommand = "ssh -p ${rentedInstance.sshProxyPort} root@${rentedInstance.sshProxyHost}"
|
|
||||||
val context = activity.applicationContext
|
|
||||||
|
|
||||||
if (termuxAvailable.value > -1) {
|
|
||||||
PermissionChecker.requestIfNoPermission(
|
|
||||||
"com.termux.permission.RUN_COMMAND",
|
|
||||||
activity, 0
|
|
||||||
) { granted, asked ->
|
|
||||||
if (granted) {
|
|
||||||
val arguments = arrayOf(
|
|
||||||
"-p", rentedInstance.sshProxyPort.toString(),
|
|
||||||
"root@" + rentedInstance.sshProxyHost
|
|
||||||
)
|
|
||||||
startTermux(context, arguments)
|
|
||||||
Thread.sleep(100)
|
|
||||||
println(activity.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
|
|
||||||
} else {
|
|
||||||
_termuxAvailable.value = -1
|
|
||||||
copyToClipboard(context, sshCommand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
copyToClipboard(context, sshCommand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyToClipboard(context: Context, text: String) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.copied_to_clipboard),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show() // TODO hide on a12
|
|
||||||
|
|
||||||
Opener.copyToClipboard(text, "ssh command", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SdCardPath")
|
|
||||||
private fun startTermux(context: Context, arguments: Array<String>) {
|
|
||||||
val noSshIntent = Intent(context, TermuxSshActivity::class.java)
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
noSshIntent,
|
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
val intent = Intent()
|
|
||||||
intent.setClassName("com.termux", "com.termux.app.RunCommandService")
|
|
||||||
intent.setAction("com.termux.RUN_COMMAND")
|
|
||||||
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/ssh")
|
|
||||||
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", arguments)
|
|
||||||
intent.putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingIntent)
|
|
||||||
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.m724.vastapp.activity.dashboard.screen
|
package eu.m724.vastapp.activity.dashboard.screen
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
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.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
@ -26,13 +25,13 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
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.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
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.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -43,7 +42,6 @@ import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) // for pullRefresh
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) // for pullRefresh
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
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.user.collectAsState()
|
||||||
|
@ -56,7 +54,7 @@ fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
state = rememberPullToRefreshState(),
|
state = rememberPullToRefreshState(),
|
||||||
onRefresh = { dashboardViewModel.refresh(context as ComponentActivity) }
|
onRefresh = { dashboardViewModel.refresh() }
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
@ -159,12 +157,12 @@ fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun balanceCardColor(balance: Double): Color {
|
fun balanceCardColor(balance: Double): Color {
|
||||||
return if (balance > 0) Color.Unspecified else MaterialTheme.colorScheme.errorContainer
|
return if (balance > 0) CardDefaults.cardColors().containerColor else MaterialTheme.colorScheme.errorContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun balanceColor(balance: Double, warningThreshold: Double): Color {
|
fun balanceColor(balance: Double, warningThreshold: Double): Color {
|
||||||
return if (balance > warningThreshold) Color.Unspecified else MaterialTheme.colorScheme.error
|
return if (balance > warningThreshold) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.error
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -2,7 +2,6 @@ package eu.m724.vastapp.activity.dashboard.screen
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
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.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
@ -12,15 +11,12 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.m724.vastapp.R
|
|
||||||
import eu.m724.vastapp.activity.Opener
|
|
||||||
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HelpScreen(dashboardViewModel: DashboardViewModel) { // TODO make this a webview
|
fun HelpScreen(dashboardViewModel: DashboardViewModel) { // TODO make this a webview
|
||||||
val activity = LocalContext.current as ComponentActivity
|
val context = LocalContext.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
@ -28,10 +24,11 @@ fun HelpScreen(dashboardViewModel: DashboardViewModel) { // TODO make this a web
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
Opener.openUrl("https://vast.ai/docs", activity)
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com"))
|
||||||
|
context.startActivity(browserIntent)
|
||||||
}) {
|
}) {
|
||||||
Text(text = "https://vast.ai/docs")
|
Text(text = "https://vast.ai/docs")
|
||||||
}
|
}
|
||||||
Text(text = stringResource(id = R.string.webview_todo), fontSize = 12.sp)
|
Text(text = "(this will be a webview)", fontSize = 12.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
package eu.m724.vastapp.activity.dashboard.screen
|
package eu.m724.vastapp.activity.dashboard.screen
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import android.widget.ProgressBar
|
||||||
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.ContextualFlowRow
|
import androidx.compose.foundation.layout.ContextualFlowRow
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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
|
||||||
|
@ -13,37 +14,30 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.m724.vastapp.R
|
|
||||||
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||||
import eu.m724.vastapp.vastai.data.RentedInstance
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class Instances {
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
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 termuxAvailable by dashboardViewModel.termuxAvailable.collectAsState()
|
|
||||||
|
|
||||||
// TODO actually get instances
|
// TODO actually get instances
|
||||||
|
|
||||||
|
@ -51,73 +45,93 @@ fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
itemCount = rentedInstances.size,
|
itemCount = 10,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) { i ->
|
) {
|
||||||
RentedInstanceCard(
|
val instance = JSONObject()
|
||||||
modifier = Modifier
|
instance.put("id", 234523)
|
||||||
.width(340.dp)
|
instance.put("machine_id", 1121323)
|
||||||
.padding(8.dp),
|
instance.put("host_id", 5924)
|
||||||
rentedInstance = rentedInstances[i],
|
instance.put("gpu_name", "RTX 4090")
|
||||||
termuxAvailable = termuxAvailable,
|
instance.put("num_gpus", 2)
|
||||||
sshButtonClick = {
|
instance.put("gpu_util", 70)
|
||||||
dashboardViewModel.sshButtonClick(activity, it)
|
instance.put("gpu_ram", 24564)
|
||||||
},
|
instance.put("vmem_usage", 0.339843)
|
||||||
)
|
|
||||||
|
InstanceCard(instance = instance, modifier = Modifier.width(340.dp).padding(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO maybe move this?
|
// TODO maybe move this?
|
||||||
@Composable
|
@Composable
|
||||||
fun RentedInstanceCard(
|
fun InstanceCard(instance: JSONObject, modifier: Modifier = Modifier) {
|
||||||
modifier: Modifier = Modifier,
|
val gpuUsage = instance.getInt("gpu_util")
|
||||||
rentedInstance: RentedInstance,
|
val vramGb = instance.getInt("gpu_ram") / 1000.0
|
||||||
termuxAvailable: Int,
|
val vramGbUsed = vramGb * instance.getDouble("vmem_usage")
|
||||||
sshButtonClick: (RentedInstance) -> Unit,
|
|
||||||
) {
|
|
||||||
val instance by remember(rentedInstance) { derivedStateOf { rentedInstance.instance } }
|
|
||||||
val label by remember(instance) { derivedStateOf {
|
|
||||||
rentedInstance.label ?: instance.machine.gpu.model
|
|
||||||
} }
|
|
||||||
|
|
||||||
Card(modifier = modifier) {
|
Card(modifier = modifier) {
|
||||||
Row(modifier = Modifier.padding(8.dp)) {
|
Column(
|
||||||
Text(label, fontSize = 22.sp)
|
modifier = Modifier
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
Column(
|
) {
|
||||||
horizontalAlignment = Alignment.End
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
Button( // TODO consider other buttons
|
Text(text = instance.getString("id"), fontSize = 14.sp)
|
||||||
modifier = Modifier.height(24.dp),
|
Text(text = "m:" + instance.getString("machine_id"), fontSize = 14.sp)
|
||||||
contentPadding = PaddingValues(0.dp),
|
Text(text = "h:" + instance.getString("host_id"), fontSize = 14.sp)
|
||||||
onClick = { sshButtonClick(rentedInstance) }
|
}
|
||||||
) {
|
|
||||||
if (termuxAvailable > -1) {
|
Row {
|
||||||
Icon(
|
Column {
|
||||||
painter = painterResource(id = R.drawable.termux_icon),
|
Row(
|
||||||
contentDescription = "Run in Termux"
|
modifier = Modifier.fillMaxWidth(0.5f),
|
||||||
)
|
verticalAlignment = Alignment.Bottom
|
||||||
Text("ssh")
|
) {
|
||||||
Spacer(modifier = Modifier.size(4.dp)) // necessary because TODO the termux icon has padding
|
Text(text = instance.getString("gpu_name"), fontSize = 24.sp)
|
||||||
} else {
|
if (instance.getInt("num_gpus") > 1) {
|
||||||
Spacer(modifier = Modifier.size(1.dp)) // TODO make this not needed?
|
Text(text = "x" + instance.getString("num_gpus"), modifier = Modifier.padding(start = 2.dp))
|
||||||
Icon(
|
}
|
||||||
modifier = Modifier.size(12.dp),
|
}
|
||||||
painter = painterResource(id = R.drawable.copy_regular), // TODO copy icon here
|
|
||||||
contentDescription = "Copy command"
|
Row(
|
||||||
)
|
modifier = Modifier.fillMaxWidth(0.5f)
|
||||||
Spacer(modifier = Modifier.size(6.dp))
|
) {
|
||||||
Text("ssh")
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(text = "GPU: $gpuUsage%", fontSize = 12.sp)
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { gpuUsage / 100f }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(text = "%.1f / %.1f G".format(vramGbUsed, vramGb), fontSize = 12.sp)
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { instance.getDouble("vmem_usage").toFloat() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
|
||||||
contentDescription = "Details about instance $label"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewInstanceCard() {
|
||||||
|
val instance = JSONObject()
|
||||||
|
instance.put("id", 3423941)
|
||||||
|
InstanceCard(instance = instance, modifier = Modifier.size(300.dp))
|
||||||
|
}
|
|
@ -78,6 +78,27 @@ class LoginActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// TODO move this where and run this when we need it
|
||||||
|
val permissionChecker = PermissionChecker(applicationContext)
|
||||||
|
if (!permissionChecker.permissionExists("com.termux.permission.RUN_COMMAND")) {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.no_termux,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
permissionChecker.requestIfNoPermission("com.termux.permission.RUN_COMMAND", this) { granted, asked ->
|
||||||
|
if (granted || !asked) return@requestIfNoPermission
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
getString(R.string.command_permission_denied),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
dashboardLauncher = registerForActivityResult(
|
dashboardLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
) { _ -> finish() } // TODO re-login here
|
) { _ -> finish() } // TODO re-login here
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
package eu.m724.vastapp.activity.termux
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.m724.vastapp.R
|
|
||||||
import eu.m724.vastapp.activity.Opener
|
|
||||||
import eu.m724.vastapp.activity.termux.ui.theme.VastappTheme
|
|
||||||
|
|
||||||
class TermuxSshActivity : ComponentActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val termuxResult = intent.getBundleExtra("result")
|
|
||||||
val exitCode = termuxResult!!.getInt("exitCode")
|
|
||||||
val internalErrorCode = termuxResult.getInt("err")
|
|
||||||
val stdout = termuxResult.getString("stdout", "")
|
|
||||||
|
|
||||||
var msg = stdout
|
|
||||||
|
|
||||||
if (internalErrorCode == -1) {
|
|
||||||
if (exitCode == 0) {
|
|
||||||
finish()
|
|
||||||
} // TODO handle other errors like 255 is connection refused
|
|
||||||
} else {
|
|
||||||
msg = termuxResult.getString("errmsg")
|
|
||||||
}
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContent {
|
|
||||||
VastappTheme {
|
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(innerPadding),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
if (internalErrorCode == 150)
|
|
||||||
SshNotInstalled(
|
|
||||||
onCopyButtonClick = {
|
|
||||||
Opener.copyToClipboard(it, "Termux command", this@TermuxSshActivity)
|
|
||||||
},
|
|
||||||
onOpenTermuxButton = {
|
|
||||||
Opener.openApp("com.termux", this@TermuxSshActivity)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else if (internalErrorCode == 2)
|
|
||||||
TermuxSetupGuide(
|
|
||||||
onUrlButtonClick = {
|
|
||||||
Opener.openUrl(it, this@TermuxSshActivity)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else
|
|
||||||
UnexpectedError(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UnexpectedError(msg: String) {
|
|
||||||
Text(stringResource(id = R.string.termux_error))
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(12.dp, 10.dp),
|
|
||||||
text = msg
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SshNotInstalled(onCopyButtonClick: (String) -> Unit, onOpenTermuxButton: () -> Unit) {
|
|
||||||
Text(stringResource(id = R.string.termux_no_ssh))
|
|
||||||
Text(stringResource(id = R.string.termux_install_dropbear))
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Card {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(12.dp, 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.width(24.dp + 8.dp))
|
|
||||||
Text("pkg install dropbear")
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
onClick = { onCopyButtonClick("pkg install dropbear") },
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
painter = painterResource(id = R.drawable.copy_regular),
|
|
||||||
contentDescription = "Copy command",
|
|
||||||
tint = LocalTextStyle.current.color
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
FilledTonalButton(onClick = onOpenTermuxButton) {
|
|
||||||
Text(stringResource(id = R.string.open_termux))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TermuxSetupGuide(onUrlButtonClick: (String) -> Unit) {
|
|
||||||
Text(stringResource(id = R.string.termux_not_configured))
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
FilledTonalButton(onClick = { onUrlButtonClick("https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent#Setup-Instructions") }) {
|
|
||||||
Text(stringResource(id = R.string.termux_open_instructions))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package eu.m724.vastapp.activity.termux.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)
|
|
|
@ -1,58 +0,0 @@
|
||||||
package eu.m724.vastapp.activity.termux.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
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
package eu.m724.vastapp.activity.termux.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
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
|
|
@ -1,7 +1,7 @@
|
||||||
package eu.m724.vastapp.vastai.api
|
package eu.m724.vastapp.vastai.api
|
||||||
|
|
||||||
import eu.m724.vastapp.vastai.ApiFailure
|
import eu.m724.vastapp.vastai.ApiFailure
|
||||||
import eu.m724.vastapp.vastai.data.RentedInstance
|
import eu.m724.vastapp.vastai.data.Instance
|
||||||
import org.chromium.net.CronetException
|
import org.chromium.net.CronetException
|
||||||
import org.chromium.net.UrlRequest
|
import org.chromium.net.UrlRequest
|
||||||
import org.chromium.net.UrlResponseInfo
|
import org.chromium.net.UrlResponseInfo
|
||||||
|
@ -10,7 +10,7 @@ import java.nio.ByteBuffer
|
||||||
import java.nio.charset.CodingErrorAction
|
import java.nio.charset.CodingErrorAction
|
||||||
|
|
||||||
class InstancesUrlRequestCallback(
|
class InstancesUrlRequestCallback(
|
||||||
val onSuccess: (List<RentedInstance>) -> Unit,
|
val onSuccess: (List<Instance>) -> Unit,
|
||||||
val onFailure: (ApiFailure) -> Unit
|
val onFailure: (ApiFailure) -> Unit
|
||||||
) : UrlRequest.Callback() {
|
) : UrlRequest.Callback() {
|
||||||
|
|
||||||
|
@ -43,11 +43,11 @@ class InstancesUrlRequestCallback(
|
||||||
println(stringResponse) // TODO don't do that
|
println(stringResponse) // TODO don't do that
|
||||||
if (info?.httpStatusCode == 200) {
|
if (info?.httpStatusCode == 200) {
|
||||||
val jsonResponse = JSONObject(stringResponse.toString())
|
val jsonResponse = JSONObject(stringResponse.toString())
|
||||||
val instances = ArrayList<RentedInstance>()
|
val instances = ArrayList<Instance>()
|
||||||
|
|
||||||
val instancesJson = jsonResponse.getJSONArray("instances")
|
val instancesJson = jsonResponse.getJSONArray("instances")
|
||||||
for (i in 0..<instancesJson.length()) {
|
for (i in 0..<instancesJson.length()) {
|
||||||
instances.add(RentedInstance.fromJson(instancesJson.getJSONObject(i)))
|
instances.add(Instance.fromJson(instancesJson.getJSONObject(i)))
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess(instances) // TODO handle json errors
|
onSuccess(instances) // TODO handle json errors
|
||||||
|
|
|
@ -50,17 +50,17 @@ data class RentedInstance(
|
||||||
Instance.fromJson(json),
|
Instance.fromJson(json),
|
||||||
json.getDouble("disk_space"),
|
json.getDouble("disk_space"),
|
||||||
json.getDouble("disk_util"),
|
json.getDouble("disk_util"),
|
||||||
(StorageCapacityConverters.gibToGb(json.optDouble("vmem_usage")) / 1000).toInt(),
|
(StorageCapacityConverters.gibToGb(json.getDouble("vmem_usage")) / 1000).toInt(),
|
||||||
(json.optDouble("mem_usage") / 1000).toInt(),
|
(json.getDouble("mem_usage") / 1000).toInt(),
|
||||||
json.optDouble("gpu_util") / 100,
|
json.getDouble("gpu_util") / 100,
|
||||||
json.getDouble("cpu_util") / 100,
|
json.getDouble("cpu_util") / 100,
|
||||||
json.optDouble("gpu_temp"),
|
json.getDouble("gpu_temp"),
|
||||||
json.optDouble("inet_down_billed").toInt(),
|
json.getDouble("inet_down_billed").toInt(),
|
||||||
json.optDouble("inet_up_billed").toInt(),
|
json.getDouble("inet_up_billed").toInt(),
|
||||||
"ssh${json.getInt("ssh_idx")}.vast.ai", // TODO
|
json.getString("ssh_host"),
|
||||||
json.getInt("ssh_port"),
|
json.getInt("ssh_port"),
|
||||||
json.getString("image_uuid"),
|
json.getString("image_uuid"),
|
||||||
json.optString("label").takeUnless { it == "null" || it.isBlank() },
|
json.optString("label").takeIf { it.isNotBlank() },
|
||||||
json.getString("local_ipaddrs").split(" ").filterNot { it == "\n" }
|
json.getString("local_ipaddrs").split(" ").filterNot { it == "\n" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package eu.m724.vastapp.vastai.data
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
|
||||||
data class User(
|
data class User(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
@ -22,7 +23,8 @@ data class User(
|
||||||
parcel.readDouble(),
|
parcel.readDouble(),
|
||||||
parcel.readDouble(),
|
parcel.readDouble(),
|
||||||
parcel.readBoolean()
|
parcel.readBoolean()
|
||||||
)
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(id)
|
parcel.writeString(id)
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="512" android:viewportWidth="448" android:width="175dp">
|
|
||||||
|
|
||||||
<path android:fillColor="#FF000000" android:pathData="M384,336l-192,0c-8.8,0 -16,-7.2 -16,-16l0,-256c0,-8.8 7.2,-16 16,-16l140.1,0L400,115.9 400,320c0,8.8 -7.2,16 -16,16zM192,384l192,0c35.3,0 64,-28.7 64,-64l0,-204.1c0,-12.7 -5.1,-24.9 -14.1,-33.9L366.1,14.1c-9,-9 -21.2,-14.1 -33.9,-14.1L192,0c-35.3,0 -64,28.7 -64,64l0,256c0,35.3 28.7,64 64,64zM64,128c-35.3,0 -64,28.7 -64,64L0,448c0,35.3 28.7,64 64,64l192,0c35.3,0 64,-28.7 64,-64l0,-32 -48,0 0,32c0,8.8 -7.2,16 -16,16L64,464c-8.8,0 -16,-7.2 -16,-16l0,-256c0,-8.8 7.2,-16 16,-16l32,0 0,-48 -32,0z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
|
@ -1,28 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:height="108dp"
|
|
||||||
android:width="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
|
|
||||||
<!-- Keep in sync with non-adaptive ic_launcher.xml -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M34,38
|
|
||||||
h6
|
|
||||||
l12,16
|
|
||||||
l-12,16
|
|
||||||
h-6
|
|
||||||
l12,-16
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M56,66
|
|
||||||
h18
|
|
||||||
v4
|
|
||||||
h-18
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</vector>
|
|
|
@ -22,13 +22,4 @@
|
||||||
<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="no_termux">Termuxn\'t</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="copied_to_clipboard">Copied command to clipboard</string>
|
|
||||||
<string name="termux_install_dropbear">Install Dropbear with:</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_open_instructions">Open instructions on github.com</string>
|
|
||||||
<string name="termux_error">An error occured:</string>
|
|
||||||
<string name="webview_todo">(this will be a webview)</string>
|
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue