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 variantLowered = variant.name.toLowerCase()
val variantCapped = variant.name.capitalize() val variantCapped = variant.name.capitalize()
kotlin.sourceSets {
getByName(variant.name) {
kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin")
}
}
task<Copy>("copy${variantCapped}Assets") { task<Copy>("copy${variantCapped}Assets") {
dependsOn(":appstub:copy$variantCapped") dependsOn(":appstub:copy$variantCapped")
dependsOn(":patch-loader:copy$variantCapped") dependsOn(":patch-loader:copy$variantCapped")
@ -75,6 +81,7 @@ dependencies {
implementation(projects.share.java) implementation(projects.share.java)
val roomVersion = "2.4.2" val roomVersion = "2.4.2"
val composeDestinationsVersion = "1.6.13-beta"
annotationProcessor("androidx.room:room-compiler:$roomVersion") annotationProcessor("androidx.room:room-compiler:$roomVersion")
compileOnly("dev.rikka.hidden:stub:2.3.1") compileOnly("dev.rikka.hidden:stub:2.3.1")
implementation("dev.rikka.hidden:compat: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.activity:activity-compose:1.6.0-alpha05")
implementation("androidx.compose.material:material-icons-extended:1.3.0-alpha01") implementation("androidx.compose.material:material-icons-extended:1.3.0-alpha01")
implementation("androidx.compose.material3:material3:1.0.0-alpha14") 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:1.3.0-alpha01")
implementation("androidx.compose.ui:ui-tooling:1.3.0-alpha01") implementation("androidx.compose.ui:ui-tooling:1.3.0-alpha01")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0")
@ -98,6 +104,8 @@ dependencies {
implementation("com.google.code.gson:gson:2.9.0") implementation("com.google.code.gson:gson:2.9.0")
implementation("dev.rikka.shizuku:api:12.1.0") implementation("dev.rikka.shizuku:api:12.1.0")
implementation("dev.rikka.shizuku:provider: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") implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3")
ksp("androidx.room:room-compiler:$roomVersion") 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.*
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.res.stringResource
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.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController 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.theme.LSPTheme
import org.lsposed.lspatch.ui.util.LocalNavController
import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.ui.util.LocalSnackbarHost
import org.lsposed.lspatch.ui.util.navigateWithState
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -27,25 +30,18 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
var mainPage by rememberSaveable { mutableStateOf(PageList.Home) }
LSPTheme { LSPTheme {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
CompositionLocalProvider( CompositionLocalProvider(LocalSnackbarHost provides snackbarHostState) {
LocalNavController provides navController,
LocalSnackbarHost provides snackbarHostState
) {
Scaffold( Scaffold(
bottomBar = { bottomBar = { BottomBar(navController) },
MainNavigationBar(mainPage) {
if (mainPage == it) return@MainNavigationBar
mainPage = it
navController.navigateWithState(it.name)
}
},
snackbarHost = { SnackbarHost(snackbarHostState) } snackbarHost = { SnackbarHost(snackbarHostState) }
) { innerPadding -> ) { innerPadding ->
MainNavHost(navController, Modifier.padding(innerPadding)) DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root,
navController = navController
)
} }
} }
} }
@ -54,30 +50,34 @@ class MainActivity : ComponentActivity() {
} }
@Composable @Composable
private fun MainNavHost(navController: NavHostController, modifier: Modifier) { private fun BottomBar(navController: NavHostController) {
NavHost( val currentDestination: Destination = navController.appCurrentDestinationAsState().value
navController = navController, ?: NavGraphs.root.startAppDestination
startDestination = PageList.Home.name, var topDestination by rememberSaveable { mutableStateOf(currentDestination.route) }
modifier = modifier LaunchedEffect(currentDestination) {
) { val queue = navController.backQueue
for (page in PageList.values()) { if (queue.size == 2) topDestination = queue[1].destination.route!!
composable(route = page.route, arguments = page.arguments, content = page.body) else if (queue.size > 2) topDestination = queue[2].destination.route!!
}
} }
}
@Composable
private fun MainNavigationBar(page: PageList, onClick: (PageList) -> Unit) {
NavigationBar(tonalElevation = 8.dp) { NavigationBar(tonalElevation = 8.dp) {
arrayOf(PageList.Repo, PageList.Manage, PageList.Home, PageList.Logs, PageList.Settings).forEach { BottomBarDestination.values().forEach { destination ->
NavigationBarItem( NavigationBarItem(
selected = page == it, selected = topDestination == destination.direction.route,
onClick = { onClick(it) }, onClick = {
icon = { navController.navigate(destination.direction.route) {
if (page == it) Icon(it.iconSelected!!, it.title) popUpTo(navController.graph.findStartDestination().id) {
else Icon(it.iconNotSelected!!, it.title) 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 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
private const val TAG = "SearchBar" private const val TAG = "SearchBar"
@OptIn(ExperimentalComposeUiApi::class) @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.text.font.FontWeight
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 com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.share.LSPConfig import org.lsposed.lspatch.share.LSPConfig
@ -34,8 +36,10 @@ import org.lsposed.lspatch.util.ShizukuApi
import rikka.shizuku.Shizuku import rikka.shizuku.Shizuku
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@RootNavGraph(start = true)
@Destination
@Composable @Composable
fun HomePage() { fun HomeScreen() {
Scaffold(topBar = { TopBar() }) { innerPadding -> Scaffold(topBar = { TopBar() }) { innerPadding ->
Column( Column(
modifier = Modifier modifier = Modifier

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.config.MyKeyStore import org.lsposed.lspatch.config.MyKeyStore
@ -31,8 +32,9 @@ import java.security.GeneralSecurityException
import java.security.KeyStore import java.security.KeyStore
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Destination
@Composable @Composable
fun SettingsPage() { fun SettingsScreen() {
Scaffold( Scaffold(
topBar = { TopBar() } topBar = { TopBar() }
) { innerPadding -> ) { innerPadding ->
@ -49,7 +51,7 @@ fun SettingsPage() {
@Composable @Composable
private fun TopBar() { private fun TopBar() {
SmallTopAppBar( 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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
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
@ -32,6 +31,9 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.viewmodel.compose.viewModel 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 kotlinx.coroutines.launch
import org.lsposed.lspatch.Constants import org.lsposed.lspatch.Constants
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
@ -41,11 +43,10 @@ import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.share.LSPConfig import org.lsposed.lspatch.share.LSPConfig
import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.component.AppItem
import org.lsposed.lspatch.ui.component.LoadingDialog import org.lsposed.lspatch.ui.component.LoadingDialog
import org.lsposed.lspatch.ui.page.PageList import org.lsposed.lspatch.ui.page.SelectAppsResult
import org.lsposed.lspatch.ui.util.LocalNavController 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.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.ui.viewmodel.manage.AppManageViewModel
import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager
import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.lspatch.util.ShizukuApi
@ -54,10 +55,12 @@ import java.io.IOException
private const val TAG = "AppManagePage" private const val TAG = "AppManagePage"
@Composable @Composable
fun AppManageBody() { fun AppManageBody(
navigator: DestinationsNavigator,
resultRecipient: ResultRecipient<SelectAppsScreenDestination, SelectAppsResult>
) {
val viewModel = viewModel<AppManageViewModel>() val viewModel = viewModel<AppManageViewModel>()
val snackbarHost = LocalSnackbarHost.current val snackbarHost = LocalSnackbarHost.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
if (viewModel.appList.isEmpty()) { if (viewModel.appList.isEmpty()) {
@ -74,20 +77,18 @@ fun AppManageBody() {
} }
} else { } else {
var scopeApp by rememberSaveable { mutableStateOf("") } var scopeApp by rememberSaveable { mutableStateOf("") }
val isCancelled by navController.currentBackStackEntry!!.observeState<Boolean>("isCancelled") resultRecipient.onNavResult {
LaunchedEffect(isCancelled) { if (it is NavResult.Value) {
if (isCancelled == false) { scope.launch {
val selected = navController.currentBackStackEntry!! val result = it.value as SelectAppsResult.MultipleApps
.savedStateHandle.getLiveData<SnapshotStateList<LSPPackageManager.AppInfo>>("selected").value!!.toSet() ConfigManager.getModulesForApp(scopeApp).forEach {
Log.d(TAG, "Clear module list for $scopeApp") ConfigManager.deactivateModule(scopeApp, it)
ConfigManager.getModulesForApp(scopeApp).forEach { }
ConfigManager.deactivateModule(scopeApp, it) result.selected.forEach {
Log.d(TAG, "Activate ${it.app.packageName} for $scopeApp")
ConfigManager.activateModule(scopeApp, Module(it.app.packageName, it.app.sourceDir))
}
} }
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 { scope.launch {
scopeApp = it.first.app.packageName scopeApp = it.first.app.packageName
val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet() val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet()
navController.currentBackStackEntry!!.setState( val initialSelected = LSPPackageManager.appList.mapNotNullTo(ArrayList()) {
"selected", if (activated.contains(it.app.packageName)) it.app.packageName else null
SnapshotStateList<LSPPackageManager.AppInfo>().apply { }
LSPPackageManager.appList.filterTo(this) { activated.contains(it.app.packageName) } navigator.navigate(SelectAppsScreenDestination(true, initialSelected))
}
)
navController.navigate(PageList.SelectApps.name + "?multiSelect=true")
} }
} }
) )
@ -220,10 +218,9 @@ fun AppManageBody() {
} }
@Composable @Composable
fun AppManageFab() { fun AppManageFab(navigator: DestinationsNavigator) {
val context = LocalContext.current val context = LocalContext.current
val snackbarHost = LocalSnackbarHost.current val snackbarHost = LocalSnackbarHost.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var shouldSelectDirectory by remember { mutableStateOf(false) } var shouldSelectDirectory by remember { mutableStateOf(false) }
var showNewPatchDialog by remember { mutableStateOf(false) } var showNewPatchDialog by remember { mutableStateOf(false) }
@ -286,7 +283,7 @@ fun AppManageFab() {
title = { title = {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.page_new_patch), text = stringResource(R.string.screen_new_patch),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
}, },
@ -296,7 +293,7 @@ fun AppManageFab() {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),
onClick = { onClick = {
navController.navigate(PageList.NewPatch.name + "?from=storage") navigator.navigate(NewPatchScreenDestination("storage"))
showNewPatchDialog = false showNewPatchDialog = false
} }
) { ) {
@ -310,7 +307,7 @@ fun AppManageFab() {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),
onClick = { onClick = {
navController.navigate(PageList.NewPatch.name + "?from=applist") navigator.navigate(NewPatchScreenDestination("applist"))
showNewPatchDialog = false showNewPatchDialog = false
} }
) { ) {

View File

@ -2,11 +2,6 @@ package org.lsposed.lspatch.ui.util
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController
val LocalNavController = compositionLocalOf<NavHostController> {
error("CompositionLocal LocalNavController not present")
}
val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> { val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {
error("CompositionLocal LocalSnackbarController not present") 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.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -22,16 +21,17 @@ class NewPatchViewModel : ViewModel() {
} }
enum class PatchState { enum class PatchState {
SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR INIT, SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR
} }
sealed class ViewAction { sealed class ViewAction {
object DoneInit : ViewAction()
data class ConfigurePatch(val app: AppInfo) : ViewAction() data class ConfigurePatch(val app: AppInfo) : ViewAction()
object SubmitPatch : ViewAction() object SubmitPatch : ViewAction()
object LaunchPatch : ViewAction() object LaunchPatch : ViewAction()
} }
var patchState by mutableStateOf(PatchState.SELECTING) var patchState by mutableStateOf(PatchState.INIT)
private set private set
var useManager by mutableStateOf(true) var useManager by mutableStateOf(true)
@ -39,7 +39,7 @@ class NewPatchViewModel : ViewModel() {
var overrideVersionCode by mutableStateOf(false) var overrideVersionCode by mutableStateOf(false)
val sign = mutableStateListOf(false, true) val sign = mutableStateListOf(false, true)
var sigBypassLevel by mutableStateOf(2) var sigBypassLevel by mutableStateOf(2)
var embeddedModules = SnapshotStateList<AppInfo>() var embeddedModules = emptyList<AppInfo>()
lateinit var patchApp: AppInfo lateinit var patchApp: AppInfo
private set private set
@ -68,12 +68,17 @@ class NewPatchViewModel : ViewModel() {
fun dispatch(action: ViewAction) { fun dispatch(action: ViewAction) {
when (action) { when (action) {
is ViewAction.DoneInit -> doneInit()
is ViewAction.ConfigurePatch -> configurePatch(action.app) is ViewAction.ConfigurePatch -> configurePatch(action.app)
is ViewAction.SubmitPatch -> submitPatch() is ViewAction.SubmitPatch -> submitPatch()
is ViewAction.LaunchPatch -> launchPatch() is ViewAction.LaunchPatch -> launchPatch()
} }
} }
private fun doneInit() {
patchState = PatchState.SELECTING
}
private fun configurePatch(app: AppInfo) { private fun configurePatch(app: AppInfo) {
Log.d(TAG, "Configuring patch for ${app.app.packageName}") Log.d(TAG, "Configuring patch for ${app.app.packageName}")
patchApp = app patchApp = app
@ -82,7 +87,7 @@ class NewPatchViewModel : ViewModel() {
private fun submitPatch() { private fun submitPatch() {
Log.d(TAG, "Submit patch") Log.d(TAG, "Submit patch")
if (useManager) embeddedModules.clear() if (useManager) embeddedModules = emptyList()
patchOptions = Patcher.Options( patchOptions = Patcher.Options(
verbose = true, verbose = true,
config = PatchConfig(useManager, debuggable, overrideVersionCode, sign[0], sign[1], sigBypassLevel, null, null), 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 android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -26,6 +27,8 @@ class SelectAppsViewModel : ViewModel() {
var filteredList by mutableStateOf(listOf<AppInfo>()) var filteredList by mutableStateOf(listOf<AppInfo>())
private set private set
val multiSelected = mutableStateListOf<AppInfo>()
fun filterAppList(refresh: Boolean, filter: (AppInfo) -> Boolean) { fun filterAppList(refresh: Boolean, filter: (AppInfo) -> Boolean) {
viewModelScope.launch { viewModelScope.launch {
if (LSPPackageManager.appList.isEmpty() || refresh) { if (LSPPackageManager.appList.isEmpty() || refresh) {

View File

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

View File

@ -9,10 +9,10 @@
<string name="modules">Modules</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="screen_repo">Repo</string>
<string name="page_logs">Logs</string> <string name="screen_logs">Logs</string>
<!-- Home Page --> <!-- Home Screen -->
<string name="home_shizuku_warning">Some functions unavailable</string> <string name="home_shizuku_warning">Some functions unavailable</string>
<string name="home_api_version">API Version</string> <string name="home_api_version">API Version</string>
<string name="home_lspatch_version">LSPatch 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_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> <string name="home_view_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>
<!-- Manage Page --> <!-- Manage Screen -->
<string name="page_manage">Manage</string> <string name="screen_manage">Manage</string>
<string name="manage_loading">Loading</string> <string name="manage_loading">Loading</string>
<string name="manage_no_apps">No patched apps yet</string> <string name="manage_no_apps">No patched apps yet</string>
<string name="manage_update_loader">Update loader</string> <string name="manage_update_loader">Update loader</string>
@ -39,8 +39,8 @@
<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> <string name="manage_no_modules">No modules yet</string>
<!-- New Patch Page --> <!-- New Patch Screen -->
<string name="page_new_patch">New Patch</string> <string name="screen_new_patch">New Patch</string>
<string name="patch_select_dir_title">Select storage directory</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_text">Select a directory to store the patched apks</string>
<string name="patch_select_dir_error">Error when setting storage directory</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_successfully">Install successfully</string>
<string name="patch_install_failed">Install failed</string> <string name="patch_install_failed">Install failed</string>
<!-- Select Apps Page --> <!-- Select Apps Screen -->
<string name="page_select_apps">Select Apps</string> <string name="screen_select_apps">Select Apps</string>
<!-- Settings Page --> <!-- Settings Screen -->
<string name="page_settings">Settings</string> <string name="screen_settings">Settings</string>
<string name="settings_keystore">Signature keystore</string> <string name="settings_keystore">Signature keystore</string>
<string name="settings_keystore_default">Built-in</string> <string name="settings_keystore_default">Built-in</string>
<string name="settings_keystore_custom">Custom</string> <string name="settings_keystore_custom">Custom</string>