mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-07-03 14:08:39 +02:00
Add SU live-reload, status bar notifications, Monet theming, and UI improvements
- Add SuEvents shared flow for live-reloading Superuser and Log tabs when root decisions are made - Add status bar notification option for SU grant/deny with auto-dismiss - Add Monet/Material You theme support with instant theme switching - Replace install page navigation with bottom sheet from Home screen - Replace deprecated ProgressDialog with MiUIX loading dialog for app hide/restore - Move save log and reboot to top bar action buttons in flash/action screens - Fix dialog button layout to use evenly distributed buttons with primary color on positive actions Made-with: Cursor
This commit is contained in:
@@ -12,11 +12,16 @@ abstract class AsyncLoadViewModel : BaseViewModel() {
|
||||
@MainThread
|
||||
fun startLoading() {
|
||||
if (loadingJob?.isActive == true) {
|
||||
// Prevent multiple jobs from running at the same time
|
||||
return
|
||||
}
|
||||
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun reload() {
|
||||
loadingJob?.cancel()
|
||||
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||
}
|
||||
|
||||
protected abstract suspend fun doLoadWork()
|
||||
}
|
||||
|
||||
@@ -45,8 +45,6 @@ import com.topjohnwu.magisk.ui.deny.DenyListViewModel
|
||||
import com.topjohnwu.magisk.ui.flash.FlashScreen
|
||||
import com.topjohnwu.magisk.ui.flash.FlashUtils
|
||||
import com.topjohnwu.magisk.ui.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallScreen
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ActionScreen
|
||||
import com.topjohnwu.magisk.ui.module.ActionViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserDetailScreen
|
||||
@@ -171,11 +169,6 @@ class MainActivity : AppCompatActivity(), SplashScreenHost {
|
||||
entry<Route.Main> {
|
||||
MainScreen(initialTab = initialTab)
|
||||
}
|
||||
entry<Route.Install> { _ ->
|
||||
val vm: InstallViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
|
||||
CollectNavEvents(vm, navigator)
|
||||
InstallScreen(vm, onBack = { navigator.pop() })
|
||||
}
|
||||
entry<Route.DenyList> { _ ->
|
||||
val vm: DenyListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
@@ -271,7 +264,9 @@ class MainActivity : AppCompatActivity(), SplashScreenHost {
|
||||
showInvalidState.value = true
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
AppMigration.restore(this@MainActivity)
|
||||
if (!AppMigration.restoreApp(this@MainActivity)) {
|
||||
toast(CoreR.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.topjohnwu.magisk.core.base.SplashScreenHost
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.ui.home.HomeScreen
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogScreen
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ModuleScreen
|
||||
@@ -85,9 +86,11 @@ fun MainScreen(initialTab: Int = Tab.HOME.ordinal) {
|
||||
when (Tab.entries[page]) {
|
||||
Tab.HOME -> {
|
||||
val vm: HomeViewModel = viewModel(factory = VMFactory)
|
||||
val installVm: InstallViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
CollectNavEvents(vm, navigator)
|
||||
HomeScreen(vm)
|
||||
CollectNavEvents(installVm, navigator)
|
||||
HomeScreen(vm, installVm)
|
||||
}
|
||||
Tab.SUPERUSER -> {
|
||||
val activity = LocalContext.current as MainActivity
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -13,20 +12,19 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import top.yukonga.miuix.kmp.basic.FloatingActionButton
|
||||
import com.topjohnwu.magisk.R
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
@@ -38,6 +36,7 @@ fun FlashScreen(viewModel: FlashViewModel, onBack: () -> Unit) {
|
||||
val showReboot by viewModel.showReboot.collectAsState()
|
||||
val items = viewModel.consoleItems
|
||||
val listState = rememberLazyListState()
|
||||
val finished = flashState != FlashViewModel.State.FLASHING
|
||||
|
||||
LaunchedEffect(items.size) {
|
||||
if (items.isNotEmpty()) {
|
||||
@@ -68,54 +67,55 @@ fun FlashScreen(viewModel: FlashViewModel, onBack: () -> Unit) {
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (finished) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
onClick = { viewModel.saveLog() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_save_md2),
|
||||
contentDescription = stringResource(CoreR.string.menuSaveLog),
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
if (flashState == FlashViewModel.State.SUCCESS && showReboot) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { viewModel.restartPressed() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_restart),
|
||||
contentDescription = stringResource(CoreR.string.reboot),
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
itemsIndexed(items) { _, line ->
|
||||
Text(
|
||||
text = line,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (flashState != FlashViewModel.State.FLASHING) {
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.menuSaveLog),
|
||||
onClick = { viewModel.saveLog() },
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(16.dp)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
itemsIndexed(items) { _, line ->
|
||||
Text(
|
||||
text = line,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
if (flashState == FlashViewModel.State.SUCCESS && showReboot) {
|
||||
FloatingActionButton(
|
||||
onClick = { viewModel.restartPressed() },
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.reboot),
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -65,10 +67,13 @@ import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
|
||||
import com.topjohnwu.magisk.ui.component.rememberLoadingDialog
|
||||
import com.topjohnwu.magisk.ui.component.ListPopupDefaults.MenuPositionProvider
|
||||
import com.topjohnwu.magisk.ui.flash.FlashUtils
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import kotlinx.coroutines.launch
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
import top.yukonga.miuix.kmp.basic.ButtonDefaults
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Checkbox
|
||||
import top.yukonga.miuix.kmp.basic.DropdownImpl
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
@@ -80,12 +85,15 @@ import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.extra.SuperArrow
|
||||
import top.yukonga.miuix.kmp.extra.SuperBottomSheet
|
||||
import top.yukonga.miuix.kmp.extra.SuperListPopup
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(viewModel: HomeViewModel) {
|
||||
fun HomeScreen(viewModel: HomeViewModel, installVm: InstallViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val installUiState by installVm.uiState.collectAsState()
|
||||
val context = LocalContext.current
|
||||
val activity = context as MainActivity
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
@@ -96,8 +104,34 @@ fun HomeScreen(viewModel: HomeViewModel) {
|
||||
val showUninstallDialog = rememberSaveable { mutableStateOf(false) }
|
||||
val showManagerDialog = rememberSaveable { mutableStateOf(false) }
|
||||
val showEnvFixDialog = rememberSaveable { mutableStateOf(false) }
|
||||
val showInstallSheet = rememberSaveable { mutableStateOf(false) }
|
||||
var envFixCode by remember { mutableIntStateOf(0) }
|
||||
|
||||
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let { installVm.onPatchFileSelected(it) }
|
||||
}
|
||||
|
||||
val secondSlotDialog = rememberConfirmDialog()
|
||||
val secondSlotTitle = stringResource(android.R.string.dialog_alert_title)
|
||||
val secondSlotMsg = stringResource(CoreR.string.install_inactive_slot_msg)
|
||||
|
||||
LaunchedEffect(installUiState.requestFilePicker) {
|
||||
if (installUiState.requestFilePicker) {
|
||||
filePicker.launch("*/*")
|
||||
installVm.onFilePickerConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(installUiState.showSecondSlotWarning) {
|
||||
if (installUiState.showSecondSlotWarning) {
|
||||
val result = secondSlotDialog.awaitConfirm(title = secondSlotTitle, content = secondSlotMsg)
|
||||
installVm.onSecondSlotWarningConsumed()
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
installVm.install()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.showUninstall) {
|
||||
if (uiState.showUninstall) {
|
||||
showUninstallDialog.value = true
|
||||
@@ -139,7 +173,7 @@ fun HomeScreen(viewModel: HomeViewModel) {
|
||||
code = envFixCode,
|
||||
activity = activity,
|
||||
loadingDialog = loadingDialog,
|
||||
onNavigateInstall = { viewModel.onMagiskPressed() },
|
||||
onNavigateInstall = { showInstallSheet.value = true },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -170,7 +204,10 @@ fun HomeScreen(viewModel: HomeViewModel) {
|
||||
NoticeCard(onHide = viewModel::hideNotice)
|
||||
}
|
||||
|
||||
MagiskCard(viewModel = viewModel)
|
||||
MagiskCard(
|
||||
viewModel = viewModel,
|
||||
onInstallClicked = { showInstallSheet.value = true }
|
||||
)
|
||||
|
||||
ManagerCard(viewModel = viewModel, uiState = uiState)
|
||||
|
||||
@@ -187,6 +224,12 @@ fun HomeScreen(viewModel: HomeViewModel) {
|
||||
DevelopersCard(onLinkClicked = { openLink(context, it) })
|
||||
}
|
||||
}
|
||||
|
||||
InstallBottomSheet(
|
||||
show = showInstallSheet,
|
||||
installVm = installVm,
|
||||
installUiState = installUiState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -276,7 +319,7 @@ private fun NoticeCard(onHide: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MagiskCard(viewModel: HomeViewModel) {
|
||||
private fun MagiskCard(viewModel: HomeViewModel, onInstallClicked: () -> Unit) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -296,11 +339,11 @@ private fun MagiskCard(viewModel: HomeViewModel) {
|
||||
when (viewModel.magiskState) {
|
||||
HomeViewModel.State.OUTDATED -> TextButton(
|
||||
text = stringResource(CoreR.string.update),
|
||||
onClick = { viewModel.onMagiskPressed() }
|
||||
onClick = onInstallClicked
|
||||
)
|
||||
else -> TextButton(
|
||||
text = stringResource(CoreR.string.install),
|
||||
onClick = { viewModel.onMagiskPressed() }
|
||||
onClick = onInstallClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -535,6 +578,139 @@ private fun openLink(context: Context, url: String) {
|
||||
} catch (_: ActivityNotFoundException) { }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstallBottomSheet(
|
||||
show: MutableState<Boolean>,
|
||||
installVm: InstallViewModel,
|
||||
installUiState: InstallViewModel.UiState,
|
||||
) {
|
||||
SuperBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = { show.value = false },
|
||||
title = stringResource(CoreR.string.install),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(bottom = 16.dp)) {
|
||||
if (installUiState.notes.isNotEmpty()) {
|
||||
Text(
|
||||
text = installUiState.notes,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (!installVm.skipOptions) {
|
||||
InstallOptionsSection(installUiState, installVm)
|
||||
}
|
||||
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.select_patch_file),
|
||||
summary = stringResource(CoreR.string.select_patch_file_summary),
|
||||
onClick = {
|
||||
show.value = false
|
||||
installVm.selectMethod(InstallViewModel.Method.PATCH)
|
||||
},
|
||||
enabled = installUiState.step >= 1 || installVm.skipOptions
|
||||
)
|
||||
|
||||
if (installVm.isRooted) {
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.direct_install),
|
||||
summary = stringResource(CoreR.string.direct_install_summary),
|
||||
onClick = {
|
||||
show.value = false
|
||||
installVm.selectMethod(InstallViewModel.Method.DIRECT)
|
||||
installVm.install()
|
||||
},
|
||||
enabled = installUiState.step >= 1 || installVm.skipOptions
|
||||
)
|
||||
}
|
||||
|
||||
if (!installVm.noSecondSlot) {
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.install_inactive_slot),
|
||||
summary = stringResource(CoreR.string.install_inactive_slot_summary),
|
||||
onClick = {
|
||||
show.value = false
|
||||
installVm.selectMethod(InstallViewModel.Method.INACTIVE_SLOT)
|
||||
},
|
||||
enabled = installUiState.step >= 1 || installVm.skipOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstallOptionsSection(
|
||||
uiState: InstallViewModel.UiState,
|
||||
viewModel: InstallViewModel
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.install_options_title),
|
||||
style = MiuixTheme.textStyles.headline2,
|
||||
)
|
||||
if (uiState.step == 0) {
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.install_next),
|
||||
onClick = { viewModel.nextStep() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.step == 0) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
if (!Info.isSAR) {
|
||||
CheckboxRow(
|
||||
label = stringResource(CoreR.string.keep_dm_verity),
|
||||
checked = Config.keepVerity,
|
||||
onCheckedChange = { Config.keepVerity = it }
|
||||
)
|
||||
}
|
||||
if (Info.isFDE) {
|
||||
CheckboxRow(
|
||||
label = stringResource(CoreR.string.keep_force_encryption),
|
||||
checked = Config.keepEnc,
|
||||
onCheckedChange = { Config.keepEnc = it }
|
||||
)
|
||||
}
|
||||
if (!Info.ramdisk) {
|
||||
CheckboxRow(
|
||||
label = stringResource(CoreR.string.recovery_mode),
|
||||
checked = Config.recovery,
|
||||
onCheckedChange = { Config.recovery = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckboxRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = { onCheckedChange(it) }
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UninstallComposableDialog(
|
||||
showDialog: MutableState<Boolean>,
|
||||
@@ -553,10 +729,7 @@ private fun UninstallComposableDialog(
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.restore_img),
|
||||
onClick = {
|
||||
@@ -570,9 +743,10 @@ private fun UninstallComposableDialog(
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spacer(Modifier.width(20.dp))
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.complete_uninstall),
|
||||
onClick = {
|
||||
@@ -583,7 +757,9 @@ private fun UninstallComposableDialog(
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.textButtonColorsPrimary()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -605,21 +781,21 @@ private fun ManagerInstallComposableDialog(
|
||||
text
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
onClick = { showDialog.value = false }
|
||||
onClick = { showDialog.value = false },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spacer(Modifier.width(20.dp))
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.install),
|
||||
onClick = {
|
||||
showDialog.value = false
|
||||
DownloadEngine.startWithActivity(activity, activity.extension, Subject.App())
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.textButtonColorsPrimary()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -651,15 +827,13 @@ private fun EnvFixComposableDialog(
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
onClick = { showDialog.value = false }
|
||||
onClick = { showDialog.value = false },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spacer(Modifier.width(20.dp))
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.ok),
|
||||
onClick = {
|
||||
@@ -682,7 +856,9 @@ private fun EnvFixComposableDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.textButtonColorsPrimary()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import com.topjohnwu.magisk.core.download.Subject.App
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -138,10 +137,6 @@ class HomeViewModel(
|
||||
_uiState.update { it.copy(envFixCode = 0) }
|
||||
}
|
||||
|
||||
fun onMagiskPressed() {
|
||||
navigateTo(Route.Install)
|
||||
}
|
||||
|
||||
fun hideNotice() {
|
||||
Config.safetyNotice = false
|
||||
_uiState.update { it.copy(isNoticeVisible = false) }
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.install
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Checkbox
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun InstallScreen(viewModel: InstallViewModel, onBack: () -> Unit) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let { viewModel.onPatchFileSelected(it) }
|
||||
}
|
||||
|
||||
val secondSlotDialog = rememberConfirmDialog()
|
||||
val secondSlotTitle = stringResource(android.R.string.dialog_alert_title)
|
||||
val secondSlotMsg = stringResource(CoreR.string.install_inactive_slot_msg)
|
||||
|
||||
LaunchedEffect(uiState.requestFilePicker) {
|
||||
if (uiState.requestFilePicker) {
|
||||
filePicker.launch("*/*")
|
||||
viewModel.onFilePickerConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.showSecondSlotWarning) {
|
||||
if (uiState.showSecondSlotWarning) {
|
||||
secondSlotDialog.showConfirm(title = secondSlotTitle, content = secondSlotMsg)
|
||||
viewModel.onSecondSlotWarningConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(CoreR.string.install),
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Back,
|
||||
contentDescription = null,
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(top = 8.dp, bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (!viewModel.skipOptions) {
|
||||
OptionsCard(uiState = uiState, viewModel = viewModel)
|
||||
}
|
||||
|
||||
MethodCard(uiState = uiState, viewModel = viewModel)
|
||||
|
||||
if (uiState.notes.isNotEmpty()) {
|
||||
NotesCard(notes = uiState.notes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OptionsCard(uiState: InstallViewModel.UiState, viewModel: InstallViewModel) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.install_options_title),
|
||||
style = MiuixTheme.textStyles.headline2,
|
||||
)
|
||||
if (uiState.step == 0) {
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.install_next),
|
||||
onClick = { viewModel.nextStep() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.step == 0) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (!Info.isSAR) {
|
||||
CheckboxRow(
|
||||
label = stringResource(CoreR.string.keep_dm_verity),
|
||||
checked = Config.keepVerity,
|
||||
onCheckedChange = { Config.keepVerity = it }
|
||||
)
|
||||
}
|
||||
if (Info.isFDE) {
|
||||
CheckboxRow(
|
||||
label = stringResource(CoreR.string.keep_force_encryption),
|
||||
checked = Config.keepEnc,
|
||||
onCheckedChange = { Config.keepEnc = it }
|
||||
)
|
||||
}
|
||||
if (!Info.ramdisk) {
|
||||
CheckboxRow(
|
||||
label = stringResource(CoreR.string.recovery_mode),
|
||||
checked = Config.recovery,
|
||||
onCheckedChange = { Config.recovery = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MethodCard(uiState: InstallViewModel.UiState, viewModel: InstallViewModel) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.install_method_title),
|
||||
style = MiuixTheme.textStyles.headline2,
|
||||
)
|
||||
if (uiState.step == 1) {
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.install_start),
|
||||
onClick = { viewModel.install() },
|
||||
enabled = viewModel.canInstall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.step == 1) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
MethodRadioRow(
|
||||
label = stringResource(CoreR.string.select_patch_file),
|
||||
selected = uiState.method == InstallViewModel.Method.PATCH,
|
||||
onClick = { viewModel.selectMethod(InstallViewModel.Method.PATCH) }
|
||||
)
|
||||
if (viewModel.isRooted) {
|
||||
MethodRadioRow(
|
||||
label = stringResource(CoreR.string.direct_install),
|
||||
selected = uiState.method == InstallViewModel.Method.DIRECT,
|
||||
onClick = { viewModel.selectMethod(InstallViewModel.Method.DIRECT) }
|
||||
)
|
||||
}
|
||||
if (!viewModel.noSecondSlot) {
|
||||
MethodRadioRow(
|
||||
label = stringResource(CoreR.string.install_inactive_slot),
|
||||
selected = uiState.method == InstallViewModel.Method.INACTIVE_SLOT,
|
||||
onClick = { viewModel.selectMethod(InstallViewModel.Method.INACTIVE_SLOT) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotesCard(notes: String) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = notes,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckboxRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = { onCheckedChange(it) }
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MethodRadioRow(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = selected,
|
||||
onCheckedChange = { onClick() }
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,9 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
|
||||
|
||||
fun onPatchFileSelected(uri: Uri) {
|
||||
_uiState.update { it.copy(patchUri = uri) }
|
||||
if (_uiState.value.method == Method.PATCH) {
|
||||
install()
|
||||
}
|
||||
}
|
||||
|
||||
fun install() {
|
||||
|
||||
@@ -10,12 +10,14 @@ import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||
import com.topjohnwu.magisk.core.su.SuEvents
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -25,6 +27,13 @@ class LogViewModel(
|
||||
private val repo: LogRepository
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
init {
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
viewModelScope.launch {
|
||||
SuEvents.logUpdated.debounce(500).collect { reload() }
|
||||
}
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = true,
|
||||
val magiskLog: String = "",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -13,20 +12,19 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import top.yukonga.miuix.kmp.basic.FloatingActionButton
|
||||
import com.topjohnwu.magisk.R
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
@@ -37,6 +35,7 @@ fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> U
|
||||
val actionState by viewModel.actionState.collectAsState()
|
||||
val items = viewModel.consoleItems
|
||||
val listState = rememberLazyListState()
|
||||
val finished = actionState != ActionViewModel.State.RUNNING
|
||||
|
||||
LaunchedEffect(items.size) {
|
||||
if (items.isNotEmpty()) {
|
||||
@@ -61,51 +60,42 @@ fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> U
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (finished) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { viewModel.saveLog() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_save_md2),
|
||||
contentDescription = stringResource(CoreR.string.menuSaveLog),
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
itemsIndexed(items) { _, line ->
|
||||
Text(
|
||||
text = line,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (actionState != ActionViewModel.State.RUNNING) {
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.menuSaveLog),
|
||||
onClick = { viewModel.saveLog() },
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(16.dp)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
itemsIndexed(items) { _, line ->
|
||||
Text(
|
||||
text = line,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.close),
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ sealed interface Route : NavKey, Parcelable {
|
||||
@Serializable
|
||||
data object Main : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data object Install : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data object DenyList : Route
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.ui.settings
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -20,6 +19,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -37,6 +37,8 @@ import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.utils.LocaleSetting
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.ui.component.rememberLoadingDialog
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.ButtonDefaults
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
@@ -45,6 +47,7 @@ import top.yukonga.miuix.kmp.basic.SmallTitle
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.basic.TextField
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import com.topjohnwu.magisk.ui.theme.ThemeState
|
||||
import top.yukonga.miuix.kmp.extra.SuperArrow
|
||||
import top.yukonga.miuix.kmp.extra.SuperDropdown
|
||||
import top.yukonga.miuix.kmp.extra.SuperSwitch
|
||||
@@ -123,6 +126,23 @@ private fun CustomizationSection(viewModel: SettingsViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
// Color Mode
|
||||
val resources = context.resources
|
||||
val colorModeEntries = remember {
|
||||
resources.getStringArray(CoreR.array.color_mode).toList()
|
||||
}
|
||||
var colorMode by remember { mutableIntStateOf(Config.colorMode) }
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.settings_color_mode),
|
||||
items = colorModeEntries,
|
||||
selectedIndex = colorMode,
|
||||
onSelectedIndexChange = { index ->
|
||||
colorMode = index
|
||||
Config.colorMode = index
|
||||
ThemeState.colorMode = index
|
||||
}
|
||||
)
|
||||
|
||||
if (isRunningAsStub && ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.add_shortcut_title),
|
||||
@@ -231,6 +251,8 @@ private fun AppSettingsSection(viewModel: SettingsViewModel) {
|
||||
|
||||
// Hide / Restore
|
||||
if (Info.env.isActive && Const.USER_ID == 0) {
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
val scope = rememberCoroutineScope()
|
||||
if (hidden) {
|
||||
var showRestoreDialog by remember { mutableStateOf(false) }
|
||||
RestoreDialog(
|
||||
@@ -238,7 +260,9 @@ private fun AppSettingsSection(viewModel: SettingsViewModel) {
|
||||
onDismiss = { showRestoreDialog = false },
|
||||
onConfirm = {
|
||||
showRestoreDialog = false
|
||||
viewModel.restoreApp(context as Activity)
|
||||
scope.launch {
|
||||
loadingDialog.withLoading { viewModel.restoreApp(context) }
|
||||
}
|
||||
}
|
||||
)
|
||||
SuperArrow(
|
||||
@@ -253,7 +277,9 @@ private fun AppSettingsSection(viewModel: SettingsViewModel) {
|
||||
onDismiss = { showHideDialog = false },
|
||||
onConfirm = { name ->
|
||||
showHideDialog = false
|
||||
viewModel.hideApp(context as Activity, name)
|
||||
scope.launch {
|
||||
loadingDialog.withLoading { viewModel.hideApp(context, name) }
|
||||
}
|
||||
}
|
||||
)
|
||||
SuperArrow(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.ui.settings
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
@@ -14,10 +14,12 @@ import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SettingsViewModel : BaseViewModel() {
|
||||
|
||||
@@ -36,12 +38,22 @@ class SettingsViewModel : BaseViewModel() {
|
||||
Shortcuts.addHomeIcon(AppContext)
|
||||
}
|
||||
|
||||
fun hideApp(activity: Activity, name: String) {
|
||||
viewModelScope.launch { AppMigration.hide(activity, name) }
|
||||
suspend fun hideApp(context: Context, name: String): Boolean {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
AppMigration.patchAndHide(context, name)
|
||||
}
|
||||
if (!success) {
|
||||
context.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun restoreApp(activity: Activity) {
|
||||
viewModelScope.launch { AppMigration.restore(activity) }
|
||||
suspend fun restoreApp(context: Context): Boolean {
|
||||
val success = AppMigration.restoreApp(context)
|
||||
if (!success) {
|
||||
context.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun createHosts() {
|
||||
|
||||
@@ -18,10 +18,12 @@ import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.su.SuEvents
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -52,6 +54,13 @@ class SuperuserViewModel(
|
||||
|
||||
var authenticate: (onSuccess: () -> Unit) -> Unit = { it() }
|
||||
|
||||
init {
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
viewModelScope.launch {
|
||||
SuEvents.policyChanged.debounce(500).collect { reload() }
|
||||
}
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = true,
|
||||
val policies: List<PolicyItem> = emptyList(),
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
package com.topjohnwu.magisk.ui.theme
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
||||
import top.yukonga.miuix.kmp.theme.LocalContentColor
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.theme.darkColorScheme
|
||||
import top.yukonga.miuix.kmp.theme.lightColorScheme
|
||||
import top.yukonga.miuix.kmp.theme.ThemeController
|
||||
|
||||
object ThemeState {
|
||||
var colorMode by mutableIntStateOf(Config.colorMode)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MagiskTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val darkTheme = when (Config.darkTheme) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> true
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> false
|
||||
else -> isSystemInDarkTheme()
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val mode = ThemeState.colorMode
|
||||
val controller = when (mode) {
|
||||
1 -> ThemeController(ColorSchemeMode.Light)
|
||||
2 -> ThemeController(ColorSchemeMode.Dark)
|
||||
3 -> ThemeController(ColorSchemeMode.MonetSystem, isDark = isDark)
|
||||
4 -> ThemeController(ColorSchemeMode.MonetLight)
|
||||
5 -> ThemeController(ColorSchemeMode.MonetDark)
|
||||
else -> ThemeController(ColorSchemeMode.System)
|
||||
}
|
||||
val colors = if (darkTheme) darkColorScheme() else lightColorScheme()
|
||||
MiuixTheme(colors = colors) {
|
||||
MiuixTheme(controller = controller) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides MiuixTheme.colorScheme.onBackground,
|
||||
content = content
|
||||
|
||||
@@ -38,6 +38,7 @@ object Config : PreferenceConfig, DBConfig {
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
const val LOCALE = "locale"
|
||||
const val DARK_THEME = "dark_theme_extended"
|
||||
const val COLOR_MODE = "color_mode"
|
||||
const val DOWNLOAD_DIR = "download_dir"
|
||||
const val SAFETY = "safety_notice"
|
||||
const val THEME_ORDINAL = "theme_ordinal"
|
||||
@@ -86,6 +87,7 @@ object Config : PreferenceConfig, DBConfig {
|
||||
// su notification
|
||||
const val NO_NOTIFICATION = 0
|
||||
const val NOTIFICATION_TOAST = 1
|
||||
const val NOTIFICATION_STATUS_BAR = 2
|
||||
|
||||
// su auto response
|
||||
const val SU_PROMPT = 0
|
||||
@@ -107,6 +109,7 @@ object Config : PreferenceConfig, DBConfig {
|
||||
var safetyNotice by preference(Key.SAFETY, true)
|
||||
var darkTheme by preference(Key.DARK_THEME, -1)
|
||||
var themeOrdinal by preference(Key.THEME_ORDINAL, 0)
|
||||
var colorMode by preference(Key.COLOR_MODE, 0)
|
||||
|
||||
private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true)
|
||||
private var localePrefs by preference(Key.LOCALE, "")
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -69,10 +70,12 @@ object SuCallbackHandler {
|
||||
}
|
||||
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
|
||||
|
||||
if (notify)
|
||||
notify(context, log.action >= SuPolicy.ALLOW, log.appName)
|
||||
|
||||
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||
|
||||
if (notify || Config.suNotification == Config.Value.NOTIFICATION_STATUS_BAR)
|
||||
notify(context, log.action >= SuPolicy.ALLOW, log.appName)
|
||||
SuEvents.notifyLogUpdated()
|
||||
SuEvents.notifyPolicyChanged()
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
@@ -87,16 +90,18 @@ object SuCallbackHandler {
|
||||
}.getOrNull() ?: "[UID] $uid"
|
||||
|
||||
notify(context, policy >= SuPolicy.ALLOW, appName)
|
||||
SuEvents.notifyPolicyChanged()
|
||||
}
|
||||
|
||||
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (granted)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast
|
||||
|
||||
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||
when (Config.suNotification) {
|
||||
Config.Value.NOTIFICATION_TOAST -> {
|
||||
val resId = if (granted) R.string.su_allow_toast else R.string.su_deny_toast
|
||||
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||
}
|
||||
Config.Value.NOTIFICATION_STATUS_BAR -> {
|
||||
Notifications.suNotification(granted, appName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
object SuEvents {
|
||||
private val _policyChanged = MutableSharedFlow<Unit>(extraBufferCapacity = 64)
|
||||
val policyChanged = _policyChanged.asSharedFlow()
|
||||
|
||||
private val _logUpdated = MutableSharedFlow<Unit>(extraBufferCapacity = 64)
|
||||
val logUpdated = _logUpdated.asSharedFlow()
|
||||
|
||||
fun notifyPolicyChanged() {
|
||||
_policyChanged.tryEmit(Unit)
|
||||
}
|
||||
|
||||
fun notifyLogUpdated() {
|
||||
_logUpdated.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,7 @@ class SuRequestHandler(
|
||||
}
|
||||
if (time >= 0) {
|
||||
policyDB.update(policy)
|
||||
SuEvents.notifyPolicyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,12 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.AppApkPath
|
||||
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.signing.JarMap
|
||||
import com.topjohnwu.magisk.core.signing.SignApk
|
||||
@@ -237,23 +234,6 @@ object AppMigration {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun hide(activity: Activity, label: String) {
|
||||
val dialog = android.app.ProgressDialog(activity).apply {
|
||||
setTitle(activity.getString(R.string.hide_app_title))
|
||||
isIndeterminate = true
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label)
|
||||
}
|
||||
if (!success) {
|
||||
dialog.dismiss()
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restoreApp(context: Context): Boolean {
|
||||
val apk = StubApk.current(context)
|
||||
val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME"
|
||||
@@ -266,20 +246,6 @@ object AppMigration {
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun restore(activity: Activity) {
|
||||
val dialog = android.app.ProgressDialog(activity).apply {
|
||||
setTitle(activity.getString(R.string.restore_img_msg))
|
||||
isIndeterminate = true
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
if (!restoreApp(activity)) {
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
suspend fun upgradeStub(context: Context, apk: File): Intent? {
|
||||
val label = context.applicationInfo.nonLocalizedLabel
|
||||
val pkg = context.packageName
|
||||
|
||||
@@ -28,6 +28,7 @@ object Notifications {
|
||||
private const val UPDATE_CHANNEL = "update"
|
||||
private const val PROGRESS_CHANNEL = "progress"
|
||||
private const val UPDATED_CHANNEL = "updated"
|
||||
private const val SU_CHANNEL = "su_notification"
|
||||
|
||||
private val nextId = AtomicInteger(APP_UPDATE_AVAILABLE_ID)
|
||||
|
||||
@@ -40,7 +41,9 @@ object Notifications {
|
||||
getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
|
||||
val channel3 = NotificationChannel(UPDATED_CHANNEL,
|
||||
getString(R.string.updated_channel), NotificationManager.IMPORTANCE_HIGH)
|
||||
mgr.createNotificationChannels(listOf(channel, channel2, channel3))
|
||||
val channel4 = NotificationChannel(SU_CHANNEL,
|
||||
getString(R.string.su_notification_channel), NotificationManager.IMPORTANCE_HIGH)
|
||||
mgr.createNotificationChannels(listOf(channel, channel2, channel3, channel4))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,5 +104,37 @@ object Notifications {
|
||||
return builder
|
||||
}
|
||||
|
||||
private const val SU_NOTIFICATION_TIMEOUT_MS = 3_000L
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun suNotification(granted: Boolean, appName: String) {
|
||||
AppContext.apply {
|
||||
val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val pending = PendingIntent.getActivity(this, 0, selfLaunchIntent(), flag)
|
||||
val title = getString(
|
||||
if (granted) R.string.su_notification_granted_title
|
||||
else R.string.su_notification_denied_title
|
||||
)
|
||||
val text = getString(
|
||||
if (granted) R.string.su_allow_toast
|
||||
else R.string.su_deny_toast,
|
||||
appName
|
||||
)
|
||||
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, SU_CHANNEL)
|
||||
.setSmallIcon(getBitmap(R.drawable.ic_magisk_outline).toIcon())
|
||||
} else {
|
||||
Notification.Builder(this).setPriority(Notification.PRIORITY_HIGH)
|
||||
.setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
}
|
||||
.setContentIntent(pending)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setAutoCancel(true)
|
||||
.setTimeoutAfter(SU_NOTIFICATION_TIMEOUT_MS)
|
||||
mgr.notify(nextId(), builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun nextId() = nextId.incrementAndGet()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<string-array name="su_notification">
|
||||
<item>@string/none</item>
|
||||
<item>@string/toast</item>
|
||||
<item>@string/notification</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="multiuser_mode">
|
||||
@@ -66,4 +67,13 @@
|
||||
<item>@string/settings_update_debug</item>
|
||||
<item>@string/settings_update_custom</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="color_mode">
|
||||
<item>@string/color_mode_system</item>
|
||||
<item>@string/color_mode_light</item>
|
||||
<item>@string/color_mode_dark</item>
|
||||
<item>@string/color_mode_monet_system</item>
|
||||
<item>@string/color_mode_monet_light</item>
|
||||
<item>@string/color_mode_monet_dark</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -40,7 +40,10 @@
|
||||
<string name="install_start">Let\'s go</string>
|
||||
<string name="manager_download_install">Press to download and install</string>
|
||||
<string name="direct_install">Direct install (Recommended)</string>
|
||||
<string name="direct_install_summary">Directly install Magisk to the boot partition</string>
|
||||
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
|
||||
<string name="install_inactive_slot_summary">Install to the inactive slot after an OTA update</string>
|
||||
<string name="select_patch_file_summary">Patch a raw image, ODIN tar, or payload.bin file</string>
|
||||
<string name="install_inactive_slot_msg">Your device will be FORCED to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
|
||||
<string name="setup_title">Additional setup</string>
|
||||
<string name="select_patch_file">Select and patch a file</string>
|
||||
@@ -73,7 +76,11 @@
|
||||
<string name="su_revoke_title">Revoke?</string>
|
||||
<string name="su_revoke_msg">Confirm to revoke %1$s Superuser rights</string>
|
||||
<string name="toast">Toast</string>
|
||||
<string name="notification">Notification</string>
|
||||
<string name="none">None</string>
|
||||
<string name="su_notification_channel">Superuser Notifications</string>
|
||||
<string name="su_notification_granted_title">Superuser Granted</string>
|
||||
<string name="su_notification_denied_title">Superuser Denied</string>
|
||||
<string name="superuser_toggle_notification">Notifications</string>
|
||||
<string name="superuser_toggle_revoke">Revoke</string>
|
||||
<string name="superuser_policy_none">No apps have asked for Superuser permission yet.</string>
|
||||
@@ -186,6 +193,13 @@
|
||||
<string name="settings_su_restrict_title">Restrict root capabilities</string>
|
||||
<string name="settings_su_restrict_summary">Will restrict new Superuser apps by default. Warning: this will break most apps. Don\'t enable it unless you know what you\'re doing.</string>
|
||||
<string name="settings_customization">Customization</string>
|
||||
<string name="settings_color_mode">Color mode</string>
|
||||
<string name="color_mode_system">System</string>
|
||||
<string name="color_mode_light">Light</string>
|
||||
<string name="color_mode_dark">Dark</string>
|
||||
<string name="color_mode_monet_system">Monet (System)</string>
|
||||
<string name="color_mode_monet_light">Monet (Light)</string>
|
||||
<string name="color_mode_monet_dark">Monet (Dark)</string>
|
||||
<string name="setting_add_shortcut_summary">Add a pretty shortcut to the home screen in case the name and icon are difficult to recognize after hiding the app</string>
|
||||
<string name="settings_doh_title">DNS over HTTPS</string>
|
||||
<string name="settings_doh_description">Workaround DNS poisoning in some nations</string>
|
||||
|
||||
Reference in New Issue
Block a user