Type safe navigation

This commit is contained in:
Nullptr 2022-07-06 16:28:33 +08:00
parent 21c43250f1
commit b76ea503dd
No known key found for this signature in database
GPG Key ID: 0B9D02052FF536BD
19 changed files with 272 additions and 304 deletions

View File

@ -49,6 +49,12 @@ afterEvaluate {
val variantLowered = variant.name.toLowerCase()
val variantCapped = variant.name.capitalize()
kotlin.sourceSets {
getByName(variant.name) {
kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin")
}
}
task<Copy>("copy${variantCapped}Assets") {
dependsOn(":appstub:copy$variantCapped")
dependsOn(":patch-loader:copy$variantCapped")
@ -75,6 +81,7 @@ dependencies {
implementation(projects.share.java)
val roomVersion = "2.4.2"
val composeDestinationsVersion = "1.6.13-beta"
annotationProcessor("androidx.room:room-compiler:$roomVersion")
compileOnly("dev.rikka.hidden:stub:2.3.1")
implementation("dev.rikka.hidden:compat:2.3.1")
@ -82,7 +89,6 @@ dependencies {
implementation("androidx.activity:activity-compose:1.6.0-alpha05")
implementation("androidx.compose.material:material-icons-extended:1.3.0-alpha01")
implementation("androidx.compose.material3:material3:1.0.0-alpha14")
implementation("androidx.compose.runtime:runtime-livedata:1.3.0-alpha01")
implementation("androidx.compose.ui:ui:1.3.0-alpha01")
implementation("androidx.compose.ui:ui-tooling:1.3.0-alpha01")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0")
@ -98,6 +104,8 @@ dependencies {
implementation("com.google.code.gson:gson:2.9.0")
implementation("dev.rikka.shizuku:api:12.1.0")
implementation("dev.rikka.shizuku:provider:12.1.0")
implementation("io.github.raamcosta.compose-destinations:core:$composeDestinationsVersion")
implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3")
ksp("androidx.room:room-compiler:$roomVersion")
ksp("io.github.raamcosta.compose-destinations:ksp:$composeDestinationsVersion")
}

View File

@ -9,16 +9,19 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import org.lsposed.lspatch.ui.page.PageList
import com.ramcosta.composedestinations.DestinationsNavHost
import org.lsposed.lspatch.ui.page.BottomBarDestination
import org.lsposed.lspatch.ui.page.NavGraphs
import org.lsposed.lspatch.ui.page.appCurrentDestinationAsState
import org.lsposed.lspatch.ui.page.destinations.Destination
import org.lsposed.lspatch.ui.page.startAppDestination
import org.lsposed.lspatch.ui.theme.LSPTheme
import org.lsposed.lspatch.ui.util.LocalNavController
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
import org.lsposed.lspatch.ui.util.navigateWithState
class MainActivity : ComponentActivity() {
@ -27,25 +30,18 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberAnimatedNavController()
var mainPage by rememberSaveable { mutableStateOf(PageList.Home) }
LSPTheme {
val snackbarHostState = remember { SnackbarHostState() }
CompositionLocalProvider(
LocalNavController provides navController,
LocalSnackbarHost provides snackbarHostState
) {
CompositionLocalProvider(LocalSnackbarHost provides snackbarHostState) {
Scaffold(
bottomBar = {
MainNavigationBar(mainPage) {
if (mainPage == it) return@MainNavigationBar
mainPage = it
navController.navigateWithState(it.name)
}
},
bottomBar = { BottomBar(navController) },
snackbarHost = { SnackbarHost(snackbarHostState) }
) { innerPadding ->
MainNavHost(navController, Modifier.padding(innerPadding))
DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root,
navController = navController
)
}
}
}
@ -54,30 +50,34 @@ class MainActivity : ComponentActivity() {
}
@Composable
private fun MainNavHost(navController: NavHostController, modifier: Modifier) {
NavHost(
navController = navController,
startDestination = PageList.Home.name,
modifier = modifier
) {
for (page in PageList.values()) {
composable(route = page.route, arguments = page.arguments, content = page.body)
}
}
private fun BottomBar(navController: NavHostController) {
val currentDestination: Destination = navController.appCurrentDestinationAsState().value
?: NavGraphs.root.startAppDestination
var topDestination by rememberSaveable { mutableStateOf(currentDestination.route) }
LaunchedEffect(currentDestination) {
val queue = navController.backQueue
if (queue.size == 2) topDestination = queue[1].destination.route!!
else if (queue.size > 2) topDestination = queue[2].destination.route!!
}
@Composable
private fun MainNavigationBar(page: PageList, onClick: (PageList) -> Unit) {
NavigationBar(tonalElevation = 8.dp) {
arrayOf(PageList.Repo, PageList.Manage, PageList.Home, PageList.Logs, PageList.Settings).forEach {
BottomBarDestination.values().forEach { destination ->
NavigationBarItem(
selected = page == it,
onClick = { onClick(it) },
icon = {
if (page == it) Icon(it.iconSelected!!, it.title)
else Icon(it.iconNotSelected!!, it.title)
selected = topDestination == destination.direction.route,
onClick = {
navController.navigate(destination.direction.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
label = { Text(it.title) },
icon = {
if (topDestination == destination.direction.route) Icon(destination.iconSelected, stringResource(destination.label))
else Icon(destination.iconNotSelected, stringResource(destination.label))
},
label = { Text(stringResource(destination.label)) },
alwaysShowLabel = false
)
}

View File

@ -26,7 +26,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
private const val TAG = "SearchBar"
@OptIn(ExperimentalComposeUiApi::class)

View File

@ -0,0 +1,23 @@
package org.lsposed.lspatch.ui.page
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.ui.graphics.vector.ImageVector
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
import org.lsposed.lspatch.R
import org.lsposed.lspatch.ui.page.destinations.*
enum class BottomBarDestination(
val direction: DirectionDestinationSpec,
@StringRes val label: Int,
val iconSelected: ImageVector,
val iconNotSelected: ImageVector
) {
Repo(RepoScreenDestination, R.string.screen_repo, Icons.Filled.GetApp, Icons.Outlined.GetApp),
Manage(ManageScreenDestination, R.string.screen_manage, Icons.Filled.Dashboard, Icons.Outlined.Dashboard),
Home(HomeScreenDestination, R.string.app_name, Icons.Filled.Home, Icons.Outlined.Home),
Logs(LogsScreenDestination, R.string.screen_logs, Icons.Filled.Assignment, Icons.Outlined.Assignment),
Settings(SettingsScreenDestination, R.string.screen_settings, Icons.Filled.Settings, Icons.Outlined.Settings);
}

View File

@ -25,6 +25,8 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import kotlinx.coroutines.launch
import org.lsposed.lspatch.R
import org.lsposed.lspatch.share.LSPConfig
@ -34,8 +36,10 @@ import org.lsposed.lspatch.util.ShizukuApi
import rikka.shizuku.Shizuku
@OptIn(ExperimentalMaterial3Api::class)
@RootNavGraph(start = true)
@Destination
@Composable
fun HomePage() {
fun HomeScreen() {
Scaffold(topBar = { TopBar() }) { innerPadding ->
Column(
modifier = Modifier

View File

@ -8,11 +8,14 @@ import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.ramcosta.composedestinations.annotation.Destination
@OptIn(ExperimentalMaterial3Api::class)
@Destination
@Composable
fun RepoPage() {
fun LogsScreen() {
Scaffold(topBar = { TopBar() }) { innerPadding ->
Text(
modifier = Modifier
@ -27,6 +30,6 @@ fun RepoPage() {
@Composable
private fun TopBar() {
SmallTopAppBar(
title = { Text(PageList.Repo.title) }
title = { Text(stringResource(BottomBarDestination.Logs.label)) }
)
}

View File

@ -13,20 +13,28 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.launch
import org.lsposed.lspatch.R
import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination
import org.lsposed.lspatch.ui.page.manage.AppManageBody
import org.lsposed.lspatch.ui.page.manage.AppManageFab
import org.lsposed.lspatch.ui.page.manage.ModuleManageBody
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@Destination
@Composable
fun ManagePage() {
fun ManageScreen(
navigator: DestinationsNavigator,
resultRecipient: ResultRecipient<SelectAppsScreenDestination, SelectAppsResult>
) {
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState()
Scaffold(
topBar = { TopBar() },
floatingActionButton = { if (pagerState.currentPage == 0) AppManageFab() }
floatingActionButton = { if (pagerState.currentPage == 0) AppManageFab(navigator) }
) { innerPadding ->
Box(Modifier.padding(innerPadding)) {
Column {
@ -58,7 +66,7 @@ fun ManagePage() {
HorizontalPager(count = 2, state = pagerState) { page ->
when (page) {
0 -> AppManageBody()
0 -> AppManageBody(navigator, resultRecipient)
1 -> ModuleManageBody()
}
}
@ -70,6 +78,6 @@ fun ManagePage() {
@Composable
private fun TopBar() {
SmallTopAppBar(
title = { Text(PageList.Manage.title) }
title = { Text(stringResource(BottomBarDestination.Manage.label)) }
)
}

View File

@ -23,17 +23,18 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.NavResult
import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.lsposed.lspatch.R
@ -42,31 +43,32 @@ import org.lsposed.lspatch.ui.component.SelectionColumn
import org.lsposed.lspatch.ui.component.ShimmerAnimation
import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox
import org.lsposed.lspatch.ui.component.settings.SettingsItem
import org.lsposed.lspatch.ui.util.*
import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination
import org.lsposed.lspatch.ui.util.LocalSnackbarHost
import org.lsposed.lspatch.ui.util.isScrolledToEnd
import org.lsposed.lspatch.ui.util.lastItemIndex
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState
import org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
import org.lsposed.lspatch.util.ShizukuApi
import java.io.File
private const val TAG = "NewPatchPage"
@OptIn(ExperimentalMaterial3Api::class)
@Destination
@Composable
fun NewPatchPage(from: String, entry: NavBackStackEntry) {
fun NewPatchScreen(
navigator: DestinationsNavigator,
resultRecipient: ResultRecipient<SelectAppsScreenDestination, SelectAppsResult>,
from: String
) {
val viewModel = viewModel<NewPatchViewModel>()
val snackbarHost = LocalSnackbarHost.current
val navController = LocalNavController.current
val lifecycleOwner = LocalLifecycleOwner.current
val isCancelled by entry.observeState<Boolean>("isCancelled")
Log.d(TAG, "PatchState: ${viewModel.patchState}")
if (viewModel.patchState == PatchState.SELECTING) {
val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks ->
if (apks.isEmpty()) {
navController.popBackStack()
navigator.navigateUp()
return@rememberLauncherForActivityResult
}
runBlocking {
@ -76,28 +78,40 @@ fun NewPatchPage(from: String, entry: NavBackStackEntry) {
}
.onFailure {
lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: "Unknown error") }
navController.popBackStack()
navigator.navigateUp()
}
}
}
Log.d(TAG, "PatchState: ${viewModel.patchState}")
when (viewModel.patchState) {
PatchState.INIT -> {
LaunchedEffect(Unit) {
LSPPackageManager.cleanTmpApkDir()
if (isCancelled == true) navController.popBackStack()
else when (from) {
when (from) {
"storage" -> storageLauncher.launch(arrayOf("application/vnd.android.package-archive"))
"applist" -> {
entry.savedStateHandle.getLiveData<AppInfo>("appInfo").observe(lifecycleOwner) {
viewModel.dispatch(ViewAction.ConfigurePatch(it))
"applist" -> navigator.navigate(SelectAppsScreenDestination(false))
}
navController.navigate(PageList.SelectApps.name + "?multiSelect=false")
viewModel.dispatch(ViewAction.DoneInit)
}
}
PatchState.SELECTING -> {
resultRecipient.onNavResult {
Log.d(TAG, "onNavResult: $it")
when (it) {
is NavResult.Canceled -> navigator.navigateUp()
is NavResult.Value -> {
val result = it.value as SelectAppsResult.SingleApp
viewModel.dispatch(ViewAction.ConfigurePatch(result.selected))
}
}
}
} else {
}
else -> {
Scaffold(
topBar = {
when (viewModel.patchState) {
PatchState.CONFIGURING -> ConfiguringTopBar { navController.popBackStack() }
PatchState.CONFIGURING -> ConfiguringTopBar { navigator.navigateUp() }
PatchState.PATCHING,
PatchState.FINISHED,
PatchState.ERROR -> CenterAlignedTopAppBar(title = { Text(viewModel.patchApp.app.packageName) })
@ -111,14 +125,18 @@ fun NewPatchPage(from: String, entry: NavBackStackEntry) {
}
) { innerPadding ->
if (viewModel.patchState == PatchState.CONFIGURING) {
LaunchedEffect(Unit) {
entry.savedStateHandle.getLiveData("selected", SnapshotStateList<AppInfo>()).observe(lifecycleOwner) {
viewModel.embeddedModules = it
PatchOptionsBody(Modifier.padding(innerPadding)) {
navigator.navigate(SelectAppsScreenDestination(true, viewModel.embeddedModules.mapTo(ArrayList()) { it.app.packageName }))
}
resultRecipient.onNavResult {
if (it is NavResult.Value) {
val result = it.value as SelectAppsResult.MultipleApps
viewModel.embeddedModules = result.selected
}
}
PatchOptionsBody(Modifier.padding(innerPadding))
} else {
DoPatchBody(Modifier.padding(innerPadding))
DoPatchBody(Modifier.padding(innerPadding), navigator)
}
}
}
}
@ -127,7 +145,7 @@ fun NewPatchPage(from: String, entry: NavBackStackEntry) {
@Composable
private fun ConfiguringTopBar(onBackClick: () -> Unit) {
SmallTopAppBar(
title = { Text(stringResource(R.string.page_new_patch)) },
title = { Text(stringResource(R.string.screen_new_patch)) },
navigationIcon = {
IconButton(
onClick = onBackClick,
@ -157,9 +175,8 @@ private fun sigBypassLvStr(level: Int) = when (level) {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PatchOptionsBody(modifier: Modifier) {
private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {
val viewModel = viewModel<NewPatchViewModel>()
val navController = LocalNavController.current
Column(modifier.verticalScroll(rememberScrollState())) {
Text(
@ -195,7 +212,7 @@ private fun PatchOptionsBody(modifier: Modifier) {
desc = stringResource(R.string.patch_portable_desc),
extraContent = {
TextButton(
onClick = { navController.navigate(PageList.SelectApps.name + "?multiSelect=true") },
onClick = onAddEmbed,
content = { Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) }
)
}
@ -266,10 +283,9 @@ private fun PatchOptionsBody(modifier: Modifier) {
}
@Composable
private fun DoPatchBody(modifier: Modifier) {
private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
val viewModel = viewModel<NewPatchViewModel>()
val snackbarHost = LocalSnackbarHost.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
@ -331,7 +347,7 @@ private fun DoPatchBody(modifier: Modifier) {
installing = false
if (status == PackageInstaller.STATUS_SUCCESS) {
lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) }
navController.popBackStack()
navigator.navigateUp()
} else if (status != LSPPackageManager.STATUS_USER_CANCELLED) {
val result = snackbarHost.showSnackbar(installFailed, copyError)
if (result == SnackbarResult.ActionPerformed) {
@ -344,7 +360,7 @@ private fun DoPatchBody(modifier: Modifier) {
Row(Modifier.padding(top = 12.dp)) {
Button(
modifier = Modifier.weight(1f),
onClick = { navController.popBackStack() },
onClick = { navigator.navigateUp() },
content = { Text(stringResource(R.string.patch_return)) }
)
Spacer(Modifier.weight(0.2f))
@ -367,7 +383,7 @@ private fun DoPatchBody(modifier: Modifier) {
Row(Modifier.padding(top = 12.dp)) {
Button(
modifier = Modifier.weight(1f),
onClick = { navController.popBackStack() },
onClick = { navigator.navigateUp() },
content = { Text(stringResource(R.string.patch_return)) }
)
Spacer(Modifier.weight(0.2f))

View File

@ -1,76 +0,0 @@
package org.lsposed.lspatch.ui.page
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavType
import androidx.navigation.navArgument
import org.lsposed.lspatch.R
enum class PageList(
val iconSelected: ImageVector? = null,
val iconNotSelected: ImageVector? = null,
val arguments: List<NamedNavArgument> = emptyList(),
val body: @Composable NavBackStackEntry.() -> Unit
) {
Repo(
iconSelected = Icons.Filled.GetApp,
iconNotSelected = Icons.Outlined.GetApp,
body = { RepoPage() }
),
Manage(
iconSelected = Icons.Filled.Dashboard,
iconNotSelected = Icons.Outlined.Dashboard,
body = { ManagePage() }
),
Home(
iconSelected = Icons.Filled.Home,
iconNotSelected = Icons.Outlined.Home,
body = { HomePage() }
),
Logs(
iconSelected = Icons.Filled.Assignment,
iconNotSelected = Icons.Outlined.Assignment,
body = { LogsPage() }
),
Settings(
iconSelected = Icons.Filled.Settings,
iconNotSelected = Icons.Outlined.Settings,
body = { SettingsPage() }
),
NewPatch(
arguments = listOf(
navArgument("from") { type = NavType.StringType }
),
body = { NewPatchPage(arguments!!.getString("from")!!, this) }
),
SelectApps(
arguments = listOf(
navArgument("multiSelect") { type = NavType.BoolType }
),
body = { SelectAppsPage(arguments!!.getBoolean("multiSelect")) }
);
val title: String
@Composable get() = when (this) {
Repo -> stringResource(R.string.page_repo)
Manage -> stringResource(R.string.page_manage)
Home -> stringResource(R.string.app_name)
Logs -> stringResource(R.string.page_logs)
Settings -> stringResource(R.string.page_settings)
NewPatch -> stringResource(R.string.page_new_patch)
SelectApps -> stringResource(R.string.page_select_apps)
}
val route = buildString {
append(name)
if (arguments.isNotEmpty()) {
append(arguments.joinToString(",", "?") { "${it.name}={${it.name}}" })
}
}
}

View File

@ -8,11 +8,14 @@ import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.ramcosta.composedestinations.annotation.Destination
@OptIn(ExperimentalMaterial3Api::class)
@Destination
@Composable
fun LogsPage() {
fun RepoScreen() {
Scaffold(topBar = { TopBar() }) { innerPadding ->
Text(
modifier = Modifier
@ -27,6 +30,6 @@ fun LogsPage() {
@Composable
private fun TopBar() {
SmallTopAppBar(
title = { Text(PageList.Logs.title) }
title = { Text(stringResource(BottomBarDestination.Repo.label)) }
)
}

View File

@ -1,6 +1,7 @@
package org.lsposed.lspatch.ui.page
import android.content.pm.ApplicationInfo
import android.os.Parcelable
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
@ -13,30 +14,38 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.result.ResultBackNavigator
import kotlinx.parcelize.Parcelize
import org.lsposed.lspatch.R
import org.lsposed.lspatch.ui.component.AppItem
import org.lsposed.lspatch.ui.component.SearchAppBar
import org.lsposed.lspatch.ui.util.LocalNavController
import org.lsposed.lspatch.ui.util.observeState
import org.lsposed.lspatch.ui.util.setState
import org.lsposed.lspatch.ui.viewmodel.SelectAppsViewModel
import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
@Parcelize
sealed class SelectAppsResult : Parcelable {
data class SingleApp(val selected: AppInfo) : SelectAppsResult()
data class MultipleApps(val selected: List<AppInfo>) : SelectAppsResult()
}
@OptIn(ExperimentalMaterial3Api::class)
@Destination
@Composable
fun SelectAppsPage(multiSelect: Boolean) {
fun SelectAppsScreen(
navigator: ResultBackNavigator<SelectAppsResult>,
multiSelect: Boolean,
initialSelected: ArrayList<String>? = null
) {
val viewModel = viewModel<SelectAppsViewModel>()
val navController = LocalNavController.current
var searchPackage by remember { mutableStateOf("") }
val filter: (AppInfo) -> Boolean = {
@ -48,17 +57,20 @@ fun SelectAppsPage(multiSelect: Boolean) {
LaunchedEffect(Unit) {
viewModel.filterAppList(false, filter)
initialSelected?.let {
val tmp = initialSelected.toSet()
viewModel.multiSelected.addAll(LSPPackageManager.appList.filter { tmp.contains(it.app.packageName) })
}
}
BackHandler {
navController.previousBackStackEntry!!.setState("isCancelled", true)
navController.popBackStack()
navigator.navigateBack()
}
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.page_select_apps)) },
title = { Text(stringResource(R.string.screen_select_apps)) },
searchText = searchPackage,
onSearchTextChange = {
searchPackage = it
@ -69,13 +81,14 @@ fun SelectAppsPage(multiSelect: Boolean) {
viewModel.filterAppList(false, filter)
},
onBackClick = {
navController.previousBackStackEntry!!.setState("isCancelled", true)
navController.popBackStack()
navigator.navigateBack()
}
)
},
floatingActionButton = {
if (multiSelect) MultiSelectFab()
if (multiSelect) MultiSelectFab {
navigator.navigateBack(SelectAppsResult.MultipleApps(viewModel.multiSelected))
}
}
) { innerPadding ->
SwipeRefresh(
@ -86,29 +99,25 @@ fun SelectAppsPage(multiSelect: Boolean) {
.fillMaxSize()
) {
if (multiSelect) MultiSelect()
else SingleSelect()
else SingleSelect {
navigator.navigateBack(SelectAppsResult.SingleApp(it))
}
}
}
}
@Composable
private fun MultiSelectFab() {
val navController = LocalNavController.current
private fun MultiSelectFab(onClick: () -> Unit) {
FloatingActionButton(
onClick = {
navController.previousBackStackEntry!!.setState("isCancelled", false)
navController.popBackStack()
},
onClick = onClick,
content = { Icon(Icons.Outlined.Done, stringResource(R.string.add)) }
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SingleSelect() {
val navController = LocalNavController.current
private fun SingleSelect(onSelect: (AppInfo) -> Unit) {
val viewModel = viewModel<SelectAppsViewModel>()
LazyColumn {
items(
items = viewModel.filteredList,
@ -119,10 +128,7 @@ private fun SingleSelect() {
icon = LSPPackageManager.getIcon(it),
label = it.label,
packageName = it.app.packageName,
onClick = {
navController.previousBackStackEntry!!.setState("appInfo", it)
navController.popBackStack()
}
onClick = { onSelect(it) }
)
}
}
@ -131,24 +137,21 @@ private fun SingleSelect() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun MultiSelect() {
val navController = LocalNavController.current
val viewModel = viewModel<SelectAppsViewModel>()
val selected by navController.previousBackStackEntry!!.observeState<SnapshotStateList<AppInfo>>("selected")
LazyColumn {
items(
items = viewModel.filteredList,
key = { it.app.packageName }
) {
val checked = selected!!.contains(it)
val checked = viewModel.multiSelected.contains(it)
AppItem(
modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)),
icon = LSPPackageManager.getIcon(it),
label = it.label,
packageName = it.app.packageName,
onClick = {
if (checked) selected!!.remove(it)
else selected!!.add(it)
if (checked) viewModel.multiSelected.remove(it)
else viewModel.multiSelected.add(it)
},
checked = checked
)

View File

@ -22,6 +22,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import kotlinx.coroutines.launch
import org.lsposed.lspatch.R
import org.lsposed.lspatch.config.MyKeyStore
@ -31,8 +32,9 @@ import java.security.GeneralSecurityException
import java.security.KeyStore
@OptIn(ExperimentalMaterial3Api::class)
@Destination
@Composable
fun SettingsPage() {
fun SettingsScreen() {
Scaffold(
topBar = { TopBar() }
) { innerPadding ->
@ -49,7 +51,7 @@ fun SettingsPage() {
@Composable
private fun TopBar() {
SmallTopAppBar(
title = { Text(stringResource(R.string.page_settings)) }
title = { Text(stringResource(R.string.screen_settings)) }
)
}

View File

@ -17,7 +17,6 @@ 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
@ -32,6 +31,9 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.NavResult
import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.launch
import org.lsposed.lspatch.Constants
import org.lsposed.lspatch.R
@ -41,11 +43,10 @@ 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.page.SelectAppsResult
import org.lsposed.lspatch.ui.page.destinations.NewPatchScreenDestination
import org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination
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
@ -54,10 +55,12 @@ import java.io.IOException
private const val TAG = "AppManagePage"
@Composable
fun AppManageBody() {
fun AppManageBody(
navigator: DestinationsNavigator,
resultRecipient: ResultRecipient<SelectAppsScreenDestination, SelectAppsResult>
) {
val viewModel = viewModel<AppManageViewModel>()
val snackbarHost = LocalSnackbarHost.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
if (viewModel.appList.isEmpty()) {
@ -74,20 +77,18 @@ fun AppManageBody() {
}
} 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")
resultRecipient.onNavResult {
if (it is NavResult.Value) {
scope.launch {
val result = it.value as SelectAppsResult.MultipleApps
ConfigManager.getModulesForApp(scopeApp).forEach {
ConfigManager.deactivateModule(scopeApp, it)
}
selected.forEach {
result.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)
}
}
}
@ -165,13 +166,10 @@ fun AppManageBody() {
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) }
val initialSelected = LSPPackageManager.appList.mapNotNullTo(ArrayList()) {
if (activated.contains(it.app.packageName)) it.app.packageName else null
}
)
navController.navigate(PageList.SelectApps.name + "?multiSelect=true")
navigator.navigate(SelectAppsScreenDestination(true, initialSelected))
}
}
)
@ -220,10 +218,9 @@ fun AppManageBody() {
}
@Composable
fun AppManageFab() {
fun AppManageFab(navigator: DestinationsNavigator) {
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) }
@ -286,7 +283,7 @@ fun AppManageFab() {
title = {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.page_new_patch),
text = stringResource(R.string.screen_new_patch),
textAlign = TextAlign.Center
)
},
@ -296,7 +293,7 @@ fun AppManageFab() {
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),
onClick = {
navController.navigate(PageList.NewPatch.name + "?from=storage")
navigator.navigate(NewPatchScreenDestination("storage"))
showNewPatchDialog = false
}
) {
@ -310,7 +307,7 @@ fun AppManageFab() {
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),
onClick = {
navController.navigate(PageList.NewPatch.name + "?from=applist")
navigator.navigate(NewPatchScreenDestination("applist"))
showNewPatchDialog = false
}
) {

View File

@ -2,11 +2,6 @@ package org.lsposed.lspatch.ui.util
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController
val LocalNavController = compositionLocalOf<NavHostController> {
error("CompositionLocal LocalNavController not present")
}
val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {
error("CompositionLocal LocalSnackbarController not present")

View File

@ -1,25 +0,0 @@
package org.lsposed.lspatch.ui.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
fun <T> NavBackStackEntry.setState(key: String, value: T?) {
savedStateHandle.getLiveData<T>(key).value = value
}
@Composable
fun <T> NavBackStackEntry.observeState(key: String, initial: T? = null) =
savedStateHandle.getLiveData(key, initial).observeAsState()
fun NavController.navigateWithState(route: String) {
navigate(route) {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}

View File

@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
@ -22,16 +21,17 @@ class NewPatchViewModel : ViewModel() {
}
enum class PatchState {
SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR
INIT, SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR
}
sealed class ViewAction {
object DoneInit : ViewAction()
data class ConfigurePatch(val app: AppInfo) : ViewAction()
object SubmitPatch : ViewAction()
object LaunchPatch : ViewAction()
}
var patchState by mutableStateOf(PatchState.SELECTING)
var patchState by mutableStateOf(PatchState.INIT)
private set
var useManager by mutableStateOf(true)
@ -39,7 +39,7 @@ class NewPatchViewModel : ViewModel() {
var overrideVersionCode by mutableStateOf(false)
val sign = mutableStateListOf(false, true)
var sigBypassLevel by mutableStateOf(2)
var embeddedModules = SnapshotStateList<AppInfo>()
var embeddedModules = emptyList<AppInfo>()
lateinit var patchApp: AppInfo
private set
@ -68,12 +68,17 @@ class NewPatchViewModel : ViewModel() {
fun dispatch(action: ViewAction) {
when (action) {
is ViewAction.DoneInit -> doneInit()
is ViewAction.ConfigurePatch -> configurePatch(action.app)
is ViewAction.SubmitPatch -> submitPatch()
is ViewAction.LaunchPatch -> launchPatch()
}
}
private fun doneInit() {
patchState = PatchState.SELECTING
}
private fun configurePatch(app: AppInfo) {
Log.d(TAG, "Configuring patch for ${app.app.packageName}")
patchApp = app
@ -82,7 +87,7 @@ class NewPatchViewModel : ViewModel() {
private fun submitPatch() {
Log.d(TAG, "Submit patch")
if (useManager) embeddedModules.clear()
if (useManager) embeddedModules = emptyList()
patchOptions = Patcher.Options(
verbose = true,
config = PatchConfig(useManager, debuggable, overrideVersionCode, sign[0], sign[1], sigBypassLevel, null, null),

View File

@ -2,6 +2,7 @@ package org.lsposed.lspatch.ui.viewmodel
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
@ -26,6 +27,8 @@ class SelectAppsViewModel : ViewModel() {
var filteredList by mutableStateOf(listOf<AppInfo>())
private set
val multiSelected = mutableStateListOf<AppInfo>()
fun filterAppList(refresh: Boolean, filter: (AppInfo) -> Boolean) {
viewModelScope.launch {
if (LSPPackageManager.appList.isEmpty() || refresh) {

View File

@ -103,7 +103,7 @@ object LSPPackageManager {
}
}
var result: Intent? = null
suspendCoroutine<Unit> { cont ->
suspendCoroutine { cont ->
val countDownLatch = CountDownLatch(1)
val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent ->
result = intent
@ -133,7 +133,7 @@ object LSPPackageManager {
withContext(Dispatchers.IO) {
runCatching {
var result: Intent? = null
suspendCoroutine<Unit> { cont ->
suspendCoroutine { cont ->
val countDownLatch = CountDownLatch(1)
val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent ->
result = intent

View File

@ -9,10 +9,10 @@
<string name="modules">Modules</string>
<string name="shizuku_available">Shizuku service available</string>
<string name="shizuku_unavailable">Shizuku service not connected</string>
<string name="page_repo">Repo</string>
<string name="page_logs">Logs</string>
<string name="screen_repo">Repo</string>
<string name="screen_logs">Logs</string>
<!-- Home Page -->
<!-- Home Screen -->
<string name="home_shizuku_warning">Some functions unavailable</string>
<string name="home_api_version">API Version</string>
<string name="home_lspatch_version">LSPatch Version</string>
@ -25,8 +25,8 @@
<string name="home_description">LSPatch is a free non-root Xposed framework based on LSPosed core.</string>
<string name="home_view_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>
<!-- Manage Page -->
<string name="page_manage">Manage</string>
<!-- Manage Screen -->
<string name="screen_manage">Manage</string>
<string name="manage_loading">Loading</string>
<string name="manage_no_apps">No patched apps yet</string>
<string name="manage_update_loader">Update loader</string>
@ -39,8 +39,8 @@
<string name="manage_uninstall_successfully">Uninstall successfully</string>
<string name="manage_no_modules">No modules yet</string>
<!-- New Patch Page -->
<string name="page_new_patch">New Patch</string>
<!-- New Patch Screen -->
<string name="screen_new_patch">New Patch</string>
<string name="patch_select_dir_title">Select storage directory</string>
<string name="patch_select_dir_text">Select a directory to store the patched apks</string>
<string name="patch_select_dir_error">Error when setting storage directory</string>
@ -66,11 +66,11 @@
<string name="patch_install_successfully">Install successfully</string>
<string name="patch_install_failed">Install failed</string>
<!-- Select Apps Page -->
<string name="page_select_apps">Select Apps</string>
<!-- Select Apps Screen -->
<string name="screen_select_apps">Select Apps</string>
<!-- Settings Page -->
<string name="page_settings">Settings</string>
<!-- Settings Screen -->
<string name="screen_settings">Settings</string>
<string name="settings_keystore">Signature keystore</string>
<string name="settings_keystore_default">Built-in</string>
<string name="settings_keystore_custom">Custom</string>