Compare commits
11 commits
3ae52c6638
...
9d0401c0f6
Author | SHA1 | Date | |
---|---|---|---|
9d0401c0f6 | |||
2ed3d5da54 | |||
b8c7a62e58 | |||
f33bc55f68 | |||
3829e9ce3f | |||
74aefb6592 | |||
38ce999965 | |||
1d24005e5e | |||
bdd6fefed1 | |||
a48c19833b | |||
0a85bb20e0 |
20 changed files with 578 additions and 189 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,12 +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(this@DashboardActivity)
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
dashboardViewModel.refreshError.collect {
|
dashboardViewModel.refreshError.collect {
|
||||||
it.forEach { errorMsg ->
|
it.forEach { errorMsg ->
|
||||||
|
|
|
@ -2,5 +2,4 @@ package eu.m724.vastapp.activity.dashboard
|
||||||
|
|
||||||
data class DashboardUiState(
|
data class DashboardUiState(
|
||||||
val refreshing: Int = 0
|
val refreshing: Int = 0
|
||||||
) {
|
)
|
||||||
}
|
|
|
@ -1,11 +1,22 @@
|
||||||
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.Instance
|
import eu.m724.vastapp.vastai.data.RentedInstance
|
||||||
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
|
||||||
|
@ -13,14 +24,18 @@ 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> =
|
||||||
_uiState.asStateFlow()
|
_uiState.asStateFlow()
|
||||||
|
|
||||||
private val _rentedInstances: MutableStateFlow<List<Instance>> = MutableStateFlow(emptyList())
|
private val _rentedInstances: MutableStateFlow<List<RentedInstance>> = MutableStateFlow(emptyList())
|
||||||
val rentedInstances: StateFlow<List<Instance>> = _rentedInstances.asStateFlow()
|
val rentedInstances: StateFlow<List<RentedInstance>> = _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()
|
||||||
|
@ -28,7 +43,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 +83,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,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
|
||||||
|
@ -25,13 +26,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
|
||||||
|
@ -42,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()
|
||||||
|
@ -54,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,
|
||||||
|
@ -157,12 +159,12 @@ fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun balanceCardColor(balance: Double): Color {
|
fun balanceCardColor(balance: Double): Color {
|
||||||
return if (balance > 0) CardDefaults.cardColors().containerColor else MaterialTheme.colorScheme.errorContainer
|
return if (balance > 0) Color.Unspecified else MaterialTheme.colorScheme.errorContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun balanceColor(balance: Double, warningThreshold: Double): Color {
|
fun balanceColor(balance: Double, warningThreshold: Double): Color {
|
||||||
return if (balance > warningThreshold) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.error
|
return if (balance > warningThreshold) Color.Unspecified else MaterialTheme.colorScheme.error
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -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
|
||||||
|
@ -11,12 +12,15 @@ 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 context = LocalContext.current
|
val activity = LocalContext.current as ComponentActivity
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
@ -24,11 +28,10 @@ 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")
|
||||||
}
|
}
|
||||||
Text(text = "(this will be a webview)", fontSize = 12.sp)
|
Text(text = stringResource(id = R.string.webview_todo), fontSize = 12.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
package eu.m724.vastapp.activity.dashboard.screen
|
package eu.m724.vastapp.activity.dashboard.screen
|
||||||
|
|
||||||
import android.widget.ProgressBar
|
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.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
|
||||||
|
@ -14,30 +13,37 @@ 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.LinearProgressIndicator
|
import androidx.compose.material3.Icon
|
||||||
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.tooling.preview.Preview
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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 org.json.JSONObject
|
import eu.m724.vastapp.vastai.data.RentedInstance
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -45,93 +51,73 @@ fun InstancesScreen(dashboardViewModel: DashboardViewModel) {
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
itemCount = 10,
|
itemCount = rentedInstances.size,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) { i ->
|
||||||
val instance = JSONObject()
|
RentedInstanceCard(
|
||||||
instance.put("id", 234523)
|
modifier = Modifier
|
||||||
instance.put("machine_id", 1121323)
|
.width(340.dp)
|
||||||
instance.put("host_id", 5924)
|
.padding(8.dp),
|
||||||
instance.put("gpu_name", "RTX 4090")
|
rentedInstance = rentedInstances[i],
|
||||||
instance.put("num_gpus", 2)
|
termuxAvailable = termuxAvailable,
|
||||||
instance.put("gpu_util", 70)
|
sshButtonClick = {
|
||||||
instance.put("gpu_ram", 24564)
|
dashboardViewModel.sshButtonClick(activity, it)
|
||||||
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 InstanceCard(instance: JSONObject, modifier: Modifier = Modifier) {
|
fun RentedInstanceCard(
|
||||||
val gpuUsage = instance.getInt("gpu_util")
|
modifier: Modifier = Modifier,
|
||||||
val vramGb = instance.getInt("gpu_ram") / 1000.0
|
rentedInstance: RentedInstance,
|
||||||
val vramGbUsed = vramGb * instance.getDouble("vmem_usage")
|
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) {
|
Card(modifier = modifier) {
|
||||||
Column(
|
Row(modifier = Modifier.padding(8.dp)) {
|
||||||
modifier = Modifier
|
Text(label, fontSize = 22.sp)
|
||||||
.fillMaxWidth()
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
.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(
|
||||||
Column {
|
horizontalAlignment = Alignment.End
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(0.5f),
|
|
||||||
verticalAlignment = Alignment.Bottom
|
|
||||||
) {
|
) {
|
||||||
Text(text = instance.getString("gpu_name"), fontSize = 24.sp)
|
Button( // TODO consider other buttons
|
||||||
if (instance.getInt("num_gpus") > 1) {
|
modifier = Modifier.height(24.dp),
|
||||||
Text(text = "x" + instance.getString("num_gpus"), modifier = Modifier.padding(start = 2.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 {
|
||||||
|
Spacer(modifier = Modifier.size(1.dp)) // TODO make this not needed?
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(12.dp),
|
||||||
|
painter = painterResource(id = R.drawable.copy_regular), // TODO copy icon here
|
||||||
|
contentDescription = "Copy command"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(6.dp))
|
||||||
|
Text("ssh")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
modifier = Modifier.fillMaxWidth(0.5f)
|
Icon(
|
||||||
) {
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
Column(
|
contentDescription = "Details about instance $label"
|
||||||
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,142 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
|
@ -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.Instance
|
import eu.m724.vastapp.vastai.data.RentedInstance
|
||||||
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<Instance>) -> Unit,
|
val onSuccess: (List<RentedInstance>) -> 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<Instance>()
|
val instances = ArrayList<RentedInstance>()
|
||||||
|
|
||||||
val instancesJson = jsonResponse.getJSONArray("instances")
|
val instancesJson = jsonResponse.getJSONArray("instances")
|
||||||
for (i in 0..<instancesJson.length()) {
|
for (i in 0..<instancesJson.length()) {
|
||||||
instances.add(Instance.fromJson(instancesJson.getJSONObject(i)))
|
instances.add(RentedInstance.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.getDouble("vmem_usage")) / 1000).toInt(),
|
(StorageCapacityConverters.gibToGb(json.optDouble("vmem_usage")) / 1000).toInt(),
|
||||||
(json.getDouble("mem_usage") / 1000).toInt(),
|
(json.optDouble("mem_usage") / 1000).toInt(),
|
||||||
json.getDouble("gpu_util") / 100,
|
json.optDouble("gpu_util") / 100,
|
||||||
json.getDouble("cpu_util") / 100,
|
json.getDouble("cpu_util") / 100,
|
||||||
json.getDouble("gpu_temp"),
|
json.optDouble("gpu_temp"),
|
||||||
json.getDouble("inet_down_billed").toInt(),
|
json.optDouble("inet_down_billed").toInt(),
|
||||||
json.getDouble("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").takeIf { it.isNotBlank() },
|
json.optString("label").takeUnless { it == "null" || it.isBlank() },
|
||||||
json.getString("local_ipaddrs").split(" ").filterNot { it == "\n" }
|
json.getString("local_ipaddrs").split(" ").filterNot { it == "\n" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ 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,
|
||||||
|
@ -23,8 +22,7 @@ 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)
|
||||||
|
|
5
app/src/main/res/drawable/copy_regular.xml
Normal file
5
app/src/main/res/drawable/copy_regular.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
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,13 @@
|
||||||
<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