From 7846b2a440bbb092d0fdaaf7aedc372a13219fbf Mon Sep 17 00:00:00 2001 From: TinyHai <34483077+TinyHai@users.noreply.github.com> Date: Thu, 2 Mar 2023 12:35:41 +0800 Subject: [PATCH] =?UTF-8?q?manager:=20replace=20SwipeRefresh=20with=20Pull?= =?UTF-8?q?RefreshIndicator=20&=20refactor=20so=E2=80=A6=20(#288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replace SwipeRefresh with PullRefreshIndicator - optimize pull refresh - refactor some code - fix install bottom in module page again --- manager/app/build.gradle.kts | 3 +- .../me/weishu/kernelsu/ui/MainActivity.kt | 7 +- .../me/weishu/kernelsu/ui/screen/Module.kt | 207 ++++++++++-------- .../me/weishu/kernelsu/ui/screen/Settings.kt | 12 +- .../me/weishu/kernelsu/ui/screen/SuperUser.kt | 33 ++- .../kernelsu/ui/viewmodel/ModuleViewModel.kt | 20 +- 6 files changed, 170 insertions(+), 112 deletions(-) diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index cf14dd09..1085c2e9 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -62,10 +62,10 @@ dependencies { val accompanistVersion = "0.28.0" val composeDestinationsVersion = "1.7.27-beta" implementation(platform("androidx.compose:compose-bom:2022.12.00")) - debugImplementation("androidx.compose.ui:ui-test-manifest") debugImplementation("androidx.compose.ui:ui-tooling") implementation("androidx.activity:activity-compose:1.6.1") + implementation("androidx.compose.material:material:1.4.0-beta02") implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material3:material3") implementation("androidx.compose.ui:ui") @@ -75,7 +75,6 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.5.3") implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion") implementation("com.google.accompanist:accompanist-navigation-animation:$accompanistVersion") - implementation("com.google.accompanist:accompanist-swiperefresh:$accompanistVersion") implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion") implementation("io.github.raamcosta.compose-destinations:animations-core:$composeDestinationsVersion") diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt index dee62c47..6c1b9069 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt @@ -16,12 +16,14 @@ import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.ramcosta.composedestinations.DestinationsNavHost +import me.weishu.kernelsu.ui.component.rememberDialogHostState import me.weishu.kernelsu.ui.screen.BottomBarDestination import me.weishu.kernelsu.ui.screen.NavGraphs import me.weishu.kernelsu.ui.screen.appCurrentDestinationAsState import me.weishu.kernelsu.ui.screen.destinations.Destination import me.weishu.kernelsu.ui.screen.startAppDestination import me.weishu.kernelsu.ui.theme.KernelSUTheme +import me.weishu.kernelsu.ui.util.LocalDialogHost import me.weishu.kernelsu.ui.util.LocalSnackbarHost class MainActivity : ComponentActivity() { @@ -38,7 +40,10 @@ class MainActivity : ComponentActivity() { bottomBar = { BottomBar(navController) }, snackbarHost = { SnackbarHost(snackbarHostState) } ) { innerPadding -> - CompositionLocalProvider(LocalSnackbarHost provides snackbarHostState) { + CompositionLocalProvider( + LocalSnackbarHost provides snackbarHostState, + LocalDialogHost provides rememberDialogHostState(), + ) { DestinationsNavHost( modifier = Modifier.padding(innerPadding), navGraph = NavGraphs.root, diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index 94a51ca1..ba3df1c4 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt @@ -8,8 +8,12 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -17,14 +21,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch @@ -32,7 +35,6 @@ import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.ConfirmDialog import me.weishu.kernelsu.ui.component.DialogResult -import me.weishu.kernelsu.ui.component.rememberDialogHostState import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel @@ -42,9 +44,6 @@ import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel @Composable fun ModuleScreen(navigator: DestinationsNavigator) { val viewModel = viewModel() - val snackBarHost = LocalSnackbarHost.current - - val scope = rememberCoroutineScope() LaunchedEffect(Unit) { if (viewModel.moduleList.isEmpty()) { @@ -53,9 +52,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } val isSafeMode = Natives.isSafeMode() + val isKSUVersionInvalid = Natives.getVersion() < 0 val hasMagisk = hasMagisk() - val hideInstallButton = isSafeMode || hasMagisk + val hideInstallButton = isSafeMode || isKSUVersionInvalid || hasMagisk Scaffold(topBar = { TopBar() @@ -91,94 +91,115 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } }) { innerPadding -> - val dialogState = rememberDialogHostState() - ConfirmDialog(dialogState) + ConfirmDialog() - val failedEnable = stringResource(R.string.module_failed_to_enable) - val failedDisable = stringResource(R.string.module_failed_to_disable) - val failedUninstall = stringResource(R.string.module_uninstall_failed) - val successUninstall = stringResource(R.string.module_uninstall_success) - val swipeState = rememberSwipeRefreshState(viewModel.isRefreshing) - // TODO: Replace SwipeRefresh with RefreshIndicator when it's ready - if (Natives.getVersion() < 8) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(stringResource(R.string.require_kernel_version_8)) - } - return@Scaffold - } - if (hasMagisk) { - Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) { - Text(stringResource(R.string.module_magisk_conflict)) - } - return@Scaffold - } - SwipeRefresh( - state = swipeState, onRefresh = { - scope.launch { viewModel.fetchModuleList() } - }, modifier = Modifier - .padding(innerPadding) - .padding(16.dp) - .fillMaxSize() - ) { - val isOverlayAvailable = overlayFsAvailable() - if (!isOverlayAvailable) { - swipeState.isRefreshing = false + when { + isKSUVersionInvalid -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(stringResource(R.string.module_overlay_fs_not_available)) + Text(stringResource(R.string.require_kernel_version_8)) } - return@SwipeRefresh } - val isEmpty = viewModel.moduleList.isEmpty() - if (isEmpty) { - swipeState.isRefreshing = false - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(stringResource(R.string.module_empty)) + hasMagisk -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + Text( + stringResource(R.string.module_magisk_conflict), + textAlign = TextAlign.Center, + ) } - } else { - LazyColumn(verticalArrangement = Arrangement.spacedBy(15.dp), - contentPadding = remember { PaddingValues(bottom = 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */) }) { + } + else -> { + ModuleList( + viewModel = viewModel, + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + ) + } + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun ModuleList(viewModel: ModuleViewModel, modifier: Modifier = Modifier) { + val failedEnable = stringResource(R.string.module_failed_to_enable) + val failedDisable = stringResource(R.string.module_failed_to_disable) + val failedUninstall = stringResource(R.string.module_uninstall_failed) + val successUninstall = stringResource(R.string.module_uninstall_success) + val reboot = stringResource(id = R.string.reboot) + val rebootToApply = stringResource(id = R.string.reboot_to_apply) + val moduleStr = stringResource(id = R.string.module) + val uninstall = stringResource(id = R.string.uninstall) + val cancel = stringResource(id = android.R.string.cancel) + val moduleUninstallConfirm = + stringResource(id = R.string.module_uninstall_confirm) + + val dialogHost = LocalDialogHost.current + val snackBarHost = LocalSnackbarHost.current + + suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) { + val dialogResult = dialogHost.showDialog( + moduleStr, + content = moduleUninstallConfirm.format(module.name), + confirm = uninstall, + dismiss = cancel + ) + if (dialogResult != DialogResult.Confirmed) { + return + } + + val success = uninstallModule(module.id) + if (success) { + viewModel.fetchModuleList() + } + val message = if (success) { + successUninstall.format(module.name) + } else { + failedUninstall.format(module.name) + } + val actionLabel = if (success) { + reboot + } else { + null + } + val result = snackBarHost.showSnackbar(message, actionLabel = actionLabel) + if (result == SnackbarResult.ActionPerformed) { + reboot() + } + } + + val refreshState = rememberPullRefreshState( + refreshing = viewModel.isRefreshing, + onRefresh = { viewModel.fetchModuleList() } + ) + Box(modifier.pullRefresh(refreshState).padding(16.dp)) { + if (viewModel.isOverlayAvailable) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(15.dp), + contentPadding = remember { PaddingValues(bottom = 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */) }, + ) { + val isEmpty = viewModel.moduleList.isEmpty() + if (isEmpty) { + item { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(stringResource(R.string.module_empty)) + } + } + } else { items(viewModel.moduleList) { module -> var isChecked by rememberSaveable(module) { mutableStateOf(module.enabled) } - val reboot = stringResource(id = R.string.reboot) - val rebootToApply = stringResource(id = R.string.reboot_to_apply) - val moduleStr = stringResource(id = R.string.module) - val uninstall = stringResource(id = R.string.uninstall) - val cancel = stringResource(id = android.R.string.cancel) - val moduleUninstallConfirm = - stringResource(id = R.string.module_uninstall_confirm) + val scope = rememberCoroutineScope() ModuleItem(module, isChecked, onUninstall = { - scope.launch { - val dialogResult = dialogState.showDialog( - moduleStr, - content = moduleUninstallConfirm.format(module.name), - confirm = uninstall, - dismiss = cancel - ) - if (dialogResult != DialogResult.Confirmed) { - return@launch - } - - val success = uninstallModule(module.id) - if (success) { - viewModel.fetchModuleList() - } - val message = if (success) { - successUninstall.format(module.name) - } else { - failedUninstall.format(module.name) - } - val actionLabel = if (success) { - reboot - } else { - null - } - val result = snackBarHost.showSnackbar( - message, actionLabel = actionLabel - ) - if (result == SnackbarResult.ActionPerformed) { - reboot() - } - } + scope.launch { onModuleUninstall(module) } }, onCheckChanged = { val success = toggleModule(module.id, !isChecked) if (success) { @@ -203,7 +224,19 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } } } + } else { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(stringResource(R.string.module_overlay_fs_not_available)) + } } + + PullRefreshIndicator( + refreshing = viewModel.isRefreshing, + state = refreshState, + modifier = Modifier.align( + Alignment.TopCenter + ) + ) } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt index 9492293b..0d0b4e26 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt @@ -2,7 +2,6 @@ package me.weishu.kernelsu.ui.screen import android.content.Intent import android.net.Uri -import android.widget.Toast import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack @@ -17,12 +16,12 @@ import com.alorma.compose.settings.ui.* import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch -import me.weishu.kernelsu.ui.util.getBugreportFile import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.SimpleDialog -import me.weishu.kernelsu.ui.component.rememberDialogHostState import me.weishu.kernelsu.ui.util.LinkifyText +import me.weishu.kernelsu.ui.util.LocalDialogHost +import me.weishu.kernelsu.ui.util.getBugreportFile /** @@ -42,9 +41,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) { paddingValues -> - val dialogState = rememberDialogHostState() - - SimpleDialog(dialogState) { + SimpleDialog { SupportCard() } @@ -75,12 +72,13 @@ fun SettingScreen(navigator: DestinationsNavigator) { val about = stringResource(id = R.string.about) val ok = stringResource(id = android.R.string.ok) val scope = rememberCoroutineScope() + val dialogHost = LocalDialogHost.current SettingsMenuLink(title = { Text(about) }, onClick = { scope.launch { - dialogState.showDialog(about, content = "unused", confirm = ok) + dialogHost.showDialog(about, content = "unused", confirm = ok) } } ) diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt index 4b650c71..0ee32ebd 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt @@ -3,11 +3,16 @@ package me.weishu.kernelsu.ui.screen import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -15,8 +20,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import coil.request.ImageRequest -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.ramcosta.composedestinations.annotation.Destination import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives @@ -26,7 +29,7 @@ import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import java.util.* -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Destination @Composable fun SuperUserScreen() { @@ -87,19 +90,19 @@ fun SuperUserScreen() { ) } ) { innerPadding -> - val failMessage = stringResource(R.string.superuser_failed_to_grant_root) - // TODO: Replace SwipeRefresh with RefreshIndicator when it's ready - SwipeRefresh( - state = rememberSwipeRefreshState(viewModel.isRefreshing), - onRefresh = { - scope.launch { viewModel.fetchAppList() } - }, + val refreshState = rememberPullRefreshState( + refreshing = viewModel.isRefreshing, + onRefresh = { scope.launch { viewModel.fetchAppList() } }, + ) + Box( modifier = Modifier .padding(innerPadding) - .fillMaxSize() + .pullRefresh(refreshState) ) { - LazyColumn { + val failMessage = stringResource(R.string.superuser_failed_to_grant_root) + + LazyColumn(Modifier.fillMaxSize()) { items(viewModel.appList, key = { it.packageName }) { app -> var isChecked by rememberSaveable(app) { mutableStateOf(app.onAllowList) } AppItem(app, isChecked) { checked -> @@ -112,6 +115,12 @@ fun SuperUserScreen() { } } } + + PullRefreshIndicator( + refreshing = viewModel.isRefreshing, + state = refreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) } } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt index 6d0558a6..3d2a6e61 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt @@ -7,9 +7,11 @@ 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.withContext +import kotlinx.coroutines.launch import me.weishu.kernelsu.ui.util.listModules +import me.weishu.kernelsu.ui.util.overlayFsAvailable import org.json.JSONArray import java.text.Collator import java.util.* @@ -36,6 +38,9 @@ class ModuleViewModel : ViewModel() { var isRefreshing by mutableStateOf(false) private set + var isOverlayAvailable by mutableStateOf(overlayFsAvailable()) + private set + val moduleList by derivedStateOf { val comparator = compareBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id) modules.sortedWith(comparator).also { @@ -43,12 +48,16 @@ class ModuleViewModel : ViewModel() { } } - suspend fun fetchModuleList() { - withContext(Dispatchers.IO) { + fun fetchModuleList() { + viewModelScope.launch(Dispatchers.IO) { isRefreshing = true + + val oldModuleList = modules + val start = SystemClock.elapsedRealtime() kotlin.runCatching { + isOverlayAvailable = overlayFsAvailable() val result = listModules() @@ -76,6 +85,11 @@ class ModuleViewModel : ViewModel() { isRefreshing = false } + // when both old and new is kotlin.collections.EmptyList + // moduleList update will don't trigger + if (oldModuleList === modules) { + isRefreshing = false + } Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules") }