implement ssh button and misc improvements
a lot for one commit but I didn't wanna commit every single line of code
This commit is contained in:
parent
74aefb6592
commit
3829e9ce3f
16 changed files with 562 additions and 162 deletions
|
@ -8,14 +8,19 @@
|
|||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Vastapp"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
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
|
||||
android:name=".activity.dashboard.DashboardActivity"
|
||||
android:exported="false"
|
||||
|
|
28
app/src/main/java/eu/m724/vastapp/activity/Opener.kt
Normal file
28
app/src/main/java/eu/m724/vastapp/activity/Opener.kt
Normal file
|
@ -0,0 +1,28 @@
|
|||
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,24 +7,28 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.app.ActivityCompat
|
||||
|
||||
class PermissionChecker(private val context: Context) {
|
||||
class PermissionChecker {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* check if the app has a permission
|
||||
* @param permission the permission
|
||||
* @param context application context
|
||||
* @return whether the app has the permission? obviously
|
||||
*/
|
||||
fun hasPermission(permission: String): Boolean {
|
||||
fun hasPermission(permission: String, context: Context): Boolean {
|
||||
return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): Boolean {
|
||||
fun permissionExists(permission: String, packageManager: PackageManager): Boolean {
|
||||
try {
|
||||
context.packageManager.getPermissionInfo(permission, 0)
|
||||
packageManager.getPermissionInfo(permission, 0)
|
||||
return true
|
||||
} catch (e: NameNotFoundException) {
|
||||
return false
|
||||
|
@ -44,25 +48,34 @@ class PermissionChecker(private val context: Context) {
|
|||
* 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, callback: (Boolean, Boolean) -> Unit) {
|
||||
fun requestIfNoPermission(permission: String, activity: ComponentActivity, requestCode: Int? = null, callback: (Boolean, Boolean) -> Unit) {
|
||||
val available = canAskForPermission(permission, activity)
|
||||
|
||||
if (hasPermission(permission)) {
|
||||
if (hasPermission(permission, activity.applicationContext)) {
|
||||
callback(true, false)
|
||||
} else if (available) { // no permission but can request
|
||||
requestPermission(permission, activity) { callback(it, true) }
|
||||
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, callback: (Boolean) -> Unit) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,13 +49,13 @@ class DashboardActivity : ComponentActivity() {
|
|||
val user = intent.getParcelableExtra<User>("user")!! // TODO null check
|
||||
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val cronetEngine = CronetEngine.Builder(baseContext).build()
|
||||
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 {
|
||||
dashboardViewModel.refresh()
|
||||
dashboardViewModel.refresh(this@DashboardActivity)
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
dashboardViewModel.refreshError.collect {
|
||||
it.forEach { errorMsg ->
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
package eu.m724.vastapp.activity.dashboard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
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.compose.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.VastApi
|
||||
import eu.m724.vastapp.vastai.api.InstancesUrlRequestCallback
|
||||
|
@ -13,7 +28,11 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import kotlinx.coroutines.flow.update
|
||||
|
||||
|
||||
class DashboardViewModel(initialUser: User, private val vastApi: VastApi) : ViewModel() { // TODO do something with the user
|
||||
class DashboardViewModel(
|
||||
private val initialUser: User,
|
||||
private val vastApi: VastApi
|
||||
) : ViewModel() { // TODO do something with the user
|
||||
|
||||
private val _uiState: MutableStateFlow<DashboardUiState> =
|
||||
MutableStateFlow(DashboardUiState(0))
|
||||
val uiState: StateFlow<DashboardUiState> =
|
||||
|
@ -28,7 +47,10 @@ class DashboardViewModel(initialUser: User, private val vastApi: VastApi) : View
|
|||
private val _refreshError: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||
val refreshError: StateFlow<List<String>> = _refreshError.asStateFlow()
|
||||
|
||||
fun refresh() {
|
||||
private val _termuxAvailable: MutableStateFlow<Int> = MutableStateFlow(0)
|
||||
val termuxAvailable: StateFlow<Int> = _termuxAvailable.asStateFlow()
|
||||
|
||||
fun refresh(activity: ComponentActivity) {
|
||||
_uiState.value = _uiState.value.copy(refreshing = 2)
|
||||
_refreshError.value = emptyList()
|
||||
|
||||
|
@ -65,7 +87,77 @@ class DashboardViewModel(initialUser: User, private val vastApi: VastApi) : View
|
|||
userRequest.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
|
||||
}
|
||||
|
||||
@SuppressLint("SdCardPath")
|
||||
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)
|
||||
}
|
||||
|
||||
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,5 +1,6 @@
|
|||
package eu.m724.vastapp.activity.dashboard.screen
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
|
@ -31,6 +32,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -41,6 +43,7 @@ import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
|||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) // for pullRefresh
|
||||
@Composable
|
||||
fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
||||
val context = LocalContext.current
|
||||
val uiState by dashboardViewModel.uiState.collectAsState()
|
||||
|
||||
val user by dashboardViewModel.user.collectAsState()
|
||||
|
@ -53,7 +56,7 @@ fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
|||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing,
|
||||
state = rememberPullToRefreshState(),
|
||||
onRefresh = { dashboardViewModel.refresh() }
|
||||
onRefresh = { dashboardViewModel.refresh(context as ComponentActivity) }
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
|
|
@ -2,6 +2,7 @@ package eu.m724.vastapp.activity.dashboard.screen
|
|||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -12,11 +13,12 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.m724.vastapp.activity.Opener
|
||||
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||
|
||||
@Composable
|
||||
fun HelpScreen(dashboardViewModel: DashboardViewModel) { // TODO make this a webview
|
||||
val context = LocalContext.current
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
@ -24,8 +26,7 @@ fun HelpScreen(dashboardViewModel: DashboardViewModel) { // TODO make this a web
|
|||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Button(onClick = {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com"))
|
||||
context.startActivity(browserIntent)
|
||||
Opener.openUrl("https://vast.ai/docs", activity)
|
||||
}) {
|
||||
Text(text = "https://vast.ai/docs")
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package eu.m724.vastapp.activity.dashboard.screen
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ContextualFlowRow
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
@ -14,20 +18,36 @@ import androidx.compose.foundation.rememberScrollState
|
|||
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.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.m724.vastapp.R
|
||||
import eu.m724.vastapp.activity.PermissionChecker
|
||||
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||
import eu.m724.vastapp.vastai.data.RentedInstance
|
||||
import org.json.JSONObject
|
||||
|
@ -38,8 +58,11 @@ class Instances {
|
|||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
|
||||
val uiState by dashboardViewModel.uiState.collectAsState()
|
||||
val rentedInstances by dashboardViewModel.rentedInstances.collectAsState()
|
||||
val termuxAvailable by dashboardViewModel.termuxAvailable.collectAsState()
|
||||
|
||||
// TODO actually get instances
|
||||
|
||||
|
@ -50,23 +73,58 @@ fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
|||
itemCount = rentedInstances.size,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) { i ->
|
||||
RentedInstanceCard(rentedInstance = rentedInstances[i], modifier = Modifier
|
||||
RentedInstanceCard(
|
||||
modifier = Modifier
|
||||
.width(340.dp)
|
||||
.padding(8.dp))
|
||||
.padding(8.dp),
|
||||
rentedInstance = rentedInstances[i],
|
||||
termuxAvailable = termuxAvailable,
|
||||
sshButtonClick = {
|
||||
dashboardViewModel.sshButtonClick(activity, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO maybe move this?
|
||||
@Composable
|
||||
fun RentedInstanceCard(rentedInstance: RentedInstance, modifier: Modifier = Modifier) {
|
||||
fun RentedInstanceCard(
|
||||
modifier: Modifier = Modifier,
|
||||
rentedInstance: RentedInstance,
|
||||
termuxAvailable: Int,
|
||||
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) {
|
||||
Row {
|
||||
Text(label, fontSize = 18.sp)
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
Text(label, fontSize = 22.sp)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Button( // TODO consider other buttons
|
||||
modifier = Modifier.height(24.dp),
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
onClick = { sshButtonClick(rentedInstance) }
|
||||
) {
|
||||
if (termuxAvailable > -1) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.termux_icon),
|
||||
contentDescription = "Run in Termux"
|
||||
)
|
||||
Text("ssh")
|
||||
Spacer(modifier = Modifier.size(4.dp)) // necessary because TODO the termux icon has padding
|
||||
} else {
|
||||
Text("ssh")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = "Details about instance $label"
|
||||
|
@ -74,77 +132,4 @@ fun RentedInstanceCard(rentedInstance: RentedInstance, modifier: Modifier = Modi
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO maybe move this?
|
||||
@Composable
|
||||
fun InstanceCard(instance: JSONObject, modifier: Modifier = Modifier) {
|
||||
val gpuUsage = instance.getInt("gpu_util")
|
||||
val vramGb = instance.getInt("gpu_ram") / 1000.0
|
||||
val vramGbUsed = vramGb * instance.getDouble("vmem_usage")
|
||||
|
||||
Card(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Text(text = instance.getString("id"), fontSize = 14.sp)
|
||||
Text(text = "m:" + instance.getString("machine_id"), fontSize = 14.sp)
|
||||
Text(text = "h:" + instance.getString("host_id"), fontSize = 14.sp)
|
||||
}
|
||||
|
||||
Row {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(0.5f),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(text = instance.getString("gpu_name"), fontSize = 24.sp)
|
||||
if (instance.getInt("num_gpus") > 1) {
|
||||
Text(text = "x" + instance.getString("num_gpus"), modifier = Modifier.padding(start = 2.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
) {
|
||||
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() }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewInstanceCard() {
|
||||
val instance = JSONObject()
|
||||
instance.put("id", 3423941)
|
||||
InstanceCard(instance = instance, modifier = Modifier.size(300.dp))
|
||||
}
|
|
@ -78,27 +78,6 @@ class LoginActivity : ComponentActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { _ -> finish() } // TODO re-login here
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
package eu.m724.vastapp.activity.termux
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
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.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.tooling.preview.Preview
|
||||
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", "")
|
||||
println(exitCode)
|
||||
println("C" + termuxResult.getString("stdout"))
|
||||
println(termuxResult.getInt("err"))
|
||||
|
||||
var msg = stdout
|
||||
|
||||
if (internalErrorCode == -1) {
|
||||
if (exitCode == 0) {
|
||||
if (stdout.isEmpty()) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
getString(R.string.termux_no_ssh),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
} 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("An error occured:")
|
||||
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("No SSH client installed.")
|
||||
Text("Install Dropbear with:")
|
||||
|
||||
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(24.dp),
|
||||
imageVector = Icons.Outlined.Home, // TODO copy icon here
|
||||
contentDescription = "Copy command"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
FilledTonalButton(onClick = onOpenTermuxButton) {
|
||||
Text("Open Termux")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TermuxSetupGuide(onUrlButtonClick: (String) -> Unit) {
|
||||
Text("Termux is not configured for usage with other apps.")
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
FilledTonalButton(onClick = { onUrlButtonClick("https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent#Setup-Instructions") }) {
|
||||
Text("Open instructions on github.com")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
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)
|
|
@ -0,0 +1,58 @@
|
|||
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
|
||||
)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
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
|
||||
)
|
||||
*/
|
||||
)
|
|
@ -57,7 +57,7 @@ data class RentedInstance(
|
|||
json.optDouble("gpu_temp"),
|
||||
json.optDouble("inet_down_billed").toInt(),
|
||||
json.optDouble("inet_up_billed").toInt(),
|
||||
json.getString("ssh_host"),
|
||||
"ssh${json.getInt("ssh_idx")}.vast.ai", // TODO
|
||||
json.getInt("ssh_port"),
|
||||
json.getString("image_uuid"),
|
||||
json.optString("label").takeUnless { it == "null" || it.isBlank() },
|
||||
|
|
28
app/src/main/res/drawable/termux_icon.xml
Normal file
28
app/src/main/res/drawable/termux_icon.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<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,4 +22,7 @@
|
|||
<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="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>
|
||||
</resources>
|
Loading…
Reference in a new issue