Anywhere dropdown menu

This commit is contained in:
Nullptr 2022-07-11 18:13:02 +08:00
parent 8498da904c
commit 75e004e4c7
No known key found for this signature in database
GPG Key ID: 0B9D02052FF536BD
12 changed files with 366 additions and 255 deletions

View File

@ -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
)
}
}
}

View File

@ -2,8 +2,6 @@ package org.lsposed.lspatch.ui.component
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForwardIos import androidx.compose.material.icons.filled.ArrowForwardIos
@ -19,15 +17,13 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.lsposed.lspatch.ui.theme.LSPTheme import org.lsposed.lspatch.ui.theme.LSPTheme
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppItem( fun AppItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: Drawable, icon: Drawable,
label: String, label: String,
packageName: String, packageName: String,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
checked: Boolean? = null, checked: Boolean? = null,
rightIcon: (@Composable () -> Unit)? = null, rightIcon: (@Composable () -> Unit)? = null,
additionalContent: (@Composable ColumnScope.() -> Unit)? = null, additionalContent: (@Composable ColumnScope.() -> Unit)? = null,
@ -37,10 +33,6 @@ fun AppItem(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
.padding(20.dp) .padding(20.dp)
) { ) {
Row( Row(
@ -68,7 +60,7 @@ fun AppItem(
if (checked != null) { if (checked != null) {
Checkbox( Checkbox(
checked = checked, checked = checked,
onCheckedChange = { onClick() }, onCheckedChange = null,
modifier = Modifier.padding(start = 20.dp) modifier = Modifier.padding(start = 20.dp)
) )
} }
@ -90,7 +82,6 @@ private fun AppItemPreview() {
icon = shape, icon = shape,
label = "Sample App", label = "Sample App",
packageName = "org.lsposed.sample", packageName = "org.lsposed.sample",
onClick = {},
rightIcon = { Icon(Icons.Filled.ArrowForwardIos, null) } rightIcon = { Icon(Icons.Filled.ArrowForwardIos, null) }
) )
} }

View File

@ -1,5 +1,6 @@
package org.lsposed.lspatch.ui.component.settings package org.lsposed.lspatch.ui.component.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -17,14 +18,13 @@ fun SettingsCheckBox(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
checked: Boolean, checked: Boolean,
enabled: Boolean = true, enabled: Boolean = true,
onClick: () -> Unit,
icon: ImageVector? = null, icon: ImageVector? = null,
title: String, title: String,
desc: String? = null, desc: String? = null,
extraContent: (@Composable ColumnScope.() -> Unit)? = null extraContent: (@Composable ColumnScope.() -> Unit)? = null
) { ) {
SettingsSlot(modifier, enabled, onClick, icon, title, desc, extraContent) { SettingsSlot(modifier, enabled, icon, title, desc, extraContent) {
Checkbox(checked = checked, onCheckedChange = { onClick() }) Checkbox(checked = checked, onCheckedChange = null)
} }
} }
@ -35,14 +35,14 @@ private fun SettingsCheckBoxPreview() {
var checked2 by remember { mutableStateOf(false) } var checked2 by remember { mutableStateOf(false) }
Column { Column {
SettingsCheckBox( SettingsCheckBox(
modifier = Modifier.clickable { checked1 = !checked1 },
checked = checked1, checked = checked1,
onClick = { checked1 = !checked1 },
title = "Title", title = "Title",
desc = "Description" desc = "Description"
) )
SettingsCheckBox( SettingsCheckBox(
modifier = Modifier.clickable { checked2 = !checked2 },
checked = checked2, checked = checked2,
onClick = { checked2 = !checked2 },
icon = Icons.Outlined.Api, icon = Icons.Outlined.Api,
title = "Title", title = "Title",
desc = "Description" desc = "Description"

View File

@ -1,6 +1,5 @@
package org.lsposed.lspatch.ui.component.settings package org.lsposed.lspatch.ui.component.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -16,22 +15,16 @@ import androidx.compose.ui.unit.dp
fun SettingsSlot( fun SettingsSlot(
modifier: Modifier, modifier: Modifier,
enabled: Boolean, enabled: Boolean,
onClick: () -> Unit,
icon: ImageVector? = null, icon: ImageVector? = null,
title: String, title: String,
desc: String?, desc: String?,
extraContent: (@Composable ColumnScope.() -> Unit)? = null, extraContent: (@Composable ColumnScope.() -> Unit)? = null,
action: (@Composable RowScope.() -> Unit)?, action: (@Composable RowScope.() -> Unit)?,
) { ) {
val enabledModifier =
if (enabled) Modifier
.alpha(1f)
.clickable(onClick = onClick)
else Modifier.alpha(0.5f)
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.then(enabledModifier) .alpha(if (enabled) 1f else 0.5f)
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -71,9 +64,8 @@ fun SettingsSlot(
fun SettingsItem( fun SettingsItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
onClick: () -> Unit,
icon: ImageVector? = null, icon: ImageVector? = null,
title: String, title: String,
desc: String? = null, desc: String? = null,
extraContent: (@Composable ColumnScope.() -> Unit)? = null extraContent: (@Composable ColumnScope.() -> Unit)? = null
) = SettingsSlot(modifier, enabled, onClick, icon, title, desc, extraContent, null) ) = SettingsSlot(modifier, enabled, icon, title, desc, extraContent, null)

View File

@ -16,14 +16,13 @@ fun SettingsSwitch(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
checked: Boolean, checked: Boolean,
enabled: Boolean = true, enabled: Boolean = true,
onClick: () -> Unit,
icon: ImageVector? = null, icon: ImageVector? = null,
title: String, title: String,
desc: String? = null, desc: String? = null,
extraContent: (@Composable ColumnScope.() -> Unit)? = null extraContent: (@Composable ColumnScope.() -> Unit)? = null
) { ) {
SettingsSlot(modifier.clickable(onClick = onClick), enabled, onClick, icon, title, desc, extraContent) { SettingsSlot(modifier, enabled, icon, title, desc, extraContent) {
Switch(checked = checked, onCheckedChange = { onClick() }) Switch(checked = checked, onCheckedChange = null)
} }
} }
@ -34,14 +33,14 @@ private fun SettingsCheckBoxPreview() {
var checked2 by remember { mutableStateOf(false) } var checked2 by remember { mutableStateOf(false) }
Column { Column {
SettingsSwitch( SettingsSwitch(
modifier = Modifier.clickable { checked1 = !checked1 },
checked = checked1, checked = checked1,
onClick = { checked1 = !checked1 },
title = "Title", title = "Title",
desc = "Description" desc = "Description"
) )
SettingsSwitch( SettingsSwitch(
modifier = Modifier.clickable { checked2 = !checked2 },
checked = checked2, checked = checked2,
onClick = { checked2 = !checked2 },
icon = Icons.Outlined.Api, icon = Icons.Outlined.Api,
title = "Title", title = "Title",
desc = "Description" desc = "Description"

View File

@ -12,6 +12,7 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -39,6 +40,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.lspApp 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.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
@ -219,28 +221,36 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {
) )
} }
SettingsCheckBox( SettingsCheckBox(
modifier = Modifier.padding(top = 6.dp), modifier = Modifier
.padding(top = 6.dp)
.clickable { viewModel.debuggable = !viewModel.debuggable },
checked = viewModel.debuggable, checked = viewModel.debuggable,
onClick = { viewModel.debuggable = !viewModel.debuggable },
icon = Icons.Outlined.BugReport, icon = Icons.Outlined.BugReport,
title = stringResource(R.string.patch_debuggable) title = stringResource(R.string.patch_debuggable)
) )
SettingsCheckBox( SettingsCheckBox(
modifier = Modifier.clickable { viewModel.overrideVersionCode = !viewModel.overrideVersionCode },
checked = viewModel.overrideVersionCode, checked = viewModel.overrideVersionCode,
onClick = { viewModel.overrideVersionCode = !viewModel.overrideVersionCode },
icon = Icons.Outlined.Layers, icon = Icons.Outlined.Layers,
title = stringResource(R.string.patch_override_version_code), title = stringResource(R.string.patch_override_version_code),
desc = stringResource(R.string.patch_override_version_code_desc) desc = stringResource(R.string.patch_override_version_code_desc)
) )
Box { var signExpanded by remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) } AnywhereDropdown(
expanded = signExpanded,
onDismissRequest = { signExpanded = false },
onClick = { signExpanded = true },
surface = {
SettingsItem( SettingsItem(
onClick = { expanded = true },
icon = Icons.Outlined.Edit, icon = Icons.Outlined.Edit,
title = stringResource(R.string.patch_sign), title = stringResource(R.string.patch_sign),
desc = viewModel.sign.mapIndexedNotNull { index, on -> if (on) "V" + (index + 1) else null }.joinToString(" + ").ifEmpty { "None" } 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 -> repeat(2) { index ->
DropdownMenuItem( DropdownMenuItem(
text = { text = {
@ -253,16 +263,19 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {
) )
} }
} }
} var bypassExpanded by remember { mutableStateOf(false) }
Box { AnywhereDropdown(
var expanded by remember { mutableStateOf(false) } expanded = bypassExpanded,
onDismissRequest = { bypassExpanded = false },
onClick = { bypassExpanded = true },
surface = {
SettingsItem( SettingsItem(
onClick = { expanded = true },
icon = Icons.Outlined.RemoveModerator, icon = Icons.Outlined.RemoveModerator,
title = stringResource(R.string.patch_sigbypass), title = stringResource(R.string.patch_sigbypass),
desc = sigBypassLvStr(viewModel.sigBypassLevel) desc = sigBypassLvStr(viewModel.sigBypassLevel)
) )
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { }
) {
repeat(3) { repeat(3) {
DropdownMenuItem( DropdownMenuItem(
text = { text = {
@ -273,13 +286,12 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {
}, },
onClick = { onClick = {
viewModel.sigBypassLevel = it viewModel.sigBypassLevel = it
expanded = false bypassExpanded = false
} }
) )
} }
} }
} }
}
} }
@Composable @Composable

View File

@ -6,6 +6,7 @@ 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
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -124,11 +125,12 @@ private fun SingleSelect(onSelect: (AppInfo) -> Unit) {
key = { it.app.packageName } key = { it.app.packageName }
) { ) {
AppItem( AppItem(
modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), modifier = Modifier
.animateItemPlacement(spring(stiffness = Spring.StiffnessLow))
.clickable { onSelect(it) },
icon = LSPPackageManager.getIcon(it), icon = LSPPackageManager.getIcon(it),
label = it.label, label = it.label,
packageName = it.app.packageName, packageName = it.app.packageName
onClick = { onSelect(it) }
) )
} }
} }
@ -145,14 +147,15 @@ private fun MultiSelect() {
) { ) {
val checked = viewModel.multiSelected.contains(it) val checked = viewModel.multiSelected.contains(it)
AppItem( AppItem(
modifier = Modifier.animateItemPlacement(spring(stiffness = Spring.StiffnessLow)), modifier = Modifier
icon = LSPPackageManager.getIcon(it), .animateItemPlacement(spring(stiffness = Spring.StiffnessLow))
label = it.label, .clickable {
packageName = it.app.packageName,
onClick = {
if (checked) viewModel.multiSelected.remove(it) if (checked) viewModel.multiSelected.remove(it)
else viewModel.multiSelected.add(it) else viewModel.multiSelected.add(it)
}, },
icon = LSPPackageManager.getIcon(it),
label = it.label,
packageName = it.app.packageName,
checked = checked checked = checked
) )
} }

View File

@ -2,9 +2,9 @@ package org.lsposed.lspatch.ui.page
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -27,6 +27,7 @@ import kotlinx.coroutines.launch
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.config.Configs import org.lsposed.lspatch.config.Configs
import org.lsposed.lspatch.config.MyKeyStore 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.CenterTopBar
import org.lsposed.lspatch.ui.component.settings.SettingsItem import org.lsposed.lspatch.ui.component.settings.SettingsItem
import org.lsposed.lspatch.ui.component.settings.SettingsSwitch import org.lsposed.lspatch.ui.component.settings.SettingsSwitch
@ -56,33 +57,36 @@ fun SettingsScreen() {
private fun KeyStore() { private fun KeyStore() {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var dropdownExpanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
Box { AnywhereDropdown(
expanded = expanded,
onDismissRequest = { expanded = false },
onClick = { expanded = true },
surface = {
SettingsItem( SettingsItem(
onClick = { dropdownExpanded = !dropdownExpanded },
icon = Icons.Outlined.Ballot, icon = Icons.Outlined.Ballot,
title = stringResource(R.string.settings_keystore), title = stringResource(R.string.settings_keystore),
desc = stringResource(if (MyKeyStore.useDefault) R.string.settings_keystore_default else R.string.settings_keystore_custom) desc = stringResource(if (MyKeyStore.useDefault) R.string.settings_keystore_default else R.string.settings_keystore_custom)
) )
DropdownMenu(expanded = dropdownExpanded, onDismissRequest = { dropdownExpanded = false }) { }
) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.settings_keystore_default)) }, text = { Text(stringResource(R.string.settings_keystore_default)) },
onClick = { onClick = {
scope.launch { MyKeyStore.reset() } scope.launch { MyKeyStore.reset() }
dropdownExpanded = false expanded = false
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.settings_keystore_custom)) }, text = { Text(stringResource(R.string.settings_keystore_custom)) },
onClick = { onClick = {
dropdownExpanded = false expanded = false
showDialog = true showDialog = true
} }
) )
} }
}
if (showDialog) { if (showDialog) {
var wrongKeystore by rememberSaveable { mutableStateOf(false) } var wrongKeystore by rememberSaveable { mutableStateOf(false) }
@ -106,7 +110,7 @@ private fun KeyStore() {
} }
AlertDialog( AlertDialog(
onDismissRequest = { dropdownExpanded = false; showDialog = false }, onDismissRequest = { expanded = false; showDialog = false },
confirmButton = { confirmButton = {
TextButton( TextButton(
content = { Text(stringResource(android.R.string.ok)) }, content = { Text(stringResource(android.R.string.ok)) },
@ -144,14 +148,14 @@ private fun KeyStore() {
} }
scope.launch { MyKeyStore.setCustom(password, alias, aliasPassword) } scope.launch { MyKeyStore.setCustom(password, alias, aliasPassword) }
dropdownExpanded = false expanded = false
showDialog = false showDialog = false
}) })
}, },
dismissButton = { dismissButton = {
TextButton( TextButton(
content = { Text(stringResource(android.R.string.cancel)) }, content = { Text(stringResource(android.R.string.cancel)) },
onClick = { dropdownExpanded = false; showDialog = false } onClick = { expanded = false; showDialog = false }
) )
}, },
title = { title = {
@ -228,8 +232,8 @@ private fun KeyStore() {
@Composable @Composable
private fun DetailPatchLogs() { private fun DetailPatchLogs() {
SettingsSwitch( SettingsSwitch(
modifier = Modifier.clickable { Configs.detailPatchLogs = !Configs.detailPatchLogs },
checked = Configs.detailPatchLogs, checked = Configs.detailPatchLogs,
onClick = { Configs.detailPatchLogs = !Configs.detailPatchLogs },
title = stringResource(R.string.settings_detail_patch_logs) title = stringResource(R.string.settings_detail_patch_logs)
) )
} }

View File

@ -42,6 +42,7 @@ import org.lsposed.lspatch.config.Configs
import org.lsposed.lspatch.database.entity.Module import org.lsposed.lspatch.database.entity.Module
import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.share.LSPConfig 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.AppItem
import org.lsposed.lspatch.ui.component.LoadingDialog import org.lsposed.lspatch.ui.component.LoadingDialog
import org.lsposed.lspatch.ui.page.SelectAppsResult 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.page.destinations.SelectAppsScreenDestination
import org.lsposed.lspatch.ui.util.LocalSnackbarHost import org.lsposed.lspatch.ui.util.LocalSnackbarHost
import org.lsposed.lspatch.ui.viewmodel.manage.AppManageViewModel 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.LSPPackageManager
import org.lsposed.lspatch.util.ShizukuApi import org.lsposed.lspatch.util.ShizukuApi
import java.io.IOException import java.io.IOException
@ -93,13 +95,16 @@ fun AppManageBody(
} }
} }
if (viewModel.processingUpdate) LoadingDialog() when (viewModel.updateLoaderState) {
viewModel.updateLoaderResult?.let { 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 updateSuccessfully = stringResource(R.string.manage_update_loader_successfully)
val updateFailed = stringResource(R.string.manage_update_loader_failed) val updateFailed = stringResource(R.string.manage_update_loader_failed)
val copyError = stringResource(R.string.copy_error) val copyError = stringResource(R.string.copy_error)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
it.onSuccess { it.result.onSuccess {
snackbarHost.showSnackbar(updateSuccessfully) snackbarHost.showSnackbar(updateSuccessfully)
}.onFailure { }.onFailure {
val result = snackbarHost.showSnackbar(updateFailed, copyError) val result = snackbarHost.showSnackbar(updateFailed, copyError)
@ -111,6 +116,20 @@ fun AppManageBody(
viewModel.dispatch(AppManageViewModel.ViewAction.ClearUpdateLoaderResult) 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)
}
}
}
LazyColumn(Modifier.fillMaxHeight()) { LazyColumn(Modifier.fillMaxHeight()) {
items( items(
@ -118,13 +137,15 @@ fun AppManageBody(
key = { it.first.app.packageName } key = { it.first.app.packageName }
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box { AnywhereDropdown(
expanded = expanded,
onDismissRequest = { expanded = false },
onClick = { expanded = true },
surface = {
AppItem( AppItem(
icon = LSPPackageManager.getIcon(it.first), icon = LSPPackageManager.getIcon(it.first),
label = it.first.label, label = it.first.label,
packageName = it.first.app.packageName, packageName = it.first.app.packageName,
onClick = { expanded = true },
onLongClick = { expanded = true },
additionalContent = { additionalContent = {
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
@ -141,7 +162,8 @@ fun AppManageBody(
) )
} }
) )
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { }
) {
val shizukuUnavailable = stringResource(R.string.shizuku_unavailable) val shizukuUnavailable = stringResource(R.string.shizuku_unavailable)
if (it.second.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE || BuildConfig.DEBUG) { if (it.second.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE || BuildConfig.DEBUG) {
DropdownMenuItem( DropdownMenuItem(
@ -174,8 +196,6 @@ fun AppManageBody(
} }
) )
} }
val optimizeSucceed = stringResource(R.string.manage_optimize_successfully)
val optimizeFailed = stringResource(R.string.manage_optimize_failed)
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.manage_optimize)) }, text = { Text(stringResource(R.string.manage_optimize)) },
onClick = { onClick = {
@ -184,8 +204,7 @@ fun AppManageBody(
if (!ShizukuApi.isPermissionGranted) { if (!ShizukuApi.isPermissionGranted) {
snackbarHost.showSnackbar(shizukuUnavailable) snackbarHost.showSnackbar(shizukuUnavailable)
} else { } else {
val result = ShizukuApi.performDexOptMode(it.first.app.packageName) viewModel.dispatch(AppManageViewModel.ViewAction.PerformOptimize(it.first))
snackbarHost.showSnackbar(if (result) optimizeSucceed else optimizeFailed)
} }
} }
} }
@ -213,7 +232,6 @@ fun AppManageBody(
} }
} }
} }
}
} }
@Composable @Composable

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import org.lsposed.lspatch.R import org.lsposed.lspatch.R
import org.lsposed.lspatch.ui.component.AnywhereDropdown
import org.lsposed.lspatch.ui.component.AppItem import org.lsposed.lspatch.ui.component.AppItem
import org.lsposed.lspatch.ui.viewmodel.manage.ModuleManageViewModel import org.lsposed.lspatch.ui.viewmodel.manage.ModuleManageViewModel
import org.lsposed.lspatch.util.LSPPackageManager import org.lsposed.lspatch.util.LSPPackageManager
@ -44,13 +45,16 @@ fun ModuleManageBody() {
key = { it.first.app.packageName } key = { it.first.app.packageName }
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box { AnywhereDropdown(
expanded = expanded,
onDismissRequest = { expanded = false },
onClick = { /* TODO: Start module */ },
onLongClick = { expanded = true },
surface = {
AppItem( AppItem(
icon = LSPPackageManager.getIcon(it.first), icon = LSPPackageManager.getIcon(it.first),
label = it.first.label, label = it.first.label,
packageName = it.first.app.packageName, packageName = it.first.app.packageName,
onClick = { /* TODO: startAndSendModuleBinder */ },
onLongClick = { expanded = true },
additionalContent = { additionalContent = {
Text( Text(
text = it.second.description, text = it.second.description,
@ -69,6 +73,9 @@ fun ModuleManageBody() {
} }
) )
} }
) {
// TODO: Implement
}
} }
} }
} }

