manager: replace SwipeRefresh with PullRefreshIndicator & refactor so… (#288)

- replace SwipeRefresh with PullRefreshIndicator
- optimize pull refresh
- refactor some code
- fix install bottom in module page again
This commit is contained in:
TinyHai
2023-03-02 12:35:41 +08:00
committed by GitHub
parent 8bbfe0c26d
commit 7846b2a440
6 changed files with 170 additions and 112 deletions

View File

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

View File

@@ -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,

View File

@@ -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<ModuleViewModel>()
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,54 +91,46 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
}
}) { innerPadding ->
val dialogState = rememberDialogHostState()
ConfirmDialog(dialogState)
ConfirmDialog()
when {
isKSUVersionInvalid -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.require_kernel_version_8))
}
}
hasMagisk -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.module_magisk_conflict),
textAlign = TextAlign.Center,
)
}
}
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 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
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.module_overlay_fs_not_available))
}
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))
}
} else {
LazyColumn(verticalArrangement = Arrangement.spacedBy(15.dp),
contentPadding = remember { PaddingValues(bottom = 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */) }) {
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)
@@ -146,16 +138,19 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val cancel = stringResource(id = android.R.string.cancel)
val moduleUninstallConfirm =
stringResource(id = R.string.module_uninstall_confirm)
ModuleItem(module, isChecked, onUninstall = {
scope.launch {
val dialogResult = dialogState.showDialog(
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@launch
return
}
val success = uninstallModule(module.id)
@@ -172,13 +167,39 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
} else {
null
}
val result = snackBarHost.showSnackbar(
message, actionLabel = actionLabel
)
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 scope = rememberCoroutineScope()
ModuleItem(module, isChecked, onUninstall = {
scope.launch { onModuleUninstall(module) }
}, onCheckChanged = {
val success = toggleModule(module.id, !isChecked)
if (success) {
@@ -203,8 +224,20 @@ 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
)
)
}
}
@OptIn(ExperimentalMaterial3Api::class)

View File

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

View File

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

View File

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