Replace Fragment navigation with Navigation3 and Compose-based UI

- Add Navigation3, serialization, miuix-icons dependencies
- Create Routes.kt, Navigator.kt for type-safe navigation
- Add per-screen TopAppBar/SmallTopAppBar with miuix Scaffold
- Rewrite MainActivity: UIActivity + setContent + NavDisplay + HorizontalPager
- Create MainScreen with bottom NavigationBar and tab paging
- Delete all 9 Fragment files and XML navigation/layout/menu resources
- Remove NavigationEvent, replace with SharedFlow<Route> in BaseViewModel
- Update ViewModels to use navigateTo(Route) instead of NavDirections
- Extract FlashUtils.installIntent() for PendingIntent creation
- Add ObserveViewEvents/CollectNavEvents Compose helpers for event dispatch

Made-with: Cursor
This commit is contained in:
LoveSy
2026-03-03 13:28:20 +08:00
committed by topjohnwu
parent e000607d71
commit 2cab7d6c7b
45 changed files with 964 additions and 1409 deletions
+3
View File
@@ -13,3 +13,6 @@ native/out
*.iml
.idea
.cursor
ramdisk.img
app/core/src/debug
app/core/src/release
+8
View File
@@ -2,6 +2,7 @@ plugins {
id("com.android.application")
kotlin("plugin.parcelize")
kotlin("plugin.compose")
kotlin("plugin.serialization")
alias(libs.plugins.legacy.kapt)
alias(libs.plugins.navigation.safeargs)
}
@@ -68,6 +69,13 @@ dependencies {
implementation(libs.lifecycle.runtime.compose)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.miuix)
implementation(libs.miuix.icons)
implementation(libs.miuix.navigation3.ui)
// Navigation3
implementation(libs.navigation3.runtime)
implementation(libs.navigationevent.compose)
implementation(libs.lifecycle.viewmodel.navigation3)
// Make sure kapt runs with a proper kotlin-stdlib
kapt(kotlin("stdlib"))
@@ -9,15 +9,16 @@ import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.events.BackPressEvent
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.events.DialogEvent
import com.topjohnwu.magisk.events.NavigationEvent
import com.topjohnwu.magisk.events.PermissionEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ui.navigation.Route
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
abstract class BaseViewModel : ViewModel(), ObservableHost {
@@ -26,6 +27,9 @@ abstract class BaseViewModel : ViewModel(), ObservableHost {
private val _viewEvents = MutableLiveData<ViewEvent>()
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
private val _navEvents = MutableSharedFlow<Route>(extraBufferCapacity = 1)
val navEvents: SharedFlow<Route> = _navEvents
open fun onSaveState(state: Bundle) {}
open fun onRestoreState(state: Bundle) {}
open fun onNetworkChanged(network: Boolean) {}
@@ -76,8 +80,8 @@ abstract class BaseViewModel : ViewModel(), ObservableHost {
DialogEvent(this).publish()
}
fun NavDirections.navigate(pop: Boolean = false) {
_viewEvents.postValue(NavigationEvent(this, pop))
fun navigateTo(route: Route) {
_navEvents.tryEmit(route)
}
}
@@ -133,7 +133,6 @@ fun ViewGroup.startAnimations() {
val transition = AutoTransition()
.setInterpolator(FastOutSlowInInterpolator())
.setDuration(400)
.excludeTarget(R.id.main_toolbar, true)
TransitionManager.beginDelayedTransition(
this,
transition
@@ -1,11 +1,11 @@
package com.topjohnwu.magisk.dialog
import android.net.Uri
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.magisk.view.MagiskDialog
class LocalModuleInstallDialog(
@@ -20,9 +20,9 @@ class LocalModuleInstallDialog(
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
viewModel.apply {
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate()
}
viewModel.navigateTo(
Route.Flash(Const.Value.FLASH_ZIP, uri.toString())
)
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
@@ -6,7 +6,7 @@ import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.ui.flash.FlashUtils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Notifications
import kotlinx.parcelize.Parcelize
@@ -26,7 +26,7 @@ class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog
override val autoLaunch: Boolean,
override val notifyId: Int = Notifications.nextId()
) : Subject.Module() {
override fun pendingIntent(context: Context) = FlashFragment.installIntent(context, file)
override fun pendingIntent(context: Context) = FlashUtils.installIntent(context, file)
}
override fun build(dialog: MagiskDialog) {
@@ -3,13 +3,13 @@ package com.topjohnwu.magisk.dialog
import android.app.ProgressDialog
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.ui.flash.FlashUtils
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.launch
@@ -50,8 +50,14 @@ class UninstallDialog : DialogBuilder {
}
private fun completeUninstall(dialog: MagiskDialog) {
(dialog.ownerActivity as NavigationActivity<*>)
.navigation.navigate(FlashFragment.uninstall())
val activity = dialog.ownerActivity ?: return
val intent = android.content.Intent(activity, activity.javaClass).apply {
action = FlashUtils.INTENT_FLASH
putExtra(FlashUtils.EXTRA_FLASH_ACTION, Const.Value.UNINSTALL)
flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or
android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
}
activity.startActivity(intent)
}
}
@@ -3,11 +3,9 @@ package com.topjohnwu.magisk.events
import android.content.Context
import android.view.View
import androidx.annotation.StringRes
import androidx.navigation.NavDirections
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.base.ContentResultCallback
@@ -70,18 +68,6 @@ class GetContentEvent(
}
}
class NavigationEvent(
private val directions: NavDirections,
private val pop: Boolean
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
(activity as? NavigationActivity<*>)?.apply {
if (pop) navigation.popBackStack()
directions.navigate()
}
}
}
class AddHomeIconEvent : ViewEvent(), ContextExecutor {
override fun invoke(context: Context) {
Shortcuts.addHomeIcon(context)
@@ -5,22 +5,29 @@ import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.forEach
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.net.toUri
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.startAnimations
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
@@ -29,40 +36,43 @@ import com.topjohnwu.magisk.core.base.SplashController
import com.topjohnwu.magisk.core.base.SplashScreenHost
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ui.deny.DenyListScreen
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.navigation.CollectNavEvents
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
import com.topjohnwu.magisk.ui.navigation.Navigator
import com.topjohnwu.magisk.ui.navigation.ObserveViewEvents
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.magisk.ui.navigation.rememberNavigator
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.io.File
import androidx.compose.runtime.Composable
import com.topjohnwu.magisk.core.R as CoreR
class MainViewModel : BaseViewModel()
class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenHost {
class MainActivity : UIActivity<ViewDataBinding>(), SplashScreenHost {
override val layoutRes = R.layout.activity_main_md2
override val layoutRes = 0
override val viewModel by viewModel<MainViewModel>()
override val navHostId: Int = R.id.main_nav_host
override val splashController = SplashController(this)
override val snackbarView: View
get() {
val fragmentOverride = currentFragment?.snackbarView
return fragmentOverride ?: super.snackbarView
}
override val snackbarAnchorView: View?
get() {
val fragmentAnchor = currentFragment?.snackbarAnchorView
return when {
fragmentAnchor?.isVisible == true -> fragmentAnchor
binding.mainNavigation.isVisible -> return binding.mainNavigation
else -> null
}
}
get() = window.decorView.findViewById(android.R.id.content)
override val snackbarAnchorView: View? get() = null
private var isRootFragment = true
private val intentState = MutableStateFlow(0)
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes)
@@ -78,11 +88,9 @@ class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenH
@SuppressLint("InlinedApi")
override fun onCreateUi(savedInstanceState: Bundle?) {
setContentView()
showUnsupportedMessage()
askForHomeShortcut()
// Ask permission to post notifications for background update check
if (Config.checkUpdate) {
withPermission(Manifest.permission.POST_NOTIFICATIONS) {
Config.checkUpdate = it
@@ -91,101 +99,99 @@ class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenH
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
navigation.addOnDestinationChangedListener { _, destination, _ ->
isRootFragment = when (destination.id) {
R.id.homeFragment,
R.id.modulesFragment,
R.id.superuserFragment,
R.id.logFragment,
R.id.settingsFragment -> true
else -> false
val initialTab = getInitialTab(intent)
setContent {
MagiskTheme {
val navigator = rememberNavigator(Route.Main)
CompositionLocalProvider(LocalNavigator provides navigator) {
HandleFlashIntent(navigator)
NavDisplay(
backStack = navigator.backStack,
onBack = { navigator.pop() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Route.Main> {
MainScreen(initialTab = initialTab)
}
entry<Route.Install> { _ ->
val vm: InstallViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
ObserveViewEvents(vm)
CollectNavEvents(vm, navigator)
InstallScreen(vm, onBack = { navigator.pop() })
}
entry<Route.DenyList> { _ ->
val vm: DenyListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
ObserveViewEvents(vm)
DenyListScreen(vm, onBack = { navigator.pop() })
}
entry<Route.Flash> { key ->
val vm: FlashViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
LaunchedEffect(key) {
if (vm.flashAction.isEmpty()) {
vm.flashAction = key.action
vm.flashUri = key.additionalData?.let { Uri.parse(it) }
vm.startFlashing()
}
}
ObserveViewEvents(vm)
FlashScreen(vm, onBack = { navigator.pop() })
}
entry<Route.Action> { key ->
val vm: ActionViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
LaunchedEffect(key) {
if (vm.actionId.isEmpty()) {
vm.actionId = key.id
vm.actionName = key.name
vm.startRunAction()
}
}
ObserveViewEvents(vm)
ActionScreen(vm, actionName = key.name, onBack = { navigator.pop() })
}
}
)
}
setDisplayHomeAsUpEnabled(!isRootFragment)
requestNavigationHidden(!isRootFragment)
binding.mainNavigation.menu.forEach {
if (it.itemId == destination.id) {
it.isChecked = true
}
}
}
setSupportActionBar(binding.mainToolbar)
binding.mainNavigation.setOnItemSelectedListener {
getScreen(it.itemId)?.navigate()
true
@Composable
private fun HandleFlashIntent(navigator: Navigator) {
val intentVersion by intentState.collectAsState()
LaunchedEffect(intentVersion) {
val currentIntent = intent ?: return@LaunchedEffect
if (currentIntent.action == FlashUtils.INTENT_FLASH) {
val action = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_ACTION)
?: return@LaunchedEffect
val uri = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_URI)
navigator.push(Route.Flash(action, uri))
currentIntent.action = null
}
binding.mainNavigation.setOnItemReselectedListener {
// https://issuetracker.google.com/issues/124538620
}
binding.mainNavigation.menu.apply {
findItem(R.id.superuserFragment)?.isEnabled = Info.showSuperUser
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded()
}
val section =
if (intent.action == Intent.ACTION_APPLICATION_PREFERENCES)
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
intentState.value += 1
}
private fun getInitialTab(intent: Intent?): Int {
val section = if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) {
Const.Nav.SETTINGS
else
intent.getStringExtra(Const.Key.OPEN_SECTION)
getScreen(section)?.navigate()
if (!isRootFragment) {
requestNavigationHidden(requiresAnimation = savedInstanceState == null)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
else -> return super.onOptionsItemSelected(item)
}
return true
}
fun setDisplayHomeAsUpEnabled(isEnabled: Boolean) {
binding.mainToolbar.startAnimations()
when {
isEnabled -> binding.mainToolbar.setNavigationIcon(R.drawable.ic_back_md2)
else -> binding.mainToolbar.navigationIcon = null
}
}
internal fun requestNavigationHidden(hide: Boolean = true, requiresAnimation: Boolean = true) {
val bottomView = binding.mainNavigation
if (requiresAnimation) {
bottomView.isVisible = true
bottomView.isHidden = hide
} else {
bottomView.isGone = hide
intent?.getStringExtra(Const.Key.OPEN_SECTION)
}
}
fun invalidateToolbar() {
//binding.mainToolbar.startAnimations()
binding.mainToolbar.invalidate()
}
private fun getScreen(name: String?): NavDirections? {
return when (name) {
Const.Nav.SUPERUSER -> MainDirections.actionSuperuserFragment()
Const.Nav.MODULES -> MainDirections.actionModuleFragment()
Const.Nav.SETTINGS -> MainDirections.actionSettingsFragment()
else -> null
}
}
private fun getScreen(id: Int): NavDirections? {
return when (id) {
R.id.homeFragment -> MainDirections.actionHomeFragment()
R.id.modulesFragment -> MainDirections.actionModuleFragment()
R.id.superuserFragment -> MainDirections.actionSuperuserFragment()
R.id.logFragment -> MainDirections.actionLogFragment()
R.id.settingsFragment -> MainDirections.actionSettingsFragment()
else -> null
return when (section) {
Const.Nav.SUPERUSER -> Tab.SUPERUSER.ordinal
Const.Nav.MODULES -> Tab.MODULES.ordinal
Const.Nav.SETTINGS -> Tab.SETTINGS.ordinal
else -> Tab.HOME.ordinal
}
}
@@ -226,8 +232,8 @@ class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenH
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
?.split(':')
?.filterNot { File("$it/magisk").exists() }
?.any { File("$it/su").exists() } == true) {
?.filterNot { java.io.File("$it/magisk").exists() }
?.any { java.io.File("$it/su").exists() } == true) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_general_title)
setMessage(CoreR.string.unsupport_other_su_msg)
@@ -258,7 +264,6 @@ class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenH
private fun askForHomeShortcut() {
if (isRunningAsStub && !Config.askedHome &&
ShortcutManagerCompat.isRequestPinShortcutSupported(this)) {
// Ask and show dialog
Config.askedHome = true
MagiskDialog(this).apply {
setTitle(CoreR.string.add_shortcut_title)
@@ -0,0 +1,133 @@
package com.topjohnwu.magisk.ui
import android.net.Uri
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.viewmodel.compose.viewModel
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.ui.deny.DenyListScreen
import com.topjohnwu.magisk.ui.deny.DenyListViewModel
import com.topjohnwu.magisk.ui.flash.FlashScreen
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import com.topjohnwu.magisk.ui.home.HomeScreen
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallScreen
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.ActionScreen
import com.topjohnwu.magisk.ui.module.ActionViewModel
import com.topjohnwu.magisk.ui.module.ModuleScreen
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.navigation.CollectNavEvents
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
import com.topjohnwu.magisk.ui.navigation.Navigator
import com.topjohnwu.magisk.ui.navigation.ObserveViewEvents
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.magisk.ui.settings.SettingsScreen
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserScreen
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.NavigationBar
import top.yukonga.miuix.kmp.basic.NavigationBarItem
import top.yukonga.miuix.kmp.basic.NavigationItem
import com.topjohnwu.magisk.core.R as CoreR
enum class Tab(val titleRes: Int, val iconRes: Int) {
HOME(CoreR.string.section_home, R.drawable.ic_home_md2),
SUPERUSER(CoreR.string.superuser, R.drawable.ic_superuser_md2),
LOG(CoreR.string.logs, R.drawable.ic_bug_md2),
MODULES(CoreR.string.modules, R.drawable.ic_module_md2),
SETTINGS(CoreR.string.settings, R.drawable.ic_settings_md2);
}
@Composable
fun MainScreen(initialTab: Int = 0) {
val navigator = LocalNavigator.current
val pagerState = rememberPagerState(initialPage = initialTab, pageCount = { Tab.entries.size })
val scope = rememberCoroutineScope()
val items = Tab.entries.map { tab ->
NavigationItem(
label = stringResource(tab.titleRes),
icon = ImageVector.vectorResource(tab.iconRes),
)
}
Column(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f),
beyondViewportPageCount = Tab.entries.size - 1,
userScrollEnabled = true,
) { page ->
when (Tab.entries[page]) {
Tab.HOME -> {
val vm: HomeViewModel = viewModel(factory = VMFactory)
ObserveViewEvents(vm)
CollectNavEvents(vm, navigator)
HomeScreen(vm)
}
Tab.SUPERUSER -> {
val vm: SuperuserViewModel = viewModel(factory = VMFactory)
ObserveViewEvents(vm)
SuperuserScreen(vm)
}
Tab.LOG -> {
val vm: LogViewModel = viewModel(factory = VMFactory)
ObserveViewEvents(vm)
LogScreen(vm)
}
Tab.MODULES -> {
val vm: ModuleViewModel = viewModel(factory = VMFactory)
ObserveViewEvents(vm)
CollectNavEvents(vm, navigator)
ModuleScreen(vm)
}
Tab.SETTINGS -> {
val vm: SettingsViewModel = viewModel(factory = VMFactory)
ObserveViewEvents(vm)
CollectNavEvents(vm, navigator)
SettingsScreen(vm)
}
}
}
NavigationBar {
items.forEachIndexed { index, item ->
val tab = Tab.entries[index]
val enabled = when (tab) {
Tab.SUPERUSER -> Info.showSuperUser
Tab.MODULES -> Info.env.isActive && LocalModule.loaded()
else -> true
}
NavigationBarItem(
modifier = Modifier.weight(1f),
icon = item.icon,
label = item.label,
selected = pagerState.currentPage == index,
enabled = enabled,
onClick = {
scope.launch { pagerState.animateScrollToPage(index) }
}
)
}
}
}
}
@@ -1,63 +0,0 @@
package com.topjohnwu.magisk.ui.deny
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class DenyListFragment : Fragment(), ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[DenyListViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onStart() {
super.onStart()
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.denylist)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
DenyListScreen(viewModel = viewModel as DenyListViewModel)
}
}
}
}
override fun onResume() {
super.onResume()
(viewModel as DenyListViewModel).startLoading()
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}
@@ -32,22 +32,49 @@ import androidx.compose.ui.unit.dp
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Checkbox
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.Switch
import top.yukonga.miuix.kmp.basic.Text
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 DenyListScreen(viewModel: DenyListViewModel) {
fun DenyListScreen(viewModel: DenyListViewModel, onBack: () -> Unit) {
val loading by viewModel.loading.collectAsState()
val apps by viewModel.filteredApps.collectAsState()
val query by viewModel.query.collectAsState()
val showSystem by viewModel.showSystem.collectAsState()
val showOS by viewModel.showOS.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
// Search input
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.denylist),
navigationIcon = {
IconButton(
modifier = Modifier.padding(start = 16.dp),
onClick = onBack
) {
Icon(
imageVector = MiuixIcons.Back,
contentDescription = null,
tint = MiuixTheme.colorScheme.onBackground
)
}
},
scrollBehavior = scrollBehavior
)
}
) { padding ->
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
SearchInput(
query = query,
onQueryChange = viewModel::setQuery,
@@ -56,7 +83,6 @@ fun DenyListScreen(viewModel: DenyListViewModel) {
.padding(horizontal = 12.dp, vertical = 4.dp)
)
// Filter chips
Row(
modifier = Modifier
.fillMaxWidth()
@@ -109,6 +135,7 @@ fun DenyListScreen(viewModel: DenyListViewModel) {
}
}
}
}
}
@Composable
@@ -153,7 +180,6 @@ private fun FilterChip(
private fun DenyAppCard(app: DenyAppState) {
Card(modifier = Modifier.fillMaxWidth()) {
Column {
// Progress bar showing percentage of checked processes
if (app.checkedPercent > 0f) {
LinearProgressIndicator(
progress = app.checkedPercent,
@@ -161,7 +187,6 @@ private fun DenyAppCard(app: DenyAppState) {
)
}
// App row
Row(
modifier = Modifier
.fillMaxWidth()
@@ -192,7 +217,6 @@ private fun DenyAppCard(app: DenyAppState) {
)
}
// Expanded process list
AnimatedVisibility(visible = app.isExpanded) {
Column(
modifier = Modifier
@@ -1,144 +0,0 @@
package com.topjohnwu.magisk.ui.flash
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavDeepLinkBuilder
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class FlashFragment : Fragment(), ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[FlashViewModel::class.java]
}
private var defaultOrientation = -1
private val backCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if ((viewModel as FlashViewModel).flashing.value != true) {
isEnabled = false
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
(viewModel as FlashViewModel).args = FlashFragmentArgs.fromBundle(requireArguments())
activity?.onBackPressedDispatcher?.addCallback(this, backCallback)
}
override fun onStart() {
super.onStart()
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.flash_screen_title)
(viewModel as FlashViewModel).state.observe(this) {
(activity as? androidx.appcompat.app.AppCompatActivity)?.supportActionBar?.setSubtitle(
when (it) {
FlashViewModel.State.FLASHING -> CoreR.string.flashing
FlashViewModel.State.SUCCESS -> CoreR.string.done
FlashViewModel.State.FAILED -> CoreR.string.failure
}
)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
defaultOrientation = activity?.requestedOrientation ?: -1
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
if (savedInstanceState == null) {
(viewModel as FlashViewModel).startFlashing()
}
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
FlashScreen(viewModel = viewModel as FlashViewModel)
}
}
}
}
@SuppressLint("WrongConstant")
override fun onDestroyView() {
if (defaultOrientation != -1) {
activity?.requestedOrientation = defaultOrientation
}
super.onDestroyView()
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
companion object {
private fun createIntent(context: Context, args: FlashFragmentArgs) =
NavDeepLinkBuilder(context)
.setGraph(R.navigation.main)
.setComponentName(MainActivity::class.java.cmp(context.packageName))
.setDestination(R.id.flashFragment)
.setArguments(args.toBundle())
.createPendingIntent()
private fun flashType(isSecondSlot: Boolean) =
if (isSecondSlot) Const.Value.FLASH_INACTIVE_SLOT else Const.Value.FLASH_MAGISK
fun flash(isSecondSlot: Boolean) = MainDirections.actionFlashFragment(
action = flashType(isSecondSlot)
)
fun patch(uri: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.PATCH_FILE,
additionalData = uri
)
fun uninstall() = MainDirections.actionFlashFragment(
action = Const.Value.UNINSTALL
)
fun installIntent(context: Context, file: Uri) = FlashFragmentArgs(
action = Const.Value.FLASH_ZIP,
additionalData = file,
).let { createIntent(context, it) }
fun install(file: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.FLASH_ZIP,
additionalData = file,
)
}
}
@@ -20,13 +20,20 @@ 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 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
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun FlashScreen(viewModel: FlashViewModel) {
fun FlashScreen(viewModel: FlashViewModel, onBack: () -> Unit) {
val flashState by viewModel.flashState.collectAsState()
val showReboot by viewModel.showReboot.collectAsState()
val items = viewModel.consoleItems
@@ -38,7 +45,34 @@ fun FlashScreen(viewModel: FlashViewModel) {
}
}
Box(modifier = Modifier.fillMaxSize()) {
val statusText = when (flashState) {
FlashViewModel.State.FLASHING -> stringResource(CoreR.string.flashing)
FlashViewModel.State.SUCCESS -> stringResource(CoreR.string.done)
FlashViewModel.State.FAILED -> stringResource(CoreR.string.failure)
}
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
SmallTopAppBar(
title = "${stringResource(CoreR.string.flash_screen_title)} - $statusText",
navigationIcon = {
IconButton(
modifier = Modifier.padding(start = 16.dp),
onClick = onBack
) {
Icon(
imageVector = MiuixIcons.Back,
contentDescription = null,
tint = MiuixTheme.colorScheme.onBackground
)
}
},
scrollBehavior = scrollBehavior
)
}
) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
LazyColumn(
state = listState,
modifier = Modifier
@@ -82,4 +116,5 @@ fun FlashScreen(viewModel: FlashViewModel) {
}
}
}
}
}
@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.ui.flash
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.ui.MainActivity
object FlashUtils {
const val INTENT_FLASH = "com.topjohnwu.magisk.intent.FLASH"
const val EXTRA_FLASH_ACTION = "flash_action"
const val EXTRA_FLASH_URI = "flash_uri"
fun installIntent(context: Context, file: Uri): PendingIntent {
val intent = Intent(context, MainActivity::class.java).apply {
component = MainActivity::class.java.cmp(context.packageName)
action = INTENT_FLASH
putExtra(EXTRA_FLASH_ACTION, Const.Value.FLASH_ZIP)
putExtra(EXTRA_FLASH_URI, file.toString())
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
return PendingIntent.getActivity(
context, file.hashCode(), intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
}
@@ -41,7 +41,8 @@ class FlashViewModel : BaseViewModel() {
val showReboot: StateFlow<Boolean> = _showReboot.asStateFlow()
val consoleItems = mutableStateListOf<String>()
lateinit var args: FlashFragmentArgs
var flashAction: String = ""
var flashUri: android.net.Uri? = null
private val logItems = mutableListOf<String>().synchronized()
private val outItems = object : CallbackList<String>() {
@@ -53,7 +54,8 @@ class FlashViewModel : BaseViewModel() {
}
fun startFlashing() {
val (action, uri) = args
val action = flashAction
val uri = flashUri
viewModelScope.launch {
val result = when (action) {
@@ -1,93 +0,0 @@
package com.topjohnwu.magisk.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class HomeFragment : Fragment(), MenuProvider, ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[HomeViewModel::class.java]
}
override fun onStart() {
super.onStart()
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.section_home)
DownloadEngine.observeProgress(this, viewModel::onProgressUpdate)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
HomeScreen(viewModel = viewModel)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_home_md2, menu)
if (!Info.isRooted)
menu.removeItem(R.id.action_reboot)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_reboot -> (activity as? NavigationActivity<*>)?.let {
RebootMenu.inflate(it).show()
}
else -> return false
}
return true
}
override fun onResume() {
super.onResume()
viewModel.resetProgress()
}
}
@@ -33,20 +33,33 @@ import com.topjohnwu.magisk.core.R as CoreR
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
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.theme.MiuixTheme
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
val uiState by viewModel.uiState.collectAsState()
val context = LocalContext.current
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.section_home),
scrollBehavior = scrollBehavior
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(top = 8.dp, bottom = 16.dp),
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (uiState.isNoticeVisible) {
@@ -69,6 +82,7 @@ fun HomeScreen(viewModel: HomeViewModel) {
DevelopersCard(onLinkClicked = { openLink(context, it) })
}
}
}
@Composable
@@ -20,6 +20,7 @@ import com.topjohnwu.magisk.dialog.EnvFixDialog
import com.topjohnwu.magisk.dialog.ManagerInstallDialog
import com.topjohnwu.magisk.dialog.UninstallDialog
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -122,7 +123,7 @@ class HomeViewModel(
}
fun onMagiskPressed() = withExternalRW {
HomeFragmentDirections.actionHomeFragmentToInstallFragment().navigate()
navigateTo(Route.Install)
}
fun hideNotice() {
@@ -34,7 +34,7 @@ object RebootMenu {
fun inflate(activity: Activity): PopupMenu {
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
val menu = PopupMenu(themeWrapper, activity.window.decorView)
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
menu.setOnMenuItemClickListener(RebootMenu::reboot)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
@@ -1,58 +0,0 @@
package com.topjohnwu.magisk.ui.install
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class InstallFragment : Fragment(), ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[InstallViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onStart() {
super.onStart()
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.install)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
InstallScreen(viewModel = viewModel as InstallViewModel)
}
}
}
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}
@@ -21,19 +21,48 @@ import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
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) {
fun InstallScreen(viewModel: InstallViewModel, onBack: () -> Unit) {
val uiState by viewModel.uiState.collectAsState()
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
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(padding)
.padding(horizontal = 12.dp)
.padding(top = 8.dp, bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
@@ -48,6 +77,7 @@ fun InstallScreen(viewModel: InstallViewModel) {
NotesCard(notes = uiState.notes)
}
}
}
}
@Composable
@@ -15,7 +15,8 @@ import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.magisk.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.events.GetContentEvent
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.ui.navigation.Route
import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -96,9 +97,16 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
fun install() {
when (_uiState.value.method) {
Method.PATCH -> FlashFragment.patch(data.value!!).navigate(true)
Method.DIRECT -> FlashFragment.flash(false).navigate(true)
Method.INACTIVE_SLOT -> FlashFragment.flash(true).navigate(true)
Method.PATCH -> navigateTo(Route.Flash(
action = Const.Value.PATCH_FILE,
additionalData = data.value!!.toString()
))
Method.DIRECT -> navigateTo(Route.Flash(
action = Const.Value.FLASH_MAGISK
))
Method.INACTIVE_SLOT -> navigateTo(Route.Flash(
action = Const.Value.FLASH_INACTIVE_SLOT
))
else -> error("Unknown method")
}
}
@@ -1,63 +0,0 @@
package com.topjohnwu.magisk.ui.log
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class LogFragment : Fragment(), ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[LogViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onStart() {
super.onStart()
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.logs)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
LogScreen(viewModel = viewModel as LogViewModel)
}
}
}
}
override fun onResume() {
super.onResume()
(viewModel as LogViewModel).startLoading()
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}
@@ -32,9 +32,12 @@ import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.model.su.SuLog
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.TabRow
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.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@@ -46,8 +49,20 @@ fun LogScreen(viewModel: LogViewModel) {
stringResource(CoreR.string.superuser),
stringResource(CoreR.string.magisk)
)
val scrollBehavior = MiuixScrollBehavior()
Column(modifier = Modifier.fillMaxSize()) {
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.logs),
scrollBehavior = scrollBehavior
)
}
) { padding ->
Column(modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
TabRow(
tabs = tabTitles,
selectedTabIndex = selectedTab,
@@ -76,6 +91,7 @@ fun LogScreen(viewModel: LogViewModel) {
}
}
}
}
}
@Composable
@@ -1,104 +0,0 @@
package com.topjohnwu.magisk.ui.module
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class ActionFragment : Fragment(), ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[ActionViewModel::class.java]
}
private var defaultOrientation = -1
private val backCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if ((viewModel as ActionViewModel).state.value != ActionViewModel.State.RUNNING) {
isEnabled = false
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
(viewModel as ActionViewModel).args = ActionFragmentArgs.fromBundle(requireArguments())
activity?.onBackPressedDispatcher?.addCallback(this, backCallback)
}
override fun onStart() {
super.onStart()
val vm = viewModel as ActionViewModel
(activity as? NavigationActivity<*>)?.setTitle(vm.args.name)
vm.state.observe(this) {
if (it == ActionViewModel.State.SUCCESS) {
context?.toast(
getString(CoreR.string.done_action, vm.args.name),
Toast.LENGTH_SHORT
)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
defaultOrientation = activity?.requestedOrientation ?: -1
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
if (savedInstanceState == null) {
(viewModel as ActionViewModel).startRunAction()
}
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
ActionScreen(
viewModel = viewModel as ActionViewModel,
onClose = { activity?.onBackPressedDispatcher?.onBackPressed() }
)
}
}
}
}
@SuppressLint("WrongConstant")
override fun onDestroyView() {
if (defaultOrientation != -1) {
activity?.requestedOrientation = defaultOrientation
}
super.onDestroyView()
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}
@@ -20,13 +20,20 @@ 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 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
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun ActionScreen(viewModel: ActionViewModel, onClose: () -> Unit) {
fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> Unit) {
val actionState by viewModel.actionState.collectAsState()
val items = viewModel.consoleItems
val listState = rememberLazyListState()
@@ -37,7 +44,28 @@ fun ActionScreen(viewModel: ActionViewModel, onClose: () -> Unit) {
}
}
Box(modifier = Modifier.fillMaxSize()) {
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
SmallTopAppBar(
title = actionName,
navigationIcon = {
IconButton(
modifier = Modifier.padding(start = 16.dp),
onClick = onBack
) {
Icon(
imageVector = MiuixIcons.Back,
contentDescription = null,
tint = MiuixTheme.colorScheme.onBackground
)
}
},
scrollBehavior = scrollBehavior
)
}
) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
LazyColumn(
state = listState,
modifier = Modifier
@@ -67,7 +95,7 @@ fun ActionScreen(viewModel: ActionViewModel, onClose: () -> Unit) {
)
FloatingActionButton(
onClick = onClose,
onClick = onBack,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
@@ -79,4 +107,5 @@ fun ActionScreen(viewModel: ActionViewModel, onClose: () -> Unit) {
}
}
}
}
}
@@ -35,7 +35,8 @@ class ActionViewModel : BaseViewModel() {
val actionState: StateFlow<State> = _actionState.asStateFlow()
val consoleItems = mutableStateListOf<String>()
lateinit var args: ActionFragmentArgs
var actionId: String = ""
var actionName: String = ""
private val logItems = mutableListOf<String>().synchronized()
private val outItems = object : CallbackList<String>() {
@@ -49,7 +50,7 @@ class ActionViewModel : BaseViewModel() {
fun startRunAction() = viewModelScope.launch {
onResult(withContext(Dispatchers.IO) {
try {
Shell.cmd("run_action '${args.id}'")
Shell.cmd("run_action '${actionId}'")
.to(outItems, logItems)
.exec().isSuccess
} catch (e: IOException) {
@@ -68,7 +69,7 @@ class ActionViewModel : BaseViewModel() {
fun saveLog() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) {
val name = "%s_action_log_%s.log".format(
args.name,
actionName,
System.currentTimeMillis().toTime(timeFormatStandard)
)
val file = MediaStoreUtils.getFile(name)
@@ -1,71 +0,0 @@
package com.topjohnwu.magisk.ui.module
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class ModuleFragment : Fragment(), ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[ModuleViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onStart() {
super.onStart()
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.modules)
(viewModel as ModuleViewModel).data.observe(this) {
it ?: return@observe
val vm = viewModel as ModuleViewModel
val displayName = runCatching { it.displayName }.getOrNull() ?: return@observe
vm.requestInstallLocalModule(it, displayName)
vm.data.value = null
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
ModuleScreen(viewModel = viewModel as ModuleViewModel)
}
}
}
}
override fun onResume() {
super.onResume()
(viewModel as ModuleViewModel).startLoading()
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}
@@ -22,29 +22,44 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.Switch
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.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun ModuleScreen(viewModel: ModuleViewModel) {
val uiState by viewModel.uiState.collectAsState()
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.modules),
scrollBehavior = scrollBehavior
)
}
) { padding ->
if (uiState.loading) {
Box(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
return
return@Scaffold
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -81,6 +96,7 @@ fun ModuleScreen(viewModel: ModuleViewModel) {
item { Spacer(Modifier.height(4.dp)) }
}
}
}
@Composable
@@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
@@ -22,6 +21,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import com.topjohnwu.magisk.ui.navigation.Route
import kotlinx.parcelize.Parcelize
import com.topjohnwu.magisk.core.R as CoreR
@@ -115,7 +115,7 @@ class ModuleViewModel : AsyncLoadViewModel() {
}
fun runAction(id: String, name: String) {
MainDirections.actionActionFragment(id, name).navigate()
navigateTo(Route.Action(id, name))
}
fun toggleEnabled(item: ModuleItem) {
@@ -0,0 +1,72 @@
package com.topjohnwu.magisk.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.navigation3.runtime.NavKey
class Navigator(initialKey: NavKey) {
val backStack: SnapshotStateList<NavKey> = mutableStateListOf(initialKey)
fun push(key: NavKey) {
backStack.add(key)
}
fun replace(key: NavKey) {
if (backStack.isNotEmpty()) {
backStack[backStack.lastIndex] = key
} else {
backStack.add(key)
}
}
fun replaceAll(keys: List<NavKey>) {
if (keys.isEmpty()) return
if (backStack.isNotEmpty()) {
backStack.clear()
backStack.addAll(keys)
}
}
fun pop() {
backStack.removeLastOrNull()
}
fun popUntil(predicate: (NavKey) -> Boolean) {
while (backStack.isNotEmpty() && !predicate(backStack.last())) {
backStack.removeAt(backStack.lastIndex)
}
}
fun current(): NavKey? = backStack.lastOrNull()
fun backStackSize(): Int = backStack.size
companion object {
val Saver: Saver<Navigator, Any> = listSaver(
save = { navigator -> navigator.backStack.toList() },
restore = { savedList ->
val initialKey = savedList.firstOrNull() ?: Route.Main
Navigator(initialKey).also {
it.backStack.clear()
it.backStack.addAll(savedList)
}
}
)
}
}
@Composable
fun rememberNavigator(startRoute: NavKey): Navigator {
return rememberSaveable(startRoute, saver = Navigator.Saver) {
Navigator(startRoute)
}
}
val LocalNavigator = staticCompositionLocalOf<Navigator> {
error("LocalNavigator not provided")
}
@@ -0,0 +1,41 @@
package com.topjohnwu.magisk.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent
@Composable
fun ObserveViewEvents(viewModel: BaseViewModel) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val activity = context as? UIActivity<*>
DisposableEffect(viewModel, lifecycleOwner) {
val observer = { event: ViewEvent ->
when (event) {
is ContextExecutor -> event(context)
is ActivityExecutor -> activity?.let { event(it) }
}
}
viewModel.viewEvents.observe(lifecycleOwner, observer)
onDispose {
viewModel.viewEvents.removeObserver(observer)
}
}
}
@Composable
fun CollectNavEvents(viewModel: BaseViewModel, navigator: Navigator) {
LaunchedEffect(viewModel) {
viewModel.navEvents.collect { route ->
navigator.push(route)
}
}
}
@@ -0,0 +1,36 @@
package com.topjohnwu.magisk.ui.navigation
import android.net.Uri
import android.os.Parcelable
import androidx.navigation3.runtime.NavKey
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
sealed interface Route : NavKey, Parcelable {
@Parcelize
@Serializable
data object Main : Route
@Parcelize
@Serializable
data object Install : Route
@Parcelize
@Serializable
data object DenyList : Route
@Parcelize
@Serializable
data class Flash(
val action: String,
val additionalData: String? = null,
) : Route
@Parcelize
@Serializable
data class Action(
val id: String,
val name: String,
) : Route
}
@@ -1,59 +0,0 @@
package com.topjohnwu.magisk.ui.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class SettingsFragment : Fragment(), ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[SettingsViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onStart() {
super.onStart()
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.settings)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
SettingsScreen(viewModel = viewModel as SettingsViewModel)
}
}
}
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}
@@ -34,8 +34,10 @@ import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
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 top.yukonga.miuix.kmp.extra.SuperArrow
@@ -46,14 +48,22 @@ import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun SettingsScreen(viewModel: SettingsViewModel) {
Scaffold { padding ->
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.settings),
scrollBehavior = scrollBehavior
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(padding)
.padding(horizontal = 12.dp)
.padding(top = 8.dp, bottom = 16.dp)
.padding(bottom = 16.dp)
) {
CustomizationSection(viewModel)
Spacer(Modifier.height(12.dp))
@@ -14,6 +14,7 @@ import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.events.AddHomeIconEvent
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -28,7 +29,7 @@ class SettingsViewModel : BaseViewModel() {
val zygiskMismatch get() = Config.zygisk != Info.isZygiskEnabled
fun navigateToDenyList() {
SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
navigateTo(Route.DenyList)
}
fun requestAddShortcut() {
@@ -1,63 +0,0 @@
package com.topjohnwu.magisk.ui.superuser
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class SuperuserFragment : Fragment(), ViewModelHolder {
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[SuperuserViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onStart() {
super.onStart()
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.superuser)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
SuperuserScreen(viewModel = viewModel as SuperuserViewModel)
}
}
}
}
override fun onResume() {
super.onResume()
(viewModel as SuperuserViewModel).startLoading()
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}
@@ -33,30 +33,46 @@ import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.core.model.su.SuPolicy
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.Slider
import top.yukonga.miuix.kmp.basic.Switch
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.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun SuperuserScreen(viewModel: SuperuserViewModel) {
val uiState by viewModel.uiState.collectAsState()
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.superuser),
scrollBehavior = scrollBehavior
)
}
) { padding ->
if (uiState.loading) {
Box(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
return
return@Scaffold
}
if (uiState.policies.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Text(
@@ -65,12 +81,13 @@ fun SuperuserScreen(viewModel: SuperuserViewModel) {
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
)
}
return
return@Scaffold
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -80,6 +97,7 @@ fun SuperuserScreen(viewModel: SuperuserViewModel) {
}
item { Spacer(Modifier.height(4.dp)) }
}
}
}
@Composable
@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:consumeSystemWindowsInsets="start|end"
app:edgeToEdge="true"
app:fitsSystemWindowsInsets="start|end"
tools:ignore="RtlHardcoded">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/main" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_toolbar_wrapper"
style="@style/WidgetFoundation.Appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar"
style="@style/WidgetFoundation.Toolbar"
android:layout_width="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_scrollFlags="noScroll"
tools:layout_marginTop="24dp"
tools:title="Home" />
</com.google.android.material.appbar.AppBarLayout>
<com.topjohnwu.magisk.widget.ConcealableBottomNavigationView
android:id="@+id/main_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:fitsSystemWindows="false"
android:paddingBottom="0dp"
app:fitsSystemWindowsInsets="start|end|bottom"
app:itemHorizontalTranslationEnabled="false"
app:itemIconTint="@color/color_menu_tint"
app:itemRippleColor="?colorPrimary"
app:itemTextAppearanceActive="@style/AppearanceFoundation.Tiny.Bold"
app:itemTextAppearanceInactive="@style/AppearanceFoundation.Tiny.Bold"
app:itemTextColor="@color/color_menu_tint"
app:labelVisibilityMode="labeled"
app:menu="@menu/menu_bottom_nav" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/homeFragment"
android:icon="@drawable/ic_home_md2"
android:title="@string/section_home"
tools:showAsAction="always" />
<item
android:id="@+id/superuserFragment"
android:icon="@drawable/ic_superuser_md2"
android:title="@string/superuser"
tools:showAsAction="always" />
<item
android:id="@+id/logFragment"
android:icon="@drawable/ic_bug_md2"
android:title="@string/logs"
tools:showAsAction="always" />
<item
android:id="@+id/modulesFragment"
android:icon="@drawable/ic_module_md2"
android:title="@string/modules"
tools:showAsAction="always" />
<item
android:id="@+id/settingsFragment"
android:icon="@drawable/ic_settings_md2"
android:title="@string/settings"
tools:showAsAction="always" />
</menu>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_reboot"
android:icon="@drawable/ic_restart"
android:title="@string/reboot"
app:showAsAction="ifRoom" />
</menu>
-156
View File
@@ -1,156 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/denyFragment"
android:name="com.topjohnwu.magisk.ui.deny.DenyListFragment"
android:label="DenyListFragment" />
<fragment
android:id="@+id/homeFragment"
android:name="com.topjohnwu.magisk.ui.home.HomeFragment"
android:label="HomeFragment">
<action
android:id="@+id/action_homeFragment_to_installFragment"
app:destination="@id/installFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop" />
</fragment>
<fragment
android:id="@+id/flashFragment"
android:name="com.topjohnwu.magisk.ui.flash.FlashFragment"
android:label="FlashFragment">
<argument
android:name="action"
app:argType="string" />
<argument
android:name="additional_data"
android:defaultValue="@null"
app:argType="android.net.Uri"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/actionFragment"
android:name="com.topjohnwu.magisk.ui.module.ActionFragment"
android:label="ActionFragment">
<argument
android:name="id"
app:argType="string" />
<argument
android:name="name"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/installFragment"
android:name="com.topjohnwu.magisk.ui.install.InstallFragment"
android:label="InstallFragment" />
<fragment
android:id="@+id/logFragment"
android:name="com.topjohnwu.magisk.ui.log.LogFragment"
android:label="LogFragment" />
<fragment
android:id="@+id/modulesFragment"
android:name="com.topjohnwu.magisk.ui.module.ModuleFragment"
android:label="ModuleFragment" />
<fragment
android:id="@+id/settingsFragment"
android:name="com.topjohnwu.magisk.ui.settings.SettingsFragment"
android:label="SettingsFragment">
<action
android:id="@+id/action_settingsFragment_to_denyFragment"
app:destination="@id/denyFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop" />
</fragment>
<fragment
android:id="@+id/superuserFragment"
android:name="com.topjohnwu.magisk.ui.superuser.SuperuserFragment"
android:label="SuperuserFragment" />
<action
android:id="@+id/action_homeFragment"
app:destination="@id/homeFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_superuserFragment"
app:destination="@id/superuserFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop"
app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_logFragment"
app:destination="@id/logFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop"
app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_moduleFragment"
app:destination="@id/modulesFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop"
app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_settingsFragment"
app:destination="@id/settingsFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop"
app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_flashFragment"
app:destination="@id/flashFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop" />
<action
android:id="@+id/action_actionFragment"
app:destination="@id/actionFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop" />
</navigation>
+1
View File
@@ -19,6 +19,7 @@ gradlePlugin {
dependencies {
implementation(kotlin("gradle-plugin", libs.versions.kotlin.get()))
implementation("org.jetbrains.kotlin:compose-compiler-gradle-plugin:${libs.versions.kotlin.get()}")
implementation("org.jetbrains.kotlin:kotlin-serialization:${libs.versions.kotlin.get()}")
implementation(libs.android.gradle.plugin)
implementation(libs.jgit)
}
+8 -1
View File
@@ -9,9 +9,11 @@ okhttp = "5.3.2"
retrofit = "3.0.0"
room = "2.8.4"
compose-bom = "2026.02.01"
lifecycle = "2.9.4"
lifecycle = "2.10.0"
activity-compose = "1.12.4"
miuix = "0.8.5"
navigation3 = "1.1.0-alpha05"
navigationevent = "1.0.2"
[libraries]
bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version = "1.83" }
@@ -70,6 +72,11 @@ compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview"
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
miuix = { module = "top.yukonga.miuix.kmp:miuix-android", version.ref = "miuix" }
miuix-icons = { module = "top.yukonga.miuix.kmp:miuix-icons-android", version.ref = "miuix" }
miuix-navigation3-ui = { module = "top.yukonga.miuix.kmp:miuix-navigation3-ui-android", version.ref = "miuix" }
navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
navigationevent-compose = { module = "androidx.navigationevent:navigationevent-compose", version.ref = "navigationevent" }
lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle" }
# Build plugins
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android" }