View File

@ -11,14 +11,15 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.lsposed.lspatch.Patcher import org.lsposed.lspatch.Patcher
import org.lsposed.lspatch.lspApp import org.lsposed.lspatch.lspApp
import org.lsposed.lspatch.share.Constants import org.lsposed.lspatch.share.Constants
import org.lsposed.lspatch.share.PatchConfig 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
import org.lsposed.lspatch.util.LSPPackageManager.AppInfo import org.lsposed.lspatch.util.LSPPackageManager.AppInfo
import org.lsposed.lspatch.util.ShizukuApi
import org.lsposed.patch.util.Logger import org.lsposed.patch.util.Logger
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -32,6 +33,8 @@ class AppManageViewModel : ViewModel() {
sealed class ViewAction { sealed class ViewAction {
data class UpdateLoader(val appInfo: AppInfo, val config: PatchConfig) : ViewAction() data class UpdateLoader(val appInfo: AppInfo, val config: PatchConfig) : ViewAction()
object ClearUpdateLoaderResult : ViewAction() object ClearUpdateLoaderResult : ViewAction()
data class PerformOptimize(val appInfo: AppInfo) : ViewAction()
object ClearOptimizeResult : ViewAction()
} }
val appList: List<Pair<AppInfo, PatchConfig>> by derivedStateOf { val appList: List<Pair<AppInfo, PatchConfig>> by derivedStateOf {
@ -46,9 +49,10 @@ class AppManageViewModel : ViewModel() {
} }
} }
var processingUpdate by mutableStateOf(false) var updateLoaderState: ProcessingState<Result<Unit>> by mutableStateOf(ProcessingState.Idle)
private set private set
var updateLoaderResult: Result<Unit>? by mutableStateOf(null)
var optimizeState: ProcessingState<Boolean> by mutableStateOf(ProcessingState.Idle)
private set private set
private val logger = object : Logger() { private val logger = object : Logger() {
@ -65,17 +69,20 @@ class AppManageViewModel : ViewModel() {
} }
} }
fun dispatch(action: ViewAction) { suspend fun dispatch(action: ViewAction) {
withContext(viewModelScope.coroutineContext) {
when (action) { when (action) {
is ViewAction.UpdateLoader -> updateLoader(action.appInfo, action.config) is ViewAction.UpdateLoader -> updateLoader(action.appInfo, action.config)
is ViewAction.ClearUpdateLoaderResult -> updateLoaderResult = null 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}") Log.i(TAG, "Update loader for ${appInfo.app.packageName}")
viewModelScope.launch { updateLoaderState = ProcessingState.Processing
processingUpdate = true
val result = runCatching { val result = runCatching {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
LSPPackageManager.cleanTmpApkDir() LSPPackageManager.cleanTmpApkDir()
@ -101,8 +108,15 @@ class AppManageViewModel : ViewModel() {
if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message) if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message)
} }
} }
processingUpdate = false updateLoaderState = ProcessingState.Done(result)
updateLoaderResult = 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)
} }
} }

View File

@ -0,0 +1,7 @@
package org.lsposed.lspatch.ui.viewstate
sealed class ProcessingState<out T> {
object Idle : ProcessingState<Nothing>()
object Processing : ProcessingState<Nothing>()
data class Done<T>(val result: T) : ProcessingState<T>()
}