diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/AnywhereDropdown.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/AnywhereDropdown.kt new file mode 100644 index 0000000..25c7d82 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/AnywhereDropdown.kt @@ -0,0 +1,64 @@ +package org.lsposed.lspatch.ui.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.DropdownMenu +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.DpOffset + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AnywhereDropdown( + modifier: Modifier = Modifier, + enabled: Boolean = true, + expanded: Boolean, + onDismissRequest: () -> Unit, + onClick: () -> Unit, + onLongClick: (() -> Unit)? = null, + surface: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit +) { + val indication = LocalIndication.current + val interactionSource = remember { MutableInteractionSource() } + val state by interactionSource.interactions.collectAsState(null) + var offset by remember { mutableStateOf(Offset.Zero) } + val dpOffset = with(LocalDensity.current) { + DpOffset(offset.x.toDp(), offset.y.toDp()) + } + + LaunchedEffect(state) { + if (state is PressInteraction.Release) { + val i = state as PressInteraction.Release + offset = i.press.pressPosition + } + } + + Box( + modifier = modifier + .combinedClickable( + interactionSource = interactionSource, + indication = indication, + enabled = enabled, + onClick = onClick, + onLongClick = onLongClick + ) + ) { + surface() + Box { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + offset = dpOffset, + content = content + ) + } + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt index 39c2099..07bccf8 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt @@ -2,8 +2,6 @@ package org.lsposed.lspatch.ui.component import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForwardIos @@ -19,15 +17,13 @@ import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter import org.lsposed.lspatch.ui.theme.LSPTheme -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AppItem( modifier: Modifier = Modifier, icon: Drawable, label: String, packageName: String, - onClick: () -> Unit, - onLongClick: (() -> Unit)? = null, checked: Boolean? = null, rightIcon: (@Composable () -> Unit)? = null, additionalContent: (@Composable ColumnScope.() -> Unit)? = null, @@ -37,10 +33,6 @@ fun AppItem( Column( modifier = modifier .fillMaxWidth() - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick - ) .padding(20.dp) ) { Row( @@ -68,7 +60,7 @@ fun AppItem( if (checked != null) { Checkbox( checked = checked, - onCheckedChange = { onClick() }, + onCheckedChange = null, modifier = Modifier.padding(start = 20.dp) ) } @@ -90,7 +82,6 @@ private fun AppItemPreview() { icon = shape, label = "Sample App", packageName = "org.lsposed.sample", - onClick = {}, rightIcon = { Icon(Icons.Filled.ArrowForwardIos, null) } ) } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt index e0d828b..dd6ab40 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt @@ -1,5 +1,6 @@ package org.lsposed.lspatch.ui.component.settings +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.icons.Icons @@ -17,14 +18,13 @@ fun SettingsCheckBox( modifier: Modifier = Modifier, checked: Boolean, enabled: Boolean = true, - onClick: () -> Unit, icon: ImageVector? = null, title: String, desc: String? = null, extraContent: (@Composable ColumnScope.() -> Unit)? = null ) { - SettingsSlot(modifier, enabled, onClick, icon, title, desc, extraContent) { - Checkbox(checked = checked, onCheckedChange = { onClick() }) + SettingsSlot(modifier, enabled, icon, title, desc, extraContent) { + Checkbox(checked = checked, onCheckedChange = null) } } @@ -35,14 +35,14 @@ private fun SettingsCheckBoxPreview() { var checked2 by remember { mutableStateOf(false) } Column { SettingsCheckBox( + modifier = Modifier.clickable { checked1 = !checked1 }, checked = checked1, - onClick = { checked1 = !checked1 }, title = "Title", desc = "Description" ) SettingsCheckBox( + modifier = Modifier.clickable { checked2 = !checked2 }, checked = checked2, - onClick = { checked2 = !checked2 }, icon = Icons.Outlined.Api, title = "Title", desc = "Description" diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt index 28a35bb..5196356 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt @@ -1,6 +1,5 @@ package org.lsposed.lspatch.ui.component.settings -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -16,22 +15,16 @@ import androidx.compose.ui.unit.dp fun SettingsSlot( modifier: Modifier, enabled: Boolean, - onClick: () -> Unit, icon: ImageVector? = null, title: String, desc: String?, extraContent: (@Composable ColumnScope.() -> Unit)? = null, action: (@Composable RowScope.() -> Unit)?, ) { - val enabledModifier = - if (enabled) Modifier - .alpha(1f) - .clickable(onClick = onClick) - else Modifier.alpha(0.5f) Row( modifier = modifier .fillMaxWidth() - .then(enabledModifier) + .alpha(if (enabled) 1f else 0.5f) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically @@ -71,9 +64,8 @@ fun SettingsSlot( fun SettingsItem( modifier: Modifier = Modifier, enabled: Boolean = true, - onClick: () -> Unit, icon: ImageVector? = null, title: String, desc: String? = null, extraContent: (@Composable ColumnScope.() -> Unit)? = null -) = SettingsSlot(modifier, enabled, onClick, icon, title, desc, extraContent, null) +) = SettingsSlot(modifier, enabled, icon, title, desc, extraContent, null) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Switch.kt b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Switch.kt index 43ffd49..7375551 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Switch.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Switch.kt @@ -16,14 +16,13 @@ fun SettingsSwitch( modifier: Modifier = Modifier, checked: Boolean, enabled: Boolean = true, - onClick: () -> Unit, icon: ImageVector? = null, title: String, desc: String? = null, extraContent: (@Composable ColumnScope.() -> Unit)? = null ) { - SettingsSlot(modifier.clickable(onClick = onClick), enabled, onClick, icon, title, desc, extraContent) { - Switch(checked = checked, onCheckedChange = { onClick() }) + SettingsSlot(modifier, enabled, icon, title, desc, extraContent) { + Switch(checked = checked, onCheckedChange = null) } } @@ -34,14 +33,14 @@ private fun SettingsCheckBoxPreview() { var checked2 by remember { mutableStateOf(false) } Column { SettingsSwitch( + modifier = Modifier.clickable { checked1 = !checked1 }, checked = checked1, - onClick = { checked1 = !checked1 }, title = "Title", desc = "Description" ) SettingsSwitch( + modifier = Modifier.clickable { checked2 = !checked2 }, checked = checked2, - onClick = { checked2 = !checked2 }, icon = Icons.Outlined.Api, title = "Title", desc = "Description" diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt index e68df35..f75e2fe 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -39,6 +40,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.lsposed.lspatch.R import org.lsposed.lspatch.lspApp +import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.SelectionColumn import org.lsposed.lspatch.ui.component.ShimmerAnimation import org.lsposed.lspatch.ui.component.settings.SettingsCheckBox @@ -219,64 +221,74 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { ) } SettingsCheckBox( - modifier = Modifier.padding(top = 6.dp), + modifier = Modifier + .padding(top = 6.dp) + .clickable { viewModel.debuggable = !viewModel.debuggable }, checked = viewModel.debuggable, - onClick = { viewModel.debuggable = !viewModel.debuggable }, icon = Icons.Outlined.BugReport, title = stringResource(R.string.patch_debuggable) ) SettingsCheckBox( + modifier = Modifier.clickable { viewModel.overrideVersionCode = !viewModel.overrideVersionCode }, checked = viewModel.overrideVersionCode, - onClick = { viewModel.overrideVersionCode = !viewModel.overrideVersionCode }, icon = Icons.Outlined.Layers, title = stringResource(R.string.patch_override_version_code), desc = stringResource(R.string.patch_override_version_code_desc) ) - Box { - var expanded by remember { mutableStateOf(false) } - SettingsItem( - onClick = { expanded = true }, - icon = Icons.Outlined.Edit, - title = stringResource(R.string.patch_sign), - desc = viewModel.sign.mapIndexedNotNull { index, on -> if (on) "V" + (index + 1) else null }.joinToString(" + ").ifEmpty { "None" } - ) - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - repeat(2) { index -> - DropdownMenuItem( - text = { - Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox(checked = viewModel.sign[index], onCheckedChange = { viewModel.sign[index] = !viewModel.sign[index] }) - Text("V" + (index + 1)) - } - }, - onClick = { viewModel.sign[index] = !viewModel.sign[index] } - ) - } + var signExpanded by remember { mutableStateOf(false) } + AnywhereDropdown( + expanded = signExpanded, + onDismissRequest = { signExpanded = false }, + onClick = { signExpanded = true }, + surface = { + SettingsItem( + icon = Icons.Outlined.Edit, + title = stringResource(R.string.patch_sign), + desc = viewModel.sign + .mapIndexedNotNull { index, on -> if (on) "V" + (index + 1) else null } + .joinToString(" + ") + .ifEmpty { "None" } + ) + } + ) { + repeat(2) { index -> + DropdownMenuItem( + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = viewModel.sign[index], onCheckedChange = { viewModel.sign[index] = !viewModel.sign[index] }) + Text("V" + (index + 1)) + } + }, + onClick = { viewModel.sign[index] = !viewModel.sign[index] } + ) } } - Box { - var expanded by remember { mutableStateOf(false) } - SettingsItem( - onClick = { expanded = true }, - icon = Icons.Outlined.RemoveModerator, - title = stringResource(R.string.patch_sigbypass), - desc = sigBypassLvStr(viewModel.sigBypassLevel) - ) - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - repeat(3) { - DropdownMenuItem( - text = { - Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton(selected = viewModel.sigBypassLevel == it, onClick = { viewModel.sigBypassLevel = it }) - Text(sigBypassLvStr(it)) - } - }, - onClick = { - viewModel.sigBypassLevel = it - expanded = false + var bypassExpanded by remember { mutableStateOf(false) } + AnywhereDropdown( + expanded = bypassExpanded, + onDismissRequest = { bypassExpanded = false }, + onClick = { bypassExpanded = true }, + surface = { + SettingsItem( + icon = Icons.Outlined.RemoveModerator, + title = stringResource(R.string.patch_sigbypass), + desc = sigBypassLvStr(viewModel.sigBypassLevel) + ) + } + ) { + repeat(3) { + DropdownMenuItem( + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton(selected = viewModel.sigBypassLevel == it, onClick = { viewModel.sigBypassLevel = it }) + Text(sigBypassLvStr(it)) } - ) - } + }, + onClick = { + viewModel.sigBypassLevel = it + bypassExpanded = false + } + ) } } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsScreen.kt index ffe74f5..25721bb 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsScreen.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -124,11 +125,12 @@ private fun SingleSelect(onSelect: (AppInfo) -> Unit) { key = { it.app.packageName } ) { AppItem( - modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), + modifier = Modifier + .animateItemPlacement(spring(stiffness = Spring.StiffnessLow)) + .clickable { onSelect(it) }, icon = LSPPackageManager.getIcon(it), label = it.label, - packageName = it.app.packageName, - onClick = { onSelect(it) } + packageName = it.app.packageName ) } } @@ -145,14 +147,15 @@ private fun MultiSelect() { ) { val checked = viewModel.multiSelected.contains(it) AppItem( - modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), + modifier = Modifier + .animateItemPlacement(spring(stiffness = Spring.StiffnessLow)) + .clickable { + if (checked) viewModel.multiSelected.remove(it) + else viewModel.multiSelected.add(it) + }, icon = LSPPackageManager.getIcon(it), label = it.label, packageName = it.app.packageName, - onClick = { - if (checked) viewModel.multiSelected.remove(it) - else viewModel.multiSelected.add(it) - }, checked = checked ) } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsScreen.kt index ff34e05..4542dcb 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsScreen.kt @@ -2,9 +2,9 @@ package org.lsposed.lspatch.ui.page import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -27,6 +27,7 @@ import kotlinx.coroutines.launch import org.lsposed.lspatch.R import org.lsposed.lspatch.config.Configs import org.lsposed.lspatch.config.MyKeyStore +import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.CenterTopBar import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.component.settings.SettingsSwitch @@ -56,32 +57,35 @@ fun SettingsScreen() { private fun KeyStore() { val context = LocalContext.current val scope = rememberCoroutineScope() - var dropdownExpanded by remember { mutableStateOf(false) } + var expanded by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) } - Box { - SettingsItem( - onClick = { dropdownExpanded = !dropdownExpanded }, - icon = Icons.Outlined.Ballot, - title = stringResource(R.string.settings_keystore), - desc = stringResource(if (MyKeyStore.useDefault) R.string.settings_keystore_default else R.string.settings_keystore_custom) - ) - DropdownMenu(expanded = dropdownExpanded, onDismissRequest = { dropdownExpanded = false }) { - DropdownMenuItem( - text = { Text(stringResource(R.string.settings_keystore_default)) }, - onClick = { - scope.launch { MyKeyStore.reset() } - dropdownExpanded = false - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.settings_keystore_custom)) }, - onClick = { - dropdownExpanded = false - showDialog = true - } + AnywhereDropdown( + expanded = expanded, + onDismissRequest = { expanded = false }, + onClick = { expanded = true }, + surface = { + SettingsItem( + icon = Icons.Outlined.Ballot, + title = stringResource(R.string.settings_keystore), + desc = stringResource(if (MyKeyStore.useDefault) R.string.settings_keystore_default else R.string.settings_keystore_custom) ) } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.settings_keystore_default)) }, + onClick = { + scope.launch { MyKeyStore.reset() } + expanded = false + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.settings_keystore_custom)) }, + onClick = { + expanded = false + showDialog = true + } + ) } if (showDialog) { @@ -106,7 +110,7 @@ private fun KeyStore() { } AlertDialog( - onDismissRequest = { dropdownExpanded = false; showDialog = false }, + onDismissRequest = { expanded = false; showDialog = false }, confirmButton = { TextButton( content = { Text(stringResource(android.R.string.ok)) }, @@ -144,14 +148,14 @@ private fun KeyStore() { } scope.launch { MyKeyStore.setCustom(password, alias, aliasPassword) } - dropdownExpanded = false + expanded = false showDialog = false }) }, dismissButton = { TextButton( content = { Text(stringResource(android.R.string.cancel)) }, - onClick = { dropdownExpanded = false; showDialog = false } + onClick = { expanded = false; showDialog = false } ) }, title = { @@ -228,8 +232,8 @@ private fun KeyStore() { @Composable private fun DetailPatchLogs() { SettingsSwitch( + modifier = Modifier.clickable { Configs.detailPatchLogs = !Configs.detailPatchLogs }, checked = Configs.detailPatchLogs, - onClick = { Configs.detailPatchLogs = !Configs.detailPatchLogs }, title = stringResource(R.string.settings_detail_patch_logs) ) } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt index 8cee268..39d2fa1 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt @@ -42,6 +42,7 @@ import org.lsposed.lspatch.config.Configs import org.lsposed.lspatch.database.entity.Module import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.share.LSPConfig +import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.component.LoadingDialog import org.lsposed.lspatch.ui.page.SelectAppsResult @@ -49,6 +50,7 @@ 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.viewmodel.manage.AppManageViewModel +import org.lsposed.lspatch.ui.viewstate.ProcessingState import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.ShizukuApi import java.io.IOException @@ -93,22 +95,39 @@ fun AppManageBody( } } - if (viewModel.processingUpdate) LoadingDialog() - viewModel.updateLoaderResult?.let { - val updateSuccessfully = stringResource(R.string.manage_update_loader_successfully) - val updateFailed = stringResource(R.string.manage_update_loader_failed) - val copyError = stringResource(R.string.copy_error) - LaunchedEffect(Unit) { - it.onSuccess { - snackbarHost.showSnackbar(updateSuccessfully) - }.onFailure { - val result = snackbarHost.showSnackbar(updateFailed, copyError) - if (result == SnackbarResult.ActionPerformed) { - val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cm.setPrimaryClip(ClipData.newPlainText("LSPatch", it.toString())) + when (viewModel.updateLoaderState) { + is ProcessingState.Idle -> Unit + is ProcessingState.Processing -> LoadingDialog() + is ProcessingState.Done -> { + val it = viewModel.updateLoaderState as ProcessingState.Done + val updateSuccessfully = stringResource(R.string.manage_update_loader_successfully) + val updateFailed = stringResource(R.string.manage_update_loader_failed) + val copyError = stringResource(R.string.copy_error) + LaunchedEffect(Unit) { + it.result.onSuccess { + snackbarHost.showSnackbar(updateSuccessfully) + }.onFailure { + val result = snackbarHost.showSnackbar(updateFailed, copyError) + if (result == SnackbarResult.ActionPerformed) { + val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText("LSPatch", it.toString())) + } } + viewModel.dispatch(AppManageViewModel.ViewAction.ClearUpdateLoaderResult) + } + } + } + when (viewModel.optimizeState) { + is ProcessingState.Idle -> Unit + is ProcessingState.Processing -> LoadingDialog() + is ProcessingState.Done -> { + val it = viewModel.optimizeState as ProcessingState.Done + val optimizeSucceed = stringResource(R.string.manage_optimize_successfully) + val optimizeFailed = stringResource(R.string.manage_optimize_failed) + LaunchedEffect(Unit) { + snackbarHost.showSnackbar(if (it.result) optimizeSucceed else optimizeFailed) + viewModel.dispatch(AppManageViewModel.ViewAction.ClearOptimizeResult) } - viewModel.dispatch(AppManageViewModel.ViewAction.ClearUpdateLoaderResult) } } @@ -118,98 +137,97 @@ fun AppManageBody( key = { it.first.app.packageName } ) { var expanded by remember { mutableStateOf(false) } - Box { - AppItem( - icon = LSPPackageManager.getIcon(it.first), - label = it.first.label, - packageName = it.first.app.packageName, - onClick = { expanded = true }, - onLongClick = { expanded = true }, - additionalContent = { - Text( - text = buildAnnotatedString { - val (text, color) = - if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary - else stringResource(R.string.patch_portable) to MaterialTheme.colorScheme.tertiary - append(AnnotatedString(text, SpanStyle(color = color))) - append(" ") - append(it.second.lspConfig.VERSION_CODE.toString()) - }, - fontWeight = FontWeight.SemiBold, - fontFamily = FontFamily.Serif, - style = MaterialTheme.typography.bodySmall - ) - } - ) - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - val shizukuUnavailable = stringResource(R.string.shizuku_unavailable) - if (it.second.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE || BuildConfig.DEBUG) { - DropdownMenuItem( - text = { Text(stringResource(R.string.manage_update_loader)) }, - onClick = { - expanded = false - scope.launch { - if (!ShizukuApi.isPermissionGranted) { - snackbarHost.showSnackbar(shizukuUnavailable) - } else { - viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second)) - } - } - } - ) - } - if (it.second.useManager) { - DropdownMenuItem( - text = { Text(stringResource(R.string.manage_module_scope)) }, - onClick = { - expanded = false - scope.launch { - scopeApp = it.first.app.packageName - val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet() - val initialSelected = LSPPackageManager.appList.mapNotNullTo(ArrayList()) { - if (activated.contains(it.app.packageName)) it.app.packageName else null - } - navigator.navigate(SelectAppsScreenDestination(true, initialSelected)) - } - } - ) - } - val optimizeSucceed = stringResource(R.string.manage_optimize_successfully) - val optimizeFailed = stringResource(R.string.manage_optimize_failed) + AnywhereDropdown( + expanded = expanded, + onDismissRequest = { expanded = false }, + onClick = { expanded = true }, + surface = { + AppItem( + icon = LSPPackageManager.getIcon(it.first), + label = it.first.label, + packageName = it.first.app.packageName, + additionalContent = { + Text( + text = buildAnnotatedString { + val (text, color) = + if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary + else stringResource(R.string.patch_portable) to MaterialTheme.colorScheme.tertiary + append(AnnotatedString(text, SpanStyle(color = color))) + append(" ") + append(it.second.lspConfig.VERSION_CODE.toString()) + }, + fontWeight = FontWeight.SemiBold, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.bodySmall + ) + } + ) + } + ) { + val shizukuUnavailable = stringResource(R.string.shizuku_unavailable) + if (it.second.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE || BuildConfig.DEBUG) { DropdownMenuItem( - text = { Text(stringResource(R.string.manage_optimize)) }, + text = { Text(stringResource(R.string.manage_update_loader)) }, onClick = { expanded = false scope.launch { if (!ShizukuApi.isPermissionGranted) { snackbarHost.showSnackbar(shizukuUnavailable) } else { - val result = ShizukuApi.performDexOptMode(it.first.app.packageName) - snackbarHost.showSnackbar(if (result) optimizeSucceed else optimizeFailed) + viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second)) } } } ) - val uninstallSuccessfully = stringResource(R.string.manage_uninstall_successfully) - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - scope.launch { - snackbarHost.showSnackbar(uninstallSuccessfully) - } - } - } + } + if (it.second.useManager) { DropdownMenuItem( - text = { Text(stringResource(R.string.uninstall)) }, + text = { Text(stringResource(R.string.manage_module_scope)) }, onClick = { expanded = false - val intent = Intent(Intent.ACTION_DELETE).apply { - data = Uri.parse("package:${it.first.app.packageName}") - putExtra(Intent.EXTRA_RETURN_RESULT, true) + scope.launch { + scopeApp = it.first.app.packageName + val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet() + val initialSelected = LSPPackageManager.appList.mapNotNullTo(ArrayList()) { + if (activated.contains(it.app.packageName)) it.app.packageName else null + } + navigator.navigate(SelectAppsScreenDestination(true, initialSelected)) } - launcher.launch(intent) } ) } + DropdownMenuItem( + text = { Text(stringResource(R.string.manage_optimize)) }, + onClick = { + expanded = false + scope.launch { + if (!ShizukuApi.isPermissionGranted) { + snackbarHost.showSnackbar(shizukuUnavailable) + } else { + viewModel.dispatch(AppManageViewModel.ViewAction.PerformOptimize(it.first)) + } + } + } + ) + val uninstallSuccessfully = stringResource(R.string.manage_uninstall_successfully) + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + scope.launch { + snackbarHost.showSnackbar(uninstallSuccessfully) + } + } + } + DropdownMenuItem( + text = { Text(stringResource(R.string.uninstall)) }, + onClick = { + expanded = false + val intent = Intent(Intent.ACTION_DELETE).apply { + data = Uri.parse("package:${it.first.app.packageName}") + putExtra(Intent.EXTRA_RETURN_RESULT, true) + } + launcher.launch(intent) + } + ) } } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/ModuleManagePage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/ModuleManagePage.kt index a0bcc87..c69da44 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/ModuleManagePage.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/manage/ModuleManagePage.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.lifecycle.viewmodel.compose.viewModel import org.lsposed.lspatch.R +import org.lsposed.lspatch.ui.component.AnywhereDropdown import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.viewmodel.manage.ModuleManageViewModel import org.lsposed.lspatch.util.LSPPackageManager @@ -44,30 +45,36 @@ fun ModuleManageBody() { key = { it.first.app.packageName } ) { var expanded by remember { mutableStateOf(false) } - Box { - AppItem( - icon = LSPPackageManager.getIcon(it.first), - label = it.first.label, - packageName = it.first.app.packageName, - onClick = { /* TODO: startAndSendModuleBinder */ }, - onLongClick = { expanded = true }, - additionalContent = { - Text( - text = it.second.description, - style = MaterialTheme.typography.bodySmall - ) - Text( - text = buildAnnotatedString { - append(AnnotatedString("API", SpanStyle(color = MaterialTheme.colorScheme.secondary))) - append(" ") - append(it.second.api.toString()) - }, - fontWeight = FontWeight.SemiBold, - fontFamily = FontFamily.Serif, - style = MaterialTheme.typography.bodySmall - ) - } - ) + AnywhereDropdown( + expanded = expanded, + onDismissRequest = { expanded = false }, + onClick = { /* TODO: Start module */ }, + onLongClick = { expanded = true }, + surface = { + AppItem( + icon = LSPPackageManager.getIcon(it.first), + label = it.first.label, + packageName = it.first.app.packageName, + additionalContent = { + Text( + text = it.second.description, + style = MaterialTheme.typography.bodySmall + ) + Text( + text = buildAnnotatedString { + append(AnnotatedString("API", SpanStyle(color = MaterialTheme.colorScheme.secondary))) + append(" ") + append(it.second.api.toString()) + }, + fontWeight = FontWeight.SemiBold, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.bodySmall + ) + } + ) + } + ) { + // TODO: Implement } } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt index f10f713..ecf3782 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt @@ -11,14 +11,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.Gson import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.share.Constants import org.lsposed.lspatch.share.PatchConfig +import org.lsposed.lspatch.ui.viewstate.ProcessingState import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager.AppInfo +import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.patch.util.Logger import java.io.FileNotFoundException import java.util.zip.ZipFile @@ -32,6 +33,8 @@ class AppManageViewModel : ViewModel() { sealed class ViewAction { data class UpdateLoader(val appInfo: AppInfo, val config: PatchConfig) : ViewAction() object ClearUpdateLoaderResult : ViewAction() + data class PerformOptimize(val appInfo: AppInfo) : ViewAction() + object ClearOptimizeResult : ViewAction() } val appList: List> by derivedStateOf { @@ -46,9 +49,10 @@ class AppManageViewModel : ViewModel() { } } - var processingUpdate by mutableStateOf(false) + var updateLoaderState: ProcessingState> by mutableStateOf(ProcessingState.Idle) private set - var updateLoaderResult: Result? by mutableStateOf(null) + + var optimizeState: ProcessingState by mutableStateOf(ProcessingState.Idle) private set private val logger = object : Logger() { @@ -65,44 +69,54 @@ class AppManageViewModel : ViewModel() { } } - fun dispatch(action: ViewAction) { - when (action) { - is ViewAction.UpdateLoader -> updateLoader(action.appInfo, action.config) - is ViewAction.ClearUpdateLoaderResult -> updateLoaderResult = null + suspend fun dispatch(action: ViewAction) { + withContext(viewModelScope.coroutineContext) { + when (action) { + is ViewAction.UpdateLoader -> updateLoader(action.appInfo, action.config) + is ViewAction.ClearUpdateLoaderResult -> updateLoaderState = ProcessingState.Idle + is ViewAction.PerformOptimize -> performOptimize(action.appInfo) + is ViewAction.ClearOptimizeResult -> optimizeState = ProcessingState.Idle + } } } - private fun updateLoader(appInfo: AppInfo, config: PatchConfig) { + private suspend fun updateLoader(appInfo: AppInfo, config: PatchConfig) { Log.i(TAG, "Update loader for ${appInfo.app.packageName}") - viewModelScope.launch { - processingUpdate = true - val result = runCatching { - withContext(Dispatchers.IO) { - LSPPackageManager.cleanTmpApkDir() - val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray()) - val patchPaths = mutableListOf() - val embeddedModulePaths = if (config.useManager) emptyList() else null - for (apk in apkPaths) { - ZipFile(apk).use { zip -> - var entry = zip.getEntry(Constants.ORIGINAL_APK_ASSET_PATH) - if (entry == null) entry = zip.getEntry("assets/lspatch/origin_apk.bin") - if (entry == null) throw FileNotFoundException("Original apk entry not found for $apk") - zip.getInputStream(entry).use { input -> - val dst = lspApp.tmpApkDir.resolve(apk.substringAfterLast('/')) - patchPaths.add(dst.absolutePath) - dst.outputStream().use { output -> - input.copyTo(output) - } + updateLoaderState = ProcessingState.Processing + val result = runCatching { + withContext(Dispatchers.IO) { + LSPPackageManager.cleanTmpApkDir() + val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray()) + val patchPaths = mutableListOf() + val embeddedModulePaths = if (config.useManager) emptyList() else null + for (apk in apkPaths) { + ZipFile(apk).use { zip -> + var entry = zip.getEntry(Constants.ORIGINAL_APK_ASSET_PATH) + if (entry == null) entry = zip.getEntry("assets/lspatch/origin_apk.bin") + if (entry == null) throw FileNotFoundException("Original apk entry not found for $apk") + zip.getInputStream(entry).use { input -> + val dst = lspApp.tmpApkDir.resolve(apk.substringAfterLast('/')) + patchPaths.add(dst.absolutePath) + dst.outputStream().use { output -> + input.copyTo(output) } } } - Patcher.patch(logger, Patcher.Options(config, patchPaths, embeddedModulePaths)) - val (status, message) = LSPPackageManager.install() - if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message) } + Patcher.patch(logger, Patcher.Options(config, patchPaths, embeddedModulePaths)) + val (status, message) = LSPPackageManager.install() + if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message) } - processingUpdate = false - updateLoaderResult = result } + updateLoaderState = ProcessingState.Done(result) + } + + private suspend fun performOptimize(appInfo: AppInfo) { + Log.i(TAG, "Perform optimize for ${appInfo.app.packageName}") + optimizeState = ProcessingState.Processing + val result = withContext(Dispatchers.IO) { + ShizukuApi.performDexOptMode(appInfo.app.packageName) + } + optimizeState = ProcessingState.Done(result) } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewstate/ProcessingState.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewstate/ProcessingState.kt new file mode 100644 index 0000000..cd8ac13 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewstate/ProcessingState.kt @@ -0,0 +1,7 @@ +package org.lsposed.lspatch.ui.viewstate + +sealed class ProcessingState { + object Idle : ProcessingState() + object Processing : ProcessingState() + data class Done(val result: T) : ProcessingState() +}