diff --git a/manager/src/main/java/org/lsposed/npatch/ui/page/manage/AppManagePage.kt b/manager/src/main/java/org/lsposed/npatch/ui/page/manage/AppManagePage.kt index 115ee51..f9e5e79 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/page/manage/AppManagePage.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/page/manage/AppManagePage.kt @@ -30,6 +30,8 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient @@ -68,191 +70,199 @@ fun AppManageBody( val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() - if (viewModel.appList.isEmpty()) { - Box(Modifier.fillMaxSize()) { - Text( - modifier = Modifier.align(Alignment.Center), - text = run { - if (NPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading) - else stringResource(R.string.manage_no_apps) - }, - fontFamily = FontFamily.Serif, - style = MaterialTheme.typography.headlineSmall - ) - } - } else { - var scopeApp by rememberSaveable { mutableStateOf("") } - var afterCheckManager by remember { mutableStateOf<(() -> Unit)?>(null) } - resultRecipient.onNavResult { - if (it is NavResult.Value) { - scope.launch { - val result = it.value as SelectAppsResult.MultipleApps - 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)) - } + var scopeApp by rememberSaveable { mutableStateOf("") } + var afterCheckManager by remember { mutableStateOf<(() -> Unit)?>(null) } + + resultRecipient.onNavResult { + if (it is NavResult.Value) { + scope.launch { + val result = it.value as SelectAppsResult.MultipleApps + 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)) } } } + } - 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("NPatch", 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("NPatch", it.toString())) } - 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) - } + } + 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) } } + } + // 下拉刷新 + SwipeRefresh( + state = rememberSwipeRefreshState(viewModel.isRefreshing), + onRefresh = { viewModel.dispatch(AppManageViewModel.ViewAction.Refresh) }, + modifier = Modifier.fillMaxSize() + ) { + if (viewModel.appList.isEmpty()) { + Box(Modifier.fillMaxSize()) { + Text( + modifier = Modifier.align(Alignment.Center), + text = run { + if (NPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading) + else stringResource(R.string.manage_no_apps) + }, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.headlineSmall + ) + } + } else { + LazyColumn(Modifier.fillMaxSize()) { + items( + items = viewModel.appList, + key = { it.first.app.packageName } + ) { (appInfo, patchConfig) -> + val isRolling = patchConfig.useManager && patchConfig.lspConfig.VERSION_CODE >= Constants.MIN_ROLLING_VERSION_CODE + val canUpdateLoader = !isRolling && (patchConfig.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE || patchConfig.managerPackageName != BuildConfig.APPLICATION_ID) + var expanded by remember { mutableStateOf(false) } - LazyColumn(Modifier.fillMaxHeight()) { - items( - items = viewModel.appList, - key = { it.first.app.packageName } - ) { (appInfo, patchConfig) -> - val isRolling = patchConfig.useManager && patchConfig.lspConfig.VERSION_CODE >= Constants.MIN_ROLLING_VERSION_CODE - val canUpdateLoader = !isRolling && (patchConfig.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE || patchConfig.managerPackageName != BuildConfig.APPLICATION_ID) - var expanded by remember { mutableStateOf(false) } + AnywhereDropdown( + expanded = expanded, + onDismissRequest = { expanded = false }, + onClick = { expanded = true }, + onLongClick = { expanded = true }, + surface = { + AppItem( + icon = NPackageManager.getIcon(appInfo), + label = appInfo.label, + packageName = appInfo.app.packageName, + additionalContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + val patchText = if (patchConfig.useManager) { + stringResource(R.string.patch_local) + } else { + stringResource(R.string.patch_integrated) + } + val patchColor = if (patchConfig.useManager) { + MaterialTheme.colorScheme.secondary + } else { + MaterialTheme.colorScheme.tertiary + } + val versionText = if (isRolling) { + stringResource(R.string.manage_rolling) + } else { + patchConfig.lspConfig.VERSION_CODE.toString() + } - AnywhereDropdown( - expanded = expanded, - onDismissRequest = { expanded = false }, - onClick = { expanded = true }, - onLongClick = { expanded = true }, - surface = { - AppItem( - icon = NPackageManager.getIcon(appInfo), - label = appInfo.label, - packageName = appInfo.app.packageName, - additionalContent = { - Row(verticalAlignment = Alignment.CenterVertically) { - val patchText = if (patchConfig.useManager) { - stringResource(R.string.patch_local) - } else { - stringResource(R.string.patch_integrated) - } - val patchColor = if (patchConfig.useManager) { - MaterialTheme.colorScheme.secondary - } else { - MaterialTheme.colorScheme.tertiary - } - val versionText = if (isRolling) { - stringResource(R.string.manage_rolling) - } else { - patchConfig.lspConfig.VERSION_CODE.toString() - } - - Text( - text = "$patchText $versionText", - color = patchColor, - fontWeight = FontWeight.SemiBold, - fontFamily = FontFamily.Serif, - style = MaterialTheme.typography.bodySmall - ) - if (canUpdateLoader) { - with(LocalDensity.current) { - val size = MaterialTheme.typography.bodySmall.fontSize * 1.2 - Icon(Icons.Filled.KeyboardCapslock, null, Modifier.size(size.toDp())) + Text( + text = "$patchText $versionText", + color = patchColor, + fontWeight = FontWeight.SemiBold, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.bodySmall + ) + if (canUpdateLoader) { + with(LocalDensity.current) { + val size = MaterialTheme.typography.bodySmall.fontSize * 1.2 + Icon(Icons.Filled.KeyboardCapslock, null, Modifier.size(size.toDp())) + } } } } - } - ) - } - ) { - DropdownMenuItem( - text = { Text(text = appInfo.label, color = MaterialTheme.colorScheme.primary) }, - onClick = {}, enabled = false - ) - val shizukuUnavailable = stringResource(R.string.shizuku_unavailable) - if (canUpdateLoader || BuildConfig.DEBUG) { + ) + } + ) { DropdownMenuItem( - text = { Text(stringResource(R.string.manage_update_loader)) }, - onClick = { - expanded = false - scope.launch { - viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(appInfo, patchConfig)) - } - } + text = { Text(text = appInfo.label, color = MaterialTheme.colorScheme.primary) }, + onClick = {}, enabled = false ) - } - if (patchConfig.useManager) { - DropdownMenuItem( - text = { Text(stringResource(R.string.manage_module_scope)) }, - onClick = { - expanded = false - scope.launch { - scopeApp = appInfo.app.packageName - val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet() - val initialSelected = NPackageManager.appList.mapNotNullTo(ArrayList()) { - if (activated.contains(it.app.packageName)) it.app.packageName else null + val shizukuUnavailable = stringResource(R.string.shizuku_unavailable) + if (canUpdateLoader || BuildConfig.DEBUG) { + DropdownMenuItem( + text = { Text(stringResource(R.string.manage_update_loader)) }, + onClick = { + expanded = false + scope.launch { + viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(appInfo, patchConfig)) + } + } + ) + } + if (patchConfig.useManager) { + DropdownMenuItem( + text = { Text(stringResource(R.string.manage_module_scope)) }, + onClick = { + expanded = false + scope.launch { + scopeApp = appInfo.app.packageName + val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet() + val initialSelected = NPackageManager.appList.mapNotNullTo(ArrayList()) { + if (activated.contains(it.app.packageName)) it.app.packageName else null + } + navigator.navigate(SelectAppsScreenDestination(true, initialSelected)) + } + } + ) + } + 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(appInfo)) } - navigator.navigate(SelectAppsScreenDestination(true, initialSelected)) } } ) - } - 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(appInfo)) + val uninstallSuccessfully = stringResource(R.string.manage_uninstall_successfully) + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + scope.launch { + snackbarHost.showSnackbar(uninstallSuccessfully) + viewModel.dispatch(AppManageViewModel.ViewAction.Refresh) } } } - ) - val uninstallSuccessfully = stringResource(R.string.manage_uninstall_successfully) - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.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:${appInfo.app.packageName}") + putExtra(Intent.EXTRA_RETURN_RESULT, true) + } + launcher.launch(intent) } - } + ) } - DropdownMenuItem( - text = { Text(stringResource(R.string.uninstall)) }, - onClick = { - expanded = false - val intent = Intent(Intent.ACTION_DELETE).apply { - data = Uri.parse("package:${appInfo.app.packageName}") - putExtra(Intent.EXTRA_RETURN_RESULT, true) - } - launcher.launch(intent) - } - ) } } } diff --git a/manager/src/main/java/org/lsposed/npatch/ui/page/manage/ModuleManagePage.kt b/manager/src/main/java/org/lsposed/npatch/ui/page/manage/ModuleManagePage.kt index 3bb8efa..24cab22 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/page/manage/ModuleManagePage.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/page/manage/ModuleManagePage.kt @@ -22,6 +22,8 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import org.lsposed.npatch.ui.component.AnywhereDropdown import org.lsposed.npatch.ui.component.AppItem import org.lsposed.npatch.R @@ -32,75 +34,82 @@ import nkbe.util.NPackageManager fun ModuleManageBody() { val context = LocalContext.current val viewModel = viewModel() - if (viewModel.appList.isEmpty()) { - Box(Modifier.fillMaxSize()) { - Text( - modifier = Modifier.align(Alignment.Center), - text = run { - if (NPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading) - else stringResource(R.string.manage_no_modules) - }, - fontFamily = FontFamily.Serif, - style = MaterialTheme.typography.headlineSmall - ) - } - } else { - LazyColumn(Modifier.fillMaxHeight()) { - items( - items = viewModel.appList, - key = { it.first.app.packageName } - ) { - var expanded by remember { mutableStateOf(false) } - val settingsIntent = remember { NPackageManager.getSettingsIntent(it.first.app.packageName) } - AnywhereDropdown( - expanded = expanded, - onDismissRequest = { expanded = false }, - onClick = { settingsIntent?.let { context.startActivity(it) } }, - onLongClick = { expanded = true }, - surface = { - AppItem( - icon = NPackageManager.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 + // 下拉刷新 + SwipeRefresh( + state = rememberSwipeRefreshState(viewModel.isRefreshing), + onRefresh = { viewModel.refresh() }, + modifier = Modifier.fillMaxSize() + ) { + if (viewModel.appList.isEmpty()) { + Box(Modifier.fillMaxSize()) { + Text( + modifier = Modifier.align(Alignment.Center), + text = run { + if (NPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading) + else stringResource(R.string.manage_no_modules) + }, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.headlineSmall + ) + } + } else { + LazyColumn(Modifier.fillMaxSize()) { + items( + items = viewModel.appList, + key = { it.first.app.packageName } + ) { + var expanded by remember { mutableStateOf(false) } + val settingsIntent = remember { NPackageManager.getSettingsIntent(it.first.app.packageName) } + AnywhereDropdown( + expanded = expanded, + onDismissRequest = { expanded = false }, + onClick = { settingsIntent?.let { context.startActivity(it) } }, + onLongClick = { expanded = true }, + surface = { + AppItem( + icon = NPackageManager.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 + ) + } + ) + } + ) { + DropdownMenuItem( + text = { Text(text = it.first.label, color = MaterialTheme.colorScheme.primary) }, + onClick = {}, enabled = false + ) + if (settingsIntent != null) { + DropdownMenuItem( + text = { Text(stringResource(R.string.manage_module_settings)) }, + onClick = { context.startActivity(settingsIntent) } + ) + } + DropdownMenuItem( + text = { Text(stringResource(R.string.manage_app_info)) }, + onClick = { + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", it.first.app.packageName, null) ) + context.startActivity(intent) } ) } - ) { - DropdownMenuItem( - text = { Text(text = it.first.label, color = MaterialTheme.colorScheme.primary) }, - onClick = {}, enabled = false - ) - if (settingsIntent != null) { - DropdownMenuItem( - text = { Text(stringResource(R.string.manage_module_settings)) }, - onClick = { context.startActivity(settingsIntent) } - ) - } - DropdownMenuItem( - text = { Text(stringResource(R.string.manage_app_info)) }, - onClick = { - val intent = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", it.first.app.packageName, null) - ) - context.startActivity(intent) - } - ) } } } diff --git a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/AppManageViewModel.kt b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/AppManageViewModel.kt index 4d50cab..30adcf0 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/AppManageViewModel.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/AppManageViewModel.kt @@ -42,7 +42,7 @@ class AppManageViewModel : ViewModel() { object ClearUpdateLoaderResult : ViewAction() data class PerformOptimize(val appInfo: AppInfo) : ViewAction() object ClearOptimizeResult : ViewAction() - object RefreshList : ViewAction() + object Refresh : ViewAction() } // 手動管理狀態,避免實時響應系統廣播導致列表跳動 @@ -98,7 +98,16 @@ class AppManageViewModel : ViewModel() { is ViewAction.ClearUpdateLoaderResult -> updateLoaderState = ProcessingState.Idle is ViewAction.PerformOptimize -> performOptimize(action.appInfo) is ViewAction.ClearOptimizeResult -> optimizeState = ProcessingState.Idle - is ViewAction.RefreshList -> loadData(silent = false) + is ViewAction.Refresh -> { + if (!isRefreshing) { + isRefreshing = true + withContext(Dispatchers.IO) { + NPackageManager.fetchAppList() + } + loadData(silent = true) + isRefreshing = false + } + } } } } diff --git a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/ModuleManageViewModel.kt b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/ModuleManageViewModel.kt index 350a8bf..05110ff 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/ModuleManageViewModel.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/ModuleManageViewModel.kt @@ -3,7 +3,13 @@ package org.lsposed.npatch.ui.viewmodel.manage import android.util.Log import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import nkbe.util.NPackageManager class ModuleManageViewModel : ViewModel() { @@ -12,6 +18,9 @@ class ModuleManageViewModel : ViewModel() { private const val TAG = "ModuleManageViewModel" } + var isRefreshing by mutableStateOf(false) + private set + class XposedInfo( val api: Int, val description: String, @@ -30,4 +39,15 @@ class ModuleManageViewModel : ViewModel() { Log.d(TAG, "Loaded ${it.size} Xposed modules") } } + + fun refresh() { + if (isRefreshing) return + viewModelScope.launch { + isRefreshing = true + withContext(Dispatchers.IO) { + NPackageManager.fetchAppList() + } + isRefreshing = false + } + } }