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
|
<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"
|
||||||
|
|
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.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
|
||||||
class PermissionChecker(private val context: Context) {
|
class PermissionChecker {
|
||||||
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if the app has a permission
|
* check if the app has a permission
|
||||||
* @param permission the permission
|
* @param permission the permission
|
||||||
|
* @param context application context
|
||||||
* @return whether the app has the permission? obviously
|
* @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
|
return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if a permission exists or if the app is installed
|
* check if a permission exists or if the app is installed
|
||||||
* @param permission the permission
|
* @param permission the permission
|
||||||
|
* @param packageManager usually from application context
|
||||||
* @return if the permission exists
|
* @return if the permission exists
|
||||||
*/
|
*/
|
||||||
fun permissionExists(permission: String): Boolean {
|
fun permissionExists(permission: String, packageManager: PackageManager): Boolean {
|
||||||
try {
|
try {
|
||||||
context.packageManager.getPermissionInfo(permission, 0)
|
packageManager.getPermissionInfo(permission, 0)
|
||||||
return true
|
return true
|
||||||
} catch (e: NameNotFoundException) {
|
} catch (e: NameNotFoundException) {
|
||||||
return false
|
return false
|
||||||
|
@ -44,25 +48,34 @@ class PermissionChecker(private val context: Context) {
|
||||||
* request a permission if that permission is not granted
|
* request a permission if that permission is not granted
|
||||||
* @param permission the permission
|
* @param permission the permission
|
||||||
* @param activity the activity you're calling from
|
* @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
|
* @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)
|
val available = canAskForPermission(permission, activity)
|
||||||
|
|
||||||
if (hasPermission(permission)) {
|
if (hasPermission(permission, activity.applicationContext)) {
|
||||||
callback(true, false)
|
callback(true, false)
|
||||||
} else if (available) { // no permission but can request
|
} 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
|
} else { // no permission and can't request
|
||||||
callback(false, false)
|
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
|
// 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(
|
activity.registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission(),
|
ActivityResultContracts.RequestPermission(),
|
||||||
callback
|
callback
|
||||||
).launch(permission)
|
).launch(permission)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -49,13 +49,13 @@ 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).build()
|
val cronetEngine = CronetEngine.Builder(baseContext).enableBrotli(true).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()
|
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,6 +1,21 @@
|
||||||
package eu.m724.vastapp.activity.dashboard
|
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
|
||||||
|
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.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
|
||||||
|
@ -13,7 +28,11 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
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> =
|
private val _uiState: MutableStateFlow<DashboardUiState> =
|
||||||
MutableStateFlow(DashboardUiState(0))
|
MutableStateFlow(DashboardUiState(0))
|
||||||
val uiState: StateFlow<DashboardUiState> =
|
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())
|
private val _refreshError: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||||
val refreshError: StateFlow<List<String>> = _refreshError.asStateFlow()
|
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)
|
_uiState.value = _uiState.value.copy(refreshing = 2)
|
||||||
_refreshError.value = emptyList()
|
_refreshError.value = emptyList()
|
||||||
|
|
||||||
|
@ -65,7 +87,77 @@ class DashboardViewModel(initialUser: User, private val vastApi: VastApi) : View
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
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
|
||||||
|
@ -31,6 +32,7 @@ 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
|
||||||
|
@ -41,6 +43,7 @@ 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()
|
||||||
|
@ -53,7 +56,7 @@ fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
state = rememberPullToRefreshState(),
|
state = rememberPullToRefreshState(),
|
||||||
onRefresh = { dashboardViewModel.refresh() }
|
onRefresh = { dashboardViewModel.refresh(context as ComponentActivity) }
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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,11 +13,12 @@ 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.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
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 context = LocalContext.current
|
val activity = LocalContext.current as ComponentActivity
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
@ -24,8 +26,7 @@ fun HelpScreen(dashboardViewModel: DashboardViewModel) { // TODO make this a web
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com"))
|
Opener.openUrl("https://vast.ai/docs", activity)
|
||||||
context.startActivity(browserIntent)
|
|
||||||
}) {
|
}) {
|
||||||
Text(text = "https://vast.ai/docs")
|
Text(text = "https://vast.ai/docs")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
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.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.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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
|
||||||
|
@ -14,20 +18,36 @@ 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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
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.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.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
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.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
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.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.tooling.preview.Preview
|
||||||
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.PermissionChecker
|
||||||
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
import eu.m724.vastapp.activity.dashboard.DashboardViewModel
|
||||||
import eu.m724.vastapp.vastai.data.RentedInstance
|
import eu.m724.vastapp.vastai.data.RentedInstance
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
@ -38,8 +58,11 @@ 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 rentedInstances by dashboardViewModel.rentedInstances.collectAsState()
|
||||||
|
val termuxAvailable by dashboardViewModel.termuxAvailable.collectAsState()
|
||||||
|
|
||||||
// TODO actually get instances
|
// TODO actually get instances
|
||||||
|
|
||||||
|
@ -50,101 +73,63 @@ fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
itemCount = rentedInstances.size,
|
itemCount = rentedInstances.size,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) { i ->
|
) { i ->
|
||||||
RentedInstanceCard(rentedInstance = rentedInstances[i], modifier = Modifier
|
RentedInstanceCard(
|
||||||
|
modifier = Modifier
|
||||||
.width(340.dp)
|
.width(340.dp)
|
||||||
.padding(8.dp))
|
.padding(8.dp),
|
||||||
|
rentedInstance = rentedInstances[i],
|
||||||
|
termuxAvailable = termuxAvailable,
|
||||||
|
sshButtonClick = {
|
||||||
|
dashboardViewModel.sshButtonClick(activity, it)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO maybe move this?
|
||||||
@Composable
|
@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 instance by remember(rentedInstance) { derivedStateOf { rentedInstance.instance } }
|
||||||
val label by remember(instance) { derivedStateOf {
|
val label by remember(instance) { derivedStateOf {
|
||||||
rentedInstance.label ?: instance.machine.gpu.model
|
rentedInstance.label ?: instance.machine.gpu.model
|
||||||
} }
|
} }
|
||||||
|
|
||||||
Card(modifier = modifier) {
|
Card(modifier = modifier) {
|
||||||
Row {
|
Row(modifier = Modifier.padding(8.dp)) {
|
||||||
Text(label, fontSize = 18.sp)
|
Text(label, fontSize = 22.sp)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
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(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
contentDescription = "Details about instance $label"
|
contentDescription = "Details about instance $label"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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?) {
|
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
|
||||||
|
|
|
@ -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("gpu_temp"),
|
||||||
json.optDouble("inet_down_billed").toInt(),
|
json.optDouble("inet_down_billed").toInt(),
|
||||||
json.optDouble("inet_up_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.getInt("ssh_port"),
|
||||||
json.getString("image_uuid"),
|
json.getString("image_uuid"),
|
||||||
json.optString("label").takeUnless { it == "null" || it.isBlank() },
|
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="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>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue