Module page
This commit is contained in:
parent
279e95b57b
commit
21c43250f1
|
|
@ -92,6 +92,7 @@ dependencies {
|
||||||
implementation("androidx.room:room-runtime:$roomVersion")
|
implementation("androidx.room:room-runtime:$roomVersion")
|
||||||
implementation("com.google.accompanist:accompanist-drawablepainter:0.24.11-rc")
|
implementation("com.google.accompanist:accompanist-drawablepainter:0.24.11-rc")
|
||||||
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.11-rc")
|
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.11-rc")
|
||||||
|
implementation("com.google.accompanist:accompanist-pager:0.24.11-rc")
|
||||||
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.11-rc")
|
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.11-rc")
|
||||||
implementation("com.google.android.material:material:1.6.1")
|
implementation("com.google.android.material:material:1.6.1")
|
||||||
implementation("com.google.code.gson:gson:2.9.0")
|
implementation("com.google.code.gson:gson:2.9.0")
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ import org.lsposed.lspatch.util.LSPPackageManager
|
||||||
import org.lsposed.lspatch.util.ShizukuApi
|
import org.lsposed.lspatch.util.ShizukuApi
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
const val TAG = "LSPatch Manager"
|
|
||||||
|
|
||||||
lateinit var lspApp: LSPApplication
|
lateinit var lspApp: LSPApplication
|
||||||
|
|
||||||
class LSPApplication : Application() {
|
class LSPApplication : Application() {
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,14 @@ import android.net.Uri
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.lsposed.lspatch.TAG
|
|
||||||
import org.lsposed.lspatch.lspApp
|
import org.lsposed.lspatch.lspApp
|
||||||
|
|
||||||
class ModuleProvider : ContentProvider() {
|
class ModuleProvider : ContentProvider() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ModuleProvider"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(): Boolean {
|
override fun onCreate(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ fun AppItem(
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
checked: Boolean? = null,
|
checked: Boolean? = null,
|
||||||
rightIcon: (@Composable () -> Unit)? = null,
|
rightIcon: (@Composable () -> Unit)? = null,
|
||||||
additionalContent: (@Composable () -> Unit)? = null,
|
additionalContent: (@Composable ColumnScope.() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
if (checked != null && rightIcon != null)
|
if (checked != null && rightIcon != null)
|
||||||
throw IllegalArgumentException("`checked` and `rightIcon` should not be both set")
|
throw IllegalArgumentException("`checked` and `rightIcon` should not be both set")
|
||||||
|
|
@ -63,7 +63,7 @@ fun AppItem(
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
additionalContent?.invoke()
|
additionalContent?.invoke(this)
|
||||||
}
|
}
|
||||||
if (checked != null) {
|
if (checked != null) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
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 org.lsposed.lspatch.TAG
|
|
||||||
|
|
||||||
|
private const val TAG = "SearchBar"
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,68 @@
|
||||||
package org.lsposed.lspatch.ui.page
|
package org.lsposed.lspatch.ui.page
|
||||||
|
|
||||||
import android.app.Activity
|
import androidx.compose.foundation.layout.Box
|
||||||
import android.content.ClipData
|
import androidx.compose.foundation.layout.Column
|
||||||
import android.content.ClipboardManager
|
import androidx.compose.foundation.layout.padding
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.lspatch.Constants.PREFS_STORAGE_DIRECTORY
|
|
||||||
import org.lsposed.lspatch.R
|
import org.lsposed.lspatch.R
|
||||||
import org.lsposed.lspatch.config.ConfigManager
|
import org.lsposed.lspatch.ui.page.manage.AppManageBody
|
||||||
import org.lsposed.lspatch.database.entity.Module
|
import org.lsposed.lspatch.ui.page.manage.AppManageFab
|
||||||
import org.lsposed.lspatch.lspApp
|
import org.lsposed.lspatch.ui.page.manage.ModuleManageBody
|
||||||
import org.lsposed.lspatch.share.LSPConfig
|
|
||||||
import org.lsposed.lspatch.ui.component.AppItem
|
|
||||||
import org.lsposed.lspatch.ui.component.LoadingDialog
|
|
||||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
|
||||||
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
|
|
||||||
import org.lsposed.lspatch.ui.util.observeState
|
|
||||||
import org.lsposed.lspatch.ui.util.setState
|
|
||||||
import org.lsposed.lspatch.ui.viewmodel.ManageViewModel
|
|
||||||
import org.lsposed.lspatch.ui.viewmodel.ManageViewModel.ViewAction
|
|
||||||
import org.lsposed.lspatch.util.LSPPackageManager
|
|
||||||
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
|
|
||||||
import org.lsposed.lspatch.util.ShizukuApi
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
private const val TAG = "ManagePage"
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ManagePage() {
|
fun ManagePage() {
|
||||||
val viewModel = viewModel<ManageViewModel>()
|
val scope = rememberCoroutineScope()
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { TopBar() },
|
topBar = { TopBar() },
|
||||||
floatingActionButton = { Fab() }
|
floatingActionButton = { if (pagerState.currentPage == 0) AppManageFab() }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Box(Modifier.padding(innerPadding)) {
|
Box(Modifier.padding(innerPadding)) {
|
||||||
Body()
|
Column {
|
||||||
|
TabRow(
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
indicator = { tabPositions ->
|
||||||
|
TabRowDefaults.Indicator(Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 0,
|
||||||
|
onClick = { scope.launch { pagerState.animateScrollToPage(0) } }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp),
|
||||||
|
text = stringResource(R.string.apps)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 1,
|
||||||
|
onClick = { scope.launch { pagerState.animateScrollToPage(1) } }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp),
|
||||||
|
text = stringResource(R.string.modules)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalPager(count = 2, state = pagerState) { page ->
|
||||||
|
when (page) {
|
||||||
|
0 -> AppManageBody()
|
||||||
|
1 -> ModuleManageBody()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,301 +73,3 @@ private fun TopBar() {
|
||||||
title = { Text(PageList.Manage.title) }
|
title = { Text(PageList.Manage.title) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Fab() {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val snackbarHost = LocalSnackbarHost.current
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
var shouldSelectDirectory by remember { mutableStateOf(false) }
|
|
||||||
var showNewPatchDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val errorText = stringResource(R.string.patch_select_dir_error)
|
|
||||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
try {
|
|
||||||
if (it.resultCode == Activity.RESULT_CANCELED) return@rememberLauncherForActivityResult
|
|
||||||
val uri = it.data?.data ?: throw IOException("No data")
|
|
||||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
|
||||||
lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, uri.toString()).apply()
|
|
||||||
Log.i(TAG, "Storage directory: ${uri.path}")
|
|
||||||
showNewPatchDialog = true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error when requesting saving directory", e)
|
|
||||||
scope.launch { snackbarHost.showSnackbar(errorText) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSelectDirectory) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { shouldSelectDirectory = false },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
content = { Text(stringResource(android.R.string.ok)) },
|
|
||||||
onClick = {
|
|
||||||
launcher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
|
|
||||||
shouldSelectDirectory = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
content = { Text(stringResource(android.R.string.cancel)) },
|
|
||||||
onClick = { shouldSelectDirectory = false }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(R.string.patch_select_dir_title),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = { Text(stringResource(R.string.patch_select_dir_text)) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showNewPatchDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showNewPatchDialog = false },
|
|
||||||
confirmButton = {},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
content = { Text(stringResource(android.R.string.cancel)) },
|
|
||||||
onClick = { showNewPatchDialog = false }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(R.string.page_new_patch),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
|
||||||
TextButton(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(PageList.NewPatch.name + "?from=storage")
|
|
||||||
showNewPatchDialog = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(vertical = 8.dp),
|
|
||||||
text = stringResource(R.string.patch_from_storage),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(PageList.NewPatch.name + "?from=applist")
|
|
||||||
showNewPatchDialog = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(vertical = 8.dp),
|
|
||||||
text = stringResource(R.string.patch_from_applist),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FloatingActionButton(
|
|
||||||
content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) },
|
|
||||||
onClick = {
|
|
||||||
val uri = lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)?.toUri()
|
|
||||||
if (uri == null) {
|
|
||||||
shouldSelectDirectory = true
|
|
||||||
} else {
|
|
||||||
runCatching {
|
|
||||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
|
||||||
if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException("Storage directory was deleted")
|
|
||||||
}.onSuccess {
|
|
||||||
showNewPatchDialog = true
|
|
||||||
}.onFailure {
|
|
||||||
Log.w(TAG, "Failed to take persistable permission for saved uri", it)
|
|
||||||
lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, null).apply()
|
|
||||||
shouldSelectDirectory = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun Body() {
|
|
||||||
val viewModel = viewModel<ManageViewModel>()
|
|
||||||
val snackbarHost = LocalSnackbarHost.current
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
if (viewModel.appList.isEmpty()) {
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.align(Alignment.Center),
|
|
||||||
text = run {
|
|
||||||
if (LSPPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading)
|
|
||||||
else stringResource(R.string.manage_no_apps)
|
|
||||||
},
|
|
||||||
fontFamily = FontFamily.Serif,
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var scopeApp by rememberSaveable { mutableStateOf("") }
|
|
||||||
val isCancelled by navController.currentBackStackEntry!!.observeState<Boolean>("isCancelled")
|
|
||||||
LaunchedEffect(isCancelled) {
|
|
||||||
if (isCancelled == false) {
|
|
||||||
val selected = navController.currentBackStackEntry!!
|
|
||||||
.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("selected").value!!.toSet()
|
|
||||||
Log.d(TAG, "Clear module list for $scopeApp")
|
|
||||||
ConfigManager.getModulesForApp(scopeApp).forEach {
|
|
||||||
ConfigManager.deactivateModule(scopeApp, it)
|
|
||||||
}
|
|
||||||
selected.forEach {
|
|
||||||
Log.d(TAG, "Activate ${it.app.packageName} for $scopeApp")
|
|
||||||
ConfigManager.activateModule(scopeApp, Module(it.app.packageName, it.app.sourceDir))
|
|
||||||
}
|
|
||||||
navController.currentBackStackEntry!!.setState("isCancelled", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewModel.processingUpdate) LoadingDialog()
|
|
||||||
viewModel.updateLoaderResult?.let {
|
|
||||||
val updateSuccessfully = stringResource(R.string.manage_update_loader_successfully)
|
|
||||||
val updateFailed = stringResource(R.string.manage_update_loader_failed)
|
|
||||||
val copyError = stringResource(R.string.copy_error)
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
it.onSuccess {
|
|
||||||
LSPPackageManager.fetchAppList()
|
|
||||||
snackbarHost.showSnackbar(updateSuccessfully)
|
|
||||||
}.onFailure {
|
|
||||||
val result = snackbarHost.showSnackbar(updateFailed, copyError)
|
|
||||||
if (result == SnackbarResult.ActionPerformed) {
|
|
||||||
val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
cm.setPrimaryClip(ClipData.newPlainText("LSPatch", it.toString()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.dispatch(ViewAction.ClearUpdateLoaderResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn {
|
|
||||||
items(
|
|
||||||
items = viewModel.appList,
|
|
||||||
key = { it.first.app.packageName }
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
Box {
|
|
||||||
AppItem(
|
|
||||||
modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)),
|
|
||||||
icon = LSPPackageManager.getIcon(it.first),
|
|
||||||
label = it.first.label,
|
|
||||||
packageName = it.first.app.packageName,
|
|
||||||
onClick = { expanded = true },
|
|
||||||
onLongClick = { expanded = true },
|
|
||||||
additionalContent = {
|
|
||||||
val text = buildAnnotatedString {
|
|
||||||
val (text, color) =
|
|
||||||
if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary
|
|
||||||
else stringResource(R.string.patch_portable) to MaterialTheme.colorScheme.tertiary
|
|
||||||
append(AnnotatedString(text, SpanStyle(color = color)))
|
|
||||||
append(" ")
|
|
||||||
append(it.second.lspConfig.VERSION_CODE.toString())
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
fontFamily = FontFamily.Serif,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
|
||||||
val shizukuUnavailable = stringResource(R.string.shizuku_unavailable)
|
|
||||||
if (it.second.lspConfig.VERSION_CODE >= 319 && it.second.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.manage_update_loader)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
scope.launch {
|
|
||||||
if (!ShizukuApi.isPermissionGranted) {
|
|
||||||
snackbarHost.showSnackbar(shizukuUnavailable)
|
|
||||||
} else {
|
|
||||||
viewModel.dispatch(ViewAction.UpdateLoader(it.first, it.second))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (it.second.useManager) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.manage_module_scope)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
scope.launch {
|
|
||||||
scopeApp = it.first.app.packageName
|
|
||||||
val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet()
|
|
||||||
navController.currentBackStackEntry!!.setState(
|
|
||||||
"selected",
|
|
||||||
SnapshotStateList<AppInfo>().apply {
|
|
||||||
LSPPackageManager.appList.filterTo(this) { activated.contains(it.app.packageName) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
navController.navigate(PageList.SelectApps.name + "?multiSelect=true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val optimizeSucceed = stringResource(R.string.manage_optimize_successfully)
|
|
||||||
val optimizeFailed = stringResource(R.string.manage_optimize_failed)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.manage_optimize)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
scope.launch {
|
|
||||||
if (!ShizukuApi.isPermissionGranted) {
|
|
||||||
snackbarHost.showSnackbar(shizukuUnavailable)
|
|
||||||
} else {
|
|
||||||
val result = ShizukuApi.performDexOptMode(it.first.app.packageName)
|
|
||||||
snackbarHost.showSnackbar(if (result) optimizeSucceed else optimizeFailed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val uninstallSuccessfully = stringResource(R.string.manage_uninstall_successfully)
|
|
||||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
scope.launch {
|
|
||||||
LSPPackageManager.fetchAppList()
|
|
||||||
snackbarHost.showSnackbar(uninstallSuccessfully)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.uninstall)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
val intent = Intent(Intent.ACTION_DELETE).apply {
|
|
||||||
data = Uri.parse("package:${it.first.app.packageName}")
|
|
||||||
putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
|
||||||
}
|
|
||||||
launcher.launch(intent)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,349 @@
|
||||||
|
package org.lsposed.lspatch.ui.page.manage
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.lsposed.lspatch.Constants
|
||||||
|
import org.lsposed.lspatch.R
|
||||||
|
import org.lsposed.lspatch.config.ConfigManager
|
||||||
|
import org.lsposed.lspatch.database.entity.Module
|
||||||
|
import org.lsposed.lspatch.lspApp
|
||||||
|
import org.lsposed.lspatch.share.LSPConfig
|
||||||
|
import org.lsposed.lspatch.ui.component.AppItem
|
||||||
|
import org.lsposed.lspatch.ui.component.LoadingDialog
|
||||||
|
import org.lsposed.lspatch.ui.page.PageList
|
||||||
|
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||||
|
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
|
||||||
|
import org.lsposed.lspatch.ui.util.observeState
|
||||||
|
import org.lsposed.lspatch.ui.util.setState
|
||||||
|
import org.lsposed.lspatch.ui.viewmodel.manage.AppManageViewModel
|
||||||
|
import org.lsposed.lspatch.util.LSPPackageManager
|
||||||
|
import org.lsposed.lspatch.util.ShizukuApi
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val TAG = "AppManagePage"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppManageBody() {
|
||||||
|
val viewModel = viewModel<AppManageViewModel>()
|
||||||
|
val snackbarHost = LocalSnackbarHost.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
if (viewModel.appList.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
text = run {
|
||||||
|
if (LSPPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading)
|
||||||
|
else stringResource(R.string.manage_no_apps)
|
||||||
|
},
|
||||||
|
fontFamily = FontFamily.Serif,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var scopeApp by rememberSaveable { mutableStateOf("") }
|
||||||
|
val isCancelled by navController.currentBackStackEntry!!.observeState<Boolean>("isCancelled")
|
||||||
|
LaunchedEffect(isCancelled) {
|
||||||
|
if (isCancelled == false) {
|
||||||
|
val selected = navController.currentBackStackEntry!!
|
||||||
|
.savedStateHandle.getLiveData<SnapshotStateList<LSPPackageManager.AppInfo>>("selected").value!!.toSet()
|
||||||
|
Log.d(TAG, "Clear module list for $scopeApp")
|
||||||
|
ConfigManager.getModulesForApp(scopeApp).forEach {
|
||||||
|
ConfigManager.deactivateModule(scopeApp, it)
|
||||||
|
}
|
||||||
|
selected.forEach {
|
||||||
|
Log.d(TAG, "Activate ${it.app.packageName} for $scopeApp")
|
||||||
|
ConfigManager.activateModule(scopeApp, Module(it.app.packageName, it.app.sourceDir))
|
||||||
|
}
|
||||||
|
navController.currentBackStackEntry!!.setState("isCancelled", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.processingUpdate) LoadingDialog()
|
||||||
|
viewModel.updateLoaderResult?.let {
|
||||||
|
val updateSuccessfully = stringResource(R.string.manage_update_loader_successfully)
|
||||||
|
val updateFailed = stringResource(R.string.manage_update_loader_failed)
|
||||||
|
val copyError = stringResource(R.string.copy_error)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
it.onSuccess {
|
||||||
|
LSPPackageManager.fetchAppList()
|
||||||
|
snackbarHost.showSnackbar(updateSuccessfully)
|
||||||
|
}.onFailure {
|
||||||
|
val result = snackbarHost.showSnackbar(updateFailed, copyError)
|
||||||
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
|
val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
cm.setPrimaryClip(ClipData.newPlainText("LSPatch", it.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.dispatch(AppManageViewModel.ViewAction.ClearUpdateLoaderResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(Modifier.fillMaxHeight()) {
|
||||||
|
items(
|
||||||
|
items = viewModel.appList,
|
||||||
|
key = { it.first.app.packageName }
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
AppItem(
|
||||||
|
icon = LSPPackageManager.getIcon(it.first),
|
||||||
|
label = it.first.label,
|
||||||
|
packageName = it.first.app.packageName,
|
||||||
|
onClick = { expanded = true },
|
||||||
|
onLongClick = { expanded = true },
|
||||||
|
additionalContent = {
|
||||||
|
Text(
|
||||||
|
text = buildAnnotatedString {
|
||||||
|
val (text, color) =
|
||||||
|
if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary
|
||||||
|
else stringResource(R.string.patch_portable) to MaterialTheme.colorScheme.tertiary
|
||||||
|
append(AnnotatedString(text, SpanStyle(color = color)))
|
||||||
|
append(" ")
|
||||||
|
append(it.second.lspConfig.VERSION_CODE.toString())
|
||||||
|
},
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontFamily = FontFamily.Serif,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
val shizukuUnavailable = stringResource(R.string.shizuku_unavailable)
|
||||||
|
if (it.second.lspConfig.VERSION_CODE >= 319 && it.second.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.manage_update_loader)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
if (!ShizukuApi.isPermissionGranted) {
|
||||||
|
snackbarHost.showSnackbar(shizukuUnavailable)
|
||||||
|
} else {
|
||||||
|
viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (it.second.useManager) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.manage_module_scope)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
scopeApp = it.first.app.packageName
|
||||||
|
val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet()
|
||||||
|
navController.currentBackStackEntry!!.setState(
|
||||||
|
"selected",
|
||||||
|
SnapshotStateList<LSPPackageManager.AppInfo>().apply {
|
||||||
|
LSPPackageManager.appList.filterTo(this) { activated.contains(it.app.packageName) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
navController.navigate(PageList.SelectApps.name + "?multiSelect=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val optimizeSucceed = stringResource(R.string.manage_optimize_successfully)
|
||||||
|
val optimizeFailed = stringResource(R.string.manage_optimize_failed)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.manage_optimize)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
if (!ShizukuApi.isPermissionGranted) {
|
||||||
|
snackbarHost.showSnackbar(shizukuUnavailable)
|
||||||
|
} else {
|
||||||
|
val result = ShizukuApi.performDexOptMode(it.first.app.packageName)
|
||||||
|
snackbarHost.showSnackbar(if (result) optimizeSucceed else optimizeFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val uninstallSuccessfully = stringResource(R.string.manage_uninstall_successfully)
|
||||||
|
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
scope.launch {
|
||||||
|
LSPPackageManager.fetchAppList()
|
||||||
|
snackbarHost.showSnackbar(uninstallSuccessfully)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.uninstall)) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
val intent = Intent(Intent.ACTION_DELETE).apply {
|
||||||
|
data = Uri.parse("package:${it.first.app.packageName}")
|
||||||
|
putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||||
|
}
|
||||||
|
launcher.launch(intent)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppManageFab() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackbarHost = LocalSnackbarHost.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var shouldSelectDirectory by remember { mutableStateOf(false) }
|
||||||
|
var showNewPatchDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val errorText = stringResource(R.string.patch_select_dir_error)
|
||||||
|
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
try {
|
||||||
|
if (it.resultCode == Activity.RESULT_CANCELED) return@rememberLauncherForActivityResult
|
||||||
|
val uri = it.data?.data ?: throw IOException("No data")
|
||||||
|
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
lspApp.prefs.edit().putString(Constants.PREFS_STORAGE_DIRECTORY, uri.toString()).apply()
|
||||||
|
Log.i(TAG, "Storage directory: ${uri.path}")
|
||||||
|
showNewPatchDialog = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error when requesting saving directory", e)
|
||||||
|
scope.launch { snackbarHost.showSnackbar(errorText) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSelectDirectory) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { shouldSelectDirectory = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
content = { Text(stringResource(android.R.string.ok)) },
|
||||||
|
onClick = {
|
||||||
|
launcher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
|
||||||
|
shouldSelectDirectory = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
content = { Text(stringResource(android.R.string.cancel)) },
|
||||||
|
onClick = { shouldSelectDirectory = false }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.patch_select_dir_title),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = { Text(stringResource(R.string.patch_select_dir_text)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showNewPatchDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showNewPatchDialog = false },
|
||||||
|
confirmButton = {},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
content = { Text(stringResource(android.R.string.cancel)) },
|
||||||
|
onClick = { showNewPatchDialog = false }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.page_new_patch),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(PageList.NewPatch.name + "?from=storage")
|
||||||
|
showNewPatchDialog = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
text = stringResource(R.string.patch_from_storage),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(PageList.NewPatch.name + "?from=applist")
|
||||||
|
showNewPatchDialog = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
text = stringResource(R.string.patch_from_applist),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) },
|
||||||
|
onClick = {
|
||||||
|
val uri = lspApp.prefs.getString(Constants.PREFS_STORAGE_DIRECTORY, null)?.toUri()
|
||||||
|
if (uri == null) {
|
||||||
|
shouldSelectDirectory = true
|
||||||
|
} else {
|
||||||
|
runCatching {
|
||||||
|
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException("Storage directory was deleted")
|
||||||
|
}.onSuccess {
|
||||||
|
showNewPatchDialog = true
|
||||||
|
}.onFailure {
|
||||||
|
Log.w(TAG, "Failed to take persistable permission for saved uri", it)
|
||||||
|
lspApp.prefs.edit().putString(Constants.PREFS_STORAGE_DIRECTORY, null).apply()
|
||||||
|
shouldSelectDirectory = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package org.lsposed.lspatch.ui.page.manage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import org.lsposed.lspatch.R
|
||||||
|
import org.lsposed.lspatch.ui.component.AppItem
|
||||||
|
import org.lsposed.lspatch.ui.viewmodel.manage.ModuleManageViewModel
|
||||||
|
import org.lsposed.lspatch.util.LSPPackageManager
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModuleManageBody() {
|
||||||
|
val viewModel = viewModel<ModuleManageViewModel>()
|
||||||
|
if (viewModel.appList.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
text = run {
|
||||||
|
if (LSPPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading)
|
||||||
|
else stringResource(R.string.manage_no_modules)
|
||||||
|
},
|
||||||
|
fontFamily = FontFamily.Serif,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(Modifier.fillMaxHeight()) {
|
||||||
|
items(
|
||||||
|
items = viewModel.appList,
|
||||||
|
key = { it.first.app.packageName }
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
AppItem(
|
||||||
|
icon = LSPPackageManager.getIcon(it.first),
|
||||||
|
label = it.first.label,
|
||||||
|
packageName = it.first.app.packageName,
|
||||||
|
onClick = { /* TODO: startAndSendModuleBinder */ },
|
||||||
|
onLongClick = { expanded = true },
|
||||||
|
additionalContent = {
|
||||||
|
Text(
|
||||||
|
text = it.second.description,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = buildAnnotatedString {
|
||||||
|
append(AnnotatedString("API", SpanStyle(color = MaterialTheme.colorScheme.secondary)))
|
||||||
|
append(" ")
|
||||||
|
append(it.second.api.toString())
|
||||||
|
},
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontFamily = FontFamily.Serif,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,12 @@ import org.lsposed.lspatch.util.LSPPackageManager
|
||||||
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
|
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
|
||||||
import org.lsposed.patch.util.Logger
|
import org.lsposed.patch.util.Logger
|
||||||
|
|
||||||
private const val TAG = "NewPatchViewModel"
|
|
||||||
|
|
||||||
class NewPatchViewModel : ViewModel() {
|
class NewPatchViewModel : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "NewPatchViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
enum class PatchState {
|
enum class PatchState {
|
||||||
SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR
|
SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ import kotlinx.coroutines.launch
|
||||||
import org.lsposed.lspatch.util.LSPPackageManager
|
import org.lsposed.lspatch.util.LSPPackageManager
|
||||||
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
|
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
|
||||||
|
|
||||||
private const val TAG = "SelectAppViewModel"
|
|
||||||
|
|
||||||
class SelectAppsViewModel : ViewModel() {
|
class SelectAppsViewModel : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SelectAppViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(TAG, "SelectAppsViewModel ${toString().substringAfterLast('@')} construct")
|
Log.d(TAG, "SelectAppsViewModel ${toString().substringAfterLast('@')} construct")
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package org.lsposed.lspatch.ui.viewmodel
|
package org.lsposed.lspatch.ui.viewmodel.manage
|
||||||
|
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
|
@ -23,9 +23,11 @@ import org.lsposed.patch.util.Logger
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
private const val TAG = "ManageViewModel"
|
class AppManageViewModel : ViewModel() {
|
||||||
|
|
||||||
class ManageViewModel : ViewModel() {
|
companion object {
|
||||||
|
private const val TAG = "ManageViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
sealed class ViewAction {
|
sealed class ViewAction {
|
||||||
data class UpdateLoader(val appInfo: AppInfo, val config: PatchConfig) : ViewAction()
|
data class UpdateLoader(val appInfo: AppInfo, val config: PatchConfig) : ViewAction()
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.lsposed.lspatch.ui.viewmodel.manage
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.lsposed.lspatch.util.LSPPackageManager
|
||||||
|
|
||||||
|
class ModuleManageViewModel : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ModuleManageViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
|
class XposedInfo(
|
||||||
|
val api: Int,
|
||||||
|
val description: String,
|
||||||
|
val scope: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
val appList: List<Pair<LSPPackageManager.AppInfo, XposedInfo>> by derivedStateOf {
|
||||||
|
LSPPackageManager.appList.mapNotNull { appInfo ->
|
||||||
|
val metaData = appInfo.app.metaData ?: return@mapNotNull null
|
||||||
|
appInfo to XposedInfo(
|
||||||
|
metaData.getInt("xposedminversion", -1).also { if (it == -1) return@mapNotNull null },
|
||||||
|
metaData.getString("xposeddescription") ?: "",
|
||||||
|
emptyList() // TODO: scope
|
||||||
|
)
|
||||||
|
}.also {
|
||||||
|
Log.d(TAG, "Loaded ${it.size} Xposed modules")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
<string name="uninstall">Uninstall</string>
|
<string name="uninstall">Uninstall</string>
|
||||||
<string name="uninstalling">Uninstalling</string>
|
<string name="uninstalling">Uninstalling</string>
|
||||||
<string name="copy_error">Copy error</string>
|
<string name="copy_error">Copy error</string>
|
||||||
|
<string name="apps">Apps</string>
|
||||||
|
<string name="modules">Modules</string>
|
||||||
<string name="shizuku_available">Shizuku service available</string>
|
<string name="shizuku_available">Shizuku service available</string>
|
||||||
<string name="shizuku_unavailable">Shizuku service not connected</string>
|
<string name="shizuku_unavailable">Shizuku service not connected</string>
|
||||||
<string name="page_repo">Repo</string>
|
<string name="page_repo">Repo</string>
|
||||||
|
|
@ -35,6 +37,7 @@
|
||||||
<string name="manage_optimize_successfully">Optimize successfully</string>
|
<string name="manage_optimize_successfully">Optimize successfully</string>
|
||||||
<string name="manage_optimize_failed">Optimize failed</string>
|
<string name="manage_optimize_failed">Optimize failed</string>
|
||||||
<string name="manage_uninstall_successfully">Uninstall successfully</string>
|
<string name="manage_uninstall_successfully">Uninstall successfully</string>
|
||||||
|
<string name="manage_no_modules">No modules yet</string>
|
||||||
|
|
||||||
<!-- New Patch Page -->
|
<!-- New Patch Page -->
|
||||||
<string name="page_new_patch">New Patch</string>
|
<string name="page_new_patch">New Patch</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue