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 *.iml
.idea .idea
.cursor .cursor
ramdisk.img
app/core/src/debug
app/core/src/release
+8
View File
@@ -2,6 +2,7 @@ plugins {
id("com.android.application") id("com.android.application")
kotlin("plugin.parcelize") kotlin("plugin.parcelize")
kotlin("plugin.compose") kotlin("plugin.compose")
kotlin("plugin.serialization")
alias(libs.plugins.legacy.kapt) alias(libs.plugins.legacy.kapt)
alias(libs.plugins.navigation.safeargs) alias(libs.plugins.navigation.safeargs)
} }
@@ -68,6 +69,13 @@ dependencies {
implementation(libs.lifecycle.runtime.compose) implementation(libs.lifecycle.runtime.compose)
implementation(libs.lifecycle.viewmodel.compose) implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.miuix) 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 // Make sure kapt runs with a proper kotlin-stdlib
kapt(kotlin("stdlib")) kapt(kotlin("stdlib"))
@@ -9,15 +9,16 @@ import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.core.R import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.databinding.ObservableHost import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.events.BackPressEvent import com.topjohnwu.magisk.events.BackPressEvent
import com.topjohnwu.magisk.events.DialogBuilder import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.events.DialogEvent import com.topjohnwu.magisk.events.DialogEvent
import com.topjohnwu.magisk.events.NavigationEvent
import com.topjohnwu.magisk.events.PermissionEvent import com.topjohnwu.magisk.events.PermissionEvent
import com.topjohnwu.magisk.events.SnackbarEvent 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 { abstract class BaseViewModel : ViewModel(), ObservableHost {
@@ -26,6 +27,9 @@ abstract class BaseViewModel : ViewModel(), ObservableHost {
private val _viewEvents = MutableLiveData<ViewEvent>() private val _viewEvents = MutableLiveData<ViewEvent>()
val viewEvents: LiveData<ViewEvent> get() = _viewEvents 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 onSaveState(state: Bundle) {}
open fun onRestoreState(state: Bundle) {} open fun onRestoreState(state: Bundle) {}
open fun onNetworkChanged(network: Boolean) {} open fun onNetworkChanged(network: Boolean) {}
@@ -76,8 +80,8 @@ abstract class BaseViewModel : ViewModel(), ObservableHost {
DialogEvent(this).publish() DialogEvent(this).publish()
} }
fun NavDirections.navigate(pop: Boolean = false) { fun navigateTo(route: Route) {
_viewEvents.postValue(NavigationEvent(this, pop)) _navEvents.tryEmit(route)
} }
} }
@@ -133,7 +133,6 @@ fun ViewGroup.startAnimations() {
val transition = AutoTransition() val transition = AutoTransition()
.setInterpolator(FastOutSlowInInterpolator()) .setInterpolator(FastOutSlowInInterpolator())
.setDuration(400) .setDuration(400)
.excludeTarget(R.id.main_toolbar, true)
TransitionManager.beginDelayedTransition( TransitionManager.beginDelayedTransition(
this, this,
transition transition
@@ -1,11 +1,11 @@
package com.topjohnwu.magisk.dialog package com.topjohnwu.magisk.dialog
import android.net.Uri import android.net.Uri
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.R import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.events.DialogBuilder import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.module.ModuleViewModel import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
class LocalModuleInstallDialog( class LocalModuleInstallDialog(
@@ -20,9 +20,9 @@ class LocalModuleInstallDialog(
setButton(MagiskDialog.ButtonType.POSITIVE) { setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok text = android.R.string.ok
onClick { onClick {
viewModel.apply { viewModel.navigateTo(
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate() Route.Flash(Const.Value.FLASH_ZIP, uri.toString())
} )
} }
} }
setButton(MagiskDialog.ButtonType.NEGATIVE) { 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.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.OnlineModule 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.MagiskDialog
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -26,7 +26,7 @@ class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog
override val autoLaunch: Boolean, override val autoLaunch: Boolean,
override val notifyId: Int = Notifications.nextId() override val notifyId: Int = Notifications.nextId()
) : Subject.Module() { ) : 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) { override fun build(dialog: MagiskDialog) {
@@ -3,13 +3,13 @@ package com.topjohnwu.magisk.dialog
import android.app.ProgressDialog import android.app.ProgressDialog
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.R import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.MagiskInstaller import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.events.DialogBuilder 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 com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -50,8 +50,14 @@ class UninstallDialog : DialogBuilder {
} }
private fun completeUninstall(dialog: MagiskDialog) { private fun completeUninstall(dialog: MagiskDialog) {
(dialog.ownerActivity as NavigationActivity<*>) val activity = dialog.ownerActivity ?: return
.navigation.navigate(FlashFragment.uninstall()) 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.content.Context
import android.view.View import android.view.View
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.navigation.NavDirections
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.arch.ActivityExecutor import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.base.ContentResultCallback 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 { class AddHomeIconEvent : ViewEvent(), ContextExecutor {
override fun invoke(context: Context) { override fun invoke(context: Context) {
Shortcuts.addHomeIcon(context) Shortcuts.addHomeIcon(context)
@@ -5,22 +5,29 @@ import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast 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.content.pm.ShortcutManagerCompat
import androidx.core.view.forEach import androidx.core.net.toUri
import androidx.core.view.isGone import androidx.databinding.ViewDataBinding
import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import com.topjohnwu.magisk.MainDirections import androidx.navigation3.runtime.NavEntry
import com.topjohnwu.magisk.R 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.BaseViewModel
import com.topjohnwu.magisk.arch.NavigationActivity import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.startAnimations import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.viewModel import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const 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.base.SplashScreenHost
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.toast 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.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.ui.theme.Theme
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import androidx.compose.runtime.Composable
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
class MainViewModel : BaseViewModel() 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 viewModel by viewModel<MainViewModel>()
override val navHostId: Int = R.id.main_nav_host
override val splashController = SplashController(this) override val splashController = SplashController(this)
override val snackbarView: View override val snackbarView: View
get() { get() = window.decorView.findViewById(android.R.id.content)
val fragmentOverride = currentFragment?.snackbarView override val snackbarAnchorView: View? get() = null
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
}
}
private var isRootFragment = true private val intentState = MutableStateFlow(0)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes) setTheme(Theme.selected.themeRes)
@@ -78,11 +88,9 @@ class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenH
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
override fun onCreateUi(savedInstanceState: Bundle?) { override fun onCreateUi(savedInstanceState: Bundle?) {
setContentView()
showUnsupportedMessage() showUnsupportedMessage()
askForHomeShortcut() askForHomeShortcut()
// Ask permission to post notifications for background update check
if (Config.checkUpdate) { if (Config.checkUpdate) {
withPermission(Manifest.permission.POST_NOTIFICATIONS) { withPermission(Manifest.permission.POST_NOTIFICATIONS) {
Config.checkUpdate = it Config.checkUpdate = it
@@ -91,101 +99,99 @@ class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenH
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
navigation.addOnDestinationChangedListener { _, destination, _ -> val initialTab = getInitialTab(intent)
isRootFragment = when (destination.id) {
R.id.homeFragment,
R.id.modulesFragment,
R.id.superuserFragment,
R.id.logFragment,
R.id.settingsFragment -> true
else -> false
}
setDisplayHomeAsUpEnabled(!isRootFragment) setContent {
requestNavigationHidden(!isRootFragment) MagiskTheme {
val navigator = rememberNavigator(Route.Main)
CompositionLocalProvider(LocalNavigator provides navigator) {
HandleFlashIntent(navigator)
binding.mainNavigation.menu.forEach { NavDisplay(
if (it.itemId == destination.id) { backStack = navigator.backStack,
it.isChecked = true 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() })
}
}
)
} }
} }
} }
}
setSupportActionBar(binding.mainToolbar) @Composable
private fun HandleFlashIntent(navigator: Navigator) {
binding.mainNavigation.setOnItemSelectedListener { val intentVersion by intentState.collectAsState()
getScreen(it.itemId)?.navigate() LaunchedEffect(intentVersion) {
true val currentIntent = intent ?: return@LaunchedEffect
} if (currentIntent.action == FlashUtils.INTENT_FLASH) {
binding.mainNavigation.setOnItemReselectedListener { val action = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_ACTION)
// https://issuetracker.google.com/issues/124538620 ?: return@LaunchedEffect
} val uri = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_URI)
binding.mainNavigation.menu.apply { navigator.push(Route.Flash(action, uri))
findItem(R.id.superuserFragment)?.isEnabled = Info.showSuperUser currentIntent.action = null
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded() }
}
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 { override fun onNewIntent(intent: Intent) {
when (item.itemId) { super.onNewIntent(intent)
android.R.id.home -> onBackPressed() setIntent(intent)
else -> return super.onOptionsItemSelected(item) intentState.value += 1
}
return true
} }
fun setDisplayHomeAsUpEnabled(isEnabled: Boolean) { private fun getInitialTab(intent: Intent?): Int {
binding.mainToolbar.startAnimations() val section = if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) {
when { Const.Nav.SETTINGS
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 { } else {
bottomView.isGone = hide intent?.getStringExtra(Const.Key.OPEN_SECTION)
} }
} return when (section) {
Const.Nav.SUPERUSER -> Tab.SUPERUSER.ordinal
fun invalidateToolbar() { Const.Nav.MODULES -> Tab.MODULES.ordinal
//binding.mainToolbar.startAnimations() Const.Nav.SETTINGS -> Tab.SETTINGS.ordinal
binding.mainToolbar.invalidate() else -> Tab.HOME.ordinal
}
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
} }
} }
@@ -226,8 +232,8 @@ class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenH
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH") if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
?.split(':') ?.split(':')
?.filterNot { File("$it/magisk").exists() } ?.filterNot { java.io.File("$it/magisk").exists() }
?.any { File("$it/su").exists() } == true) { ?.any { java.io.File("$it/su").exists() } == true) {
MagiskDialog(this).apply { MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_general_title) setTitle(CoreR.string.unsupport_general_title)
setMessage(CoreR.string.unsupport_other_su_msg) setMessage(CoreR.string.unsupport_other_su_msg)
@@ -258,7 +264,6 @@ class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenH
private fun askForHomeShortcut() { private fun askForHomeShortcut() {
if (isRunningAsStub && !Config.askedHome && if (isRunningAsStub && !Config.askedHome &&
ShortcutManagerCompat.isRequestPinShortcutSupported(this)) { ShortcutManagerCompat.isRequestPinShortcutSupported(this)) {
// Ask and show dialog
Config.askedHome = true Config.askedHome = true
MagiskDialog(this).apply { MagiskDialog(this).apply {
setTitle(CoreR.string.add_shortcut_title) 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,80 +32,107 @@ import androidx.compose.ui.unit.dp
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Checkbox import top.yukonga.miuix.kmp.basic.Checkbox
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator 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.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.Switch
import top.yukonga.miuix.kmp.basic.Text 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 top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
@Composable @Composable
fun DenyListScreen(viewModel: DenyListViewModel) { fun DenyListScreen(viewModel: DenyListViewModel, onBack: () -> Unit) {
val loading by viewModel.loading.collectAsState() val loading by viewModel.loading.collectAsState()
val apps by viewModel.filteredApps.collectAsState() val apps by viewModel.filteredApps.collectAsState()
val query by viewModel.query.collectAsState() val query by viewModel.query.collectAsState()
val showSystem by viewModel.showSystem.collectAsState() val showSystem by viewModel.showSystem.collectAsState()
val showOS by viewModel.showOS.collectAsState() val showOS by viewModel.showOS.collectAsState()
Column(modifier = Modifier.fillMaxSize()) { val scrollBehavior = MiuixScrollBehavior()
// Search input Scaffold(
SearchInput( topBar = {
query = query, TopAppBar(
onQueryChange = viewModel::setQuery, title = stringResource(CoreR.string.denylist),
modifier = Modifier navigationIcon = {
.fillMaxWidth() IconButton(
.padding(horizontal = 12.dp, vertical = 4.dp) modifier = Modifier.padding(start = 16.dp),
) onClick = onBack
) {
// Filter chips Icon(
Row( imageVector = MiuixIcons.Back,
modifier = Modifier contentDescription = null,
.fillMaxWidth() tint = MiuixTheme.colorScheme.onBackground
.padding(horizontal = 12.dp), )
horizontalArrangement = Arrangement.spacedBy(12.dp) }
) { },
FilterChip( scrollBehavior = scrollBehavior
label = stringResource(CoreR.string.show_system_app),
checked = showSystem,
onCheckedChange = viewModel::setShowSystem
)
FilterChip(
label = stringResource(CoreR.string.show_os_app),
checked = showOS,
enabled = showSystem,
onCheckedChange = viewModel::setShowOS
) )
} }
) { padding ->
Spacer(Modifier.height(8.dp)) Column(modifier = Modifier.fillMaxSize().padding(padding)) {
SearchInput(
if (loading) { query = query,
Box( onQueryChange = viewModel::setQuery,
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(CoreR.string.loading),
style = MiuixTheme.textStyles.headline2
)
Spacer(Modifier.height(16.dp))
CircularProgressIndicator()
}
}
} else {
LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp), .padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
items( FilterChip(
items = apps, label = stringResource(CoreR.string.show_system_app),
key = { it.info.packageName } checked = showSystem,
) { app -> onCheckedChange = viewModel::setShowSystem
DenyAppCard(app) )
FilterChip(
label = stringResource(CoreR.string.show_os_app),
checked = showOS,
enabled = showSystem,
onCheckedChange = viewModel::setShowOS
)
}
Spacer(Modifier.height(8.dp))
if (loading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(CoreR.string.loading),
style = MiuixTheme.textStyles.headline2
)
Spacer(Modifier.height(16.dp))
CircularProgressIndicator()
}
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = apps,
key = { it.info.packageName }
) { app ->
DenyAppCard(app)
}
item { Spacer(Modifier.height(8.dp)) }
} }
item { Spacer(Modifier.height(8.dp)) }
} }
} }
} }
@@ -153,7 +180,6 @@ private fun FilterChip(
private fun DenyAppCard(app: DenyAppState) { private fun DenyAppCard(app: DenyAppState) {
Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
Column { Column {
// Progress bar showing percentage of checked processes
if (app.checkedPercent > 0f) { if (app.checkedPercent > 0f) {
LinearProgressIndicator( LinearProgressIndicator(
progress = app.checkedPercent, progress = app.checkedPercent,
@@ -161,7 +187,6 @@ private fun DenyAppCard(app: DenyAppState) {
) )
} }
// App row
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -192,7 +217,6 @@ private fun DenyAppCard(app: DenyAppState) {
) )
} }
// Expanded process list
AnimatedVisibility(visible = app.isExpanded) { AnimatedVisibility(visible = app.isExpanded) {
Column( Column(
modifier = Modifier 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import top.yukonga.miuix.kmp.basic.FloatingActionButton 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.Text
import top.yukonga.miuix.kmp.basic.TextButton 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 top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
@Composable @Composable
fun FlashScreen(viewModel: FlashViewModel) { fun FlashScreen(viewModel: FlashViewModel, onBack: () -> Unit) {
val flashState by viewModel.flashState.collectAsState() val flashState by viewModel.flashState.collectAsState()
val showReboot by viewModel.showReboot.collectAsState() val showReboot by viewModel.showReboot.collectAsState()
val items = viewModel.consoleItems val items = viewModel.consoleItems
@@ -38,48 +45,76 @@ fun FlashScreen(viewModel: FlashViewModel) {
} }
} }
Box(modifier = Modifier.fillMaxSize()) { val statusText = when (flashState) {
LazyColumn( FlashViewModel.State.FLASHING -> stringResource(CoreR.string.flashing)
state = listState, FlashViewModel.State.SUCCESS -> stringResource(CoreR.string.done)
modifier = Modifier FlashViewModel.State.FAILED -> stringResource(CoreR.string.failure)
.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) { val scrollBehavior = MiuixScrollBehavior()
TextButton( Scaffold(
text = stringResource(CoreR.string.menuSaveLog), topBar = {
onClick = { viewModel.saveLog() }, SmallTopAppBar(
modifier = Modifier title = "${stringResource(CoreR.string.flash_screen_title)} - $statusText",
.align(Alignment.BottomStart) navigationIcon = {
.padding(16.dp) IconButton(
modifier = Modifier.padding(start = 16.dp),
onClick = onBack
) {
Icon(
imageVector = MiuixIcons.Back,
contentDescription = null,
tint = MiuixTheme.colorScheme.onBackground
)
}
},
scrollBehavior = scrollBehavior
) )
} }
) { padding ->
if (flashState == FlashViewModel.State.SUCCESS && showReboot) { Box(modifier = Modifier.fillMaxSize().padding(padding)) {
FloatingActionButton( LazyColumn(
onClick = { viewModel.restartPressed() }, state = listState,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .fillMaxSize()
.padding(16.dp) .horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp, vertical = 4.dp)
) { ) {
Text( itemsIndexed(items) { _, line ->
text = stringResource(CoreR.string.reboot), Text(
modifier = Modifier.padding(horizontal = 16.dp) 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)
) )
} }
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)
)
}
}
} }
} }
} }
@@ -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 showReboot: StateFlow<Boolean> = _showReboot.asStateFlow()
val consoleItems = mutableStateListOf<String>() 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 logItems = mutableListOf<String>().synchronized()
private val outItems = object : CallbackList<String>() { private val outItems = object : CallbackList<String>() {
@@ -53,7 +54,8 @@ class FlashViewModel : BaseViewModel() {
} }
fun startFlashing() { fun startFlashing() {
val (action, uri) = args val action = flashAction
val uri = flashUri
viewModelScope.launch { viewModelScope.launch {
val result = when (action) { 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,41 +33,55 @@ import com.topjohnwu.magisk.core.R as CoreR
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator 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.Text
import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
@Composable @Composable
fun HomeScreen(viewModel: HomeViewModel) { fun HomeScreen(viewModel: HomeViewModel) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val context = LocalContext.current val context = LocalContext.current
val scrollBehavior = MiuixScrollBehavior()
Column( Scaffold(
modifier = Modifier topBar = {
.verticalScroll(rememberScrollState()) TopAppBar(
.padding(horizontal = 16.dp) title = stringResource(CoreR.string.section_home),
.padding(top = 8.dp, bottom = 16.dp), scrollBehavior = scrollBehavior
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (uiState.isNoticeVisible) {
NoticeCard(onHide = viewModel::hideNotice)
}
MagiskCard(viewModel = viewModel)
ManagerCard(viewModel = viewModel, uiState = uiState)
if (Info.env.isActive) {
TextButton(
text = stringResource(CoreR.string.uninstall_magisk_title),
onClick = { viewModel.onDeletePressed() },
modifier = Modifier.fillMaxWidth()
) )
} }
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (uiState.isNoticeVisible) {
NoticeCard(onHide = viewModel::hideNotice)
}
SupportCard(onLinkClicked = { viewModel.onLinkPressed(it) }) MagiskCard(viewModel = viewModel)
DevelopersCard(onLinkClicked = { openLink(context, it) }) ManagerCard(viewModel = viewModel, uiState = uiState)
if (Info.env.isActive) {
TextButton(
text = stringResource(CoreR.string.uninstall_magisk_title),
onClick = { viewModel.onDeletePressed() },
modifier = Modifier.fillMaxWidth()
)
}
SupportCard(onLinkClicked = { viewModel.onLinkPressed(it) })
DevelopersCard(onLinkClicked = { openLink(context, it) })
}
} }
} }
@@ -20,6 +20,7 @@ import com.topjohnwu.magisk.dialog.EnvFixDialog
import com.topjohnwu.magisk.dialog.ManagerInstallDialog import com.topjohnwu.magisk.dialog.ManagerInstallDialog
import com.topjohnwu.magisk.dialog.UninstallDialog import com.topjohnwu.magisk.dialog.UninstallDialog
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -122,7 +123,7 @@ class HomeViewModel(
} }
fun onMagiskPressed() = withExternalRW { fun onMagiskPressed() = withExternalRW {
HomeFragmentDirections.actionHomeFragmentToInstallFragment().navigate() navigateTo(Route.Install)
} }
fun hideNotice() { fun hideNotice() {
@@ -34,7 +34,7 @@ object RebootMenu {
fun inflate(activity: Activity): PopupMenu { fun inflate(activity: Activity): PopupMenu {
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_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) activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
menu.setOnMenuItemClickListener(RebootMenu::reboot) menu.setOnMenuItemClickListener(RebootMenu::reboot)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && 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,31 +21,61 @@ import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Checkbox 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.Text
import top.yukonga.miuix.kmp.basic.TextButton 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 top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
@Composable @Composable
fun InstallScreen(viewModel: InstallViewModel) { fun InstallScreen(viewModel: InstallViewModel, onBack: () -> Unit) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
Column( val scrollBehavior = MiuixScrollBehavior()
modifier = Modifier Scaffold(
.fillMaxSize() topBar = {
.verticalScroll(rememberScrollState()) TopAppBar(
.padding(horizontal = 12.dp) title = stringResource(CoreR.string.install),
.padding(top = 8.dp, bottom = 16.dp), navigationIcon = {
verticalArrangement = Arrangement.spacedBy(12.dp) IconButton(
) { modifier = Modifier.padding(start = 16.dp),
if (!viewModel.skipOptions) { onClick = onBack
OptionsCard(uiState = uiState, viewModel = viewModel) ) {
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)
) {
if (!viewModel.skipOptions) {
OptionsCard(uiState = uiState, viewModel = viewModel)
}
MethodCard(uiState = uiState, viewModel = viewModel) MethodCard(uiState = uiState, viewModel = viewModel)
if (uiState.notes.isNotEmpty()) { if (uiState.notes.isNotEmpty()) {
NotesCard(notes = uiState.notes) NotesCard(notes = uiState.notes)
}
} }
} }
} }
@@ -15,7 +15,8 @@ import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.repository.NetworkService import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.magisk.dialog.SecondSlotWarningDialog import com.topjohnwu.magisk.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.events.GetContentEvent 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 io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -96,9 +97,16 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
fun install() { fun install() {
when (_uiState.value.method) { when (_uiState.value.method) {
Method.PATCH -> FlashFragment.patch(data.value!!).navigate(true) Method.PATCH -> navigateTo(Route.Flash(
Method.DIRECT -> FlashFragment.flash(false).navigate(true) action = Const.Value.PATCH_FILE,
Method.INACTIVE_SLOT -> FlashFragment.flash(true).navigate(true) 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") 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 com.topjohnwu.magisk.core.model.su.SuLog
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator 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.TabRow
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
@@ -46,33 +49,46 @@ fun LogScreen(viewModel: LogViewModel) {
stringResource(CoreR.string.superuser), stringResource(CoreR.string.superuser),
stringResource(CoreR.string.magisk) stringResource(CoreR.string.magisk)
) )
val scrollBehavior = MiuixScrollBehavior()
Column(modifier = Modifier.fillMaxSize()) { Scaffold(
TabRow( topBar = {
tabs = tabTitles, TopAppBar(
selectedTabIndex = selectedTab, title = stringResource(CoreR.string.logs),
onTabSelected = { selectedTab = it }, scrollBehavior = scrollBehavior
modifier = Modifier.fillMaxWidth() )
) }
) { padding ->
Column(modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
TabRow(
tabs = tabTitles,
selectedTabIndex = selectedTab,
onTabSelected = { selectedTab = it },
modifier = Modifier.fillMaxWidth()
)
if (uiState.loading) { if (uiState.loading) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
CircularProgressIndicator() CircularProgressIndicator()
} }
} else { } else {
when (selectedTab) { when (selectedTab) {
0 -> SuLogTab( 0 -> SuLogTab(
logs = uiState.suLogs, logs = uiState.suLogs,
onClear = { viewModel.clearLog() } onClear = { viewModel.clearLog() }
) )
1 -> MagiskLogTab( 1 -> MagiskLogTab(
log = uiState.magiskLog, log = uiState.magiskLog,
onSave = { viewModel.saveMagiskLog() }, onSave = { viewModel.saveMagiskLog() },
onClear = { viewModel.clearMagiskLog() } onClear = { viewModel.clearMagiskLog() }
) )
}
} }
} }
} }
@@ -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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import top.yukonga.miuix.kmp.basic.FloatingActionButton 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.Text
import top.yukonga.miuix.kmp.basic.TextButton 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 top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
@Composable @Composable
fun ActionScreen(viewModel: ActionViewModel, onClose: () -> Unit) { fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> Unit) {
val actionState by viewModel.actionState.collectAsState() val actionState by viewModel.actionState.collectAsState()
val items = viewModel.consoleItems val items = viewModel.consoleItems
val listState = rememberLazyListState() val listState = rememberLazyListState()
@@ -37,45 +44,67 @@ fun ActionScreen(viewModel: ActionViewModel, onClose: () -> Unit) {
} }
} }
Box(modifier = Modifier.fillMaxSize()) { val scrollBehavior = MiuixScrollBehavior()
LazyColumn( Scaffold(
state = listState, topBar = {
modifier = Modifier SmallTopAppBar(
.fillMaxSize() title = actionName,
.horizontalScroll(rememberScrollState()) navigationIcon = {
.padding(horizontal = 8.dp, vertical = 4.dp) IconButton(
) { modifier = Modifier.padding(start = 16.dp),
itemsIndexed(items) { _, line -> onClick = onBack
Text( ) {
text = line, Icon(
fontFamily = FontFamily.Monospace, imageVector = MiuixIcons.Back,
fontSize = 12.sp, contentDescription = null,
lineHeight = 16.sp, tint = MiuixTheme.colorScheme.onBackground
color = MiuixTheme.colorScheme.onSurface, )
modifier = Modifier.fillMaxWidth() }
) },
} scrollBehavior = scrollBehavior
}
if (actionState != ActionViewModel.State.RUNNING) {
TextButton(
text = stringResource(CoreR.string.menuSaveLog),
onClick = { viewModel.saveLog() },
modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp)
) )
}
FloatingActionButton( ) { padding ->
onClick = onClose, Box(modifier = Modifier.fillMaxSize().padding(padding)) {
LazyColumn(
state = listState,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .fillMaxSize()
.padding(16.dp) .horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp, vertical = 4.dp)
) { ) {
Text( itemsIndexed(items) { _, line ->
text = stringResource(CoreR.string.close), Text(
modifier = Modifier.padding(horizontal = 16.dp) 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)
) )
FloatingActionButton(
onClick = onBack,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
) {
Text(
text = stringResource(CoreR.string.close),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
} }
} }
} }
@@ -35,7 +35,8 @@ class ActionViewModel : BaseViewModel() {
val actionState: StateFlow<State> = _actionState.asStateFlow() val actionState: StateFlow<State> = _actionState.asStateFlow()
val consoleItems = mutableStateListOf<String>() val consoleItems = mutableStateListOf<String>()
lateinit var args: ActionFragmentArgs var actionId: String = ""
var actionName: String = ""
private val logItems = mutableListOf<String>().synchronized() private val logItems = mutableListOf<String>().synchronized()
private val outItems = object : CallbackList<String>() { private val outItems = object : CallbackList<String>() {
@@ -49,7 +50,7 @@ class ActionViewModel : BaseViewModel() {
fun startRunAction() = viewModelScope.launch { fun startRunAction() = viewModelScope.launch {
onResult(withContext(Dispatchers.IO) { onResult(withContext(Dispatchers.IO) {
try { try {
Shell.cmd("run_action '${args.id}'") Shell.cmd("run_action '${actionId}'")
.to(outItems, logItems) .to(outItems, logItems)
.exec().isSuccess .exec().isSuccess
} catch (e: IOException) { } catch (e: IOException) {
@@ -68,7 +69,7 @@ class ActionViewModel : BaseViewModel() {
fun saveLog() = withExternalRW { fun saveLog() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val name = "%s_action_log_%s.log".format( val name = "%s_action_log_%s.log".format(
args.name, actionName,
System.currentTimeMillis().toTime(timeFormatStandard) System.currentTimeMillis().toTime(timeFormatStandard)
) )
val file = MediaStoreUtils.getFile(name) 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,64 +22,80 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator 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.Switch
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
@Composable @Composable
fun ModuleScreen(viewModel: ModuleViewModel) { fun ModuleScreen(viewModel: ModuleViewModel) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val scrollBehavior = MiuixScrollBehavior()
if (uiState.loading) { Scaffold(
Box( topBar = {
modifier = Modifier.fillMaxSize(), TopAppBar(
contentAlignment = Alignment.Center title = stringResource(CoreR.string.modules),
) { scrollBehavior = scrollBehavior
CircularProgressIndicator()
}
return
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item { Spacer(Modifier.height(4.dp)) }
item {
TextButton(
text = stringResource(CoreR.string.module_action_install_external),
onClick = { viewModel.installPressed() },
modifier = Modifier.fillMaxWidth()
) )
} }
) { padding ->
if (uiState.modules.isEmpty()) { if (uiState.loading) {
item { Box(
Box( modifier = Modifier
modifier = Modifier .fillMaxSize()
.fillMaxWidth() .padding(padding),
.padding(vertical = 32.dp), contentAlignment = Alignment.Center
contentAlignment = Alignment.Center ) {
) { CircularProgressIndicator()
Text(
text = stringResource(CoreR.string.module_empty),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
)
}
}
} else {
items(uiState.modules, key = { it.module.id }) { item ->
ModuleCard(item = item, viewModel = viewModel)
} }
return@Scaffold
} }
item { Spacer(Modifier.height(4.dp)) } LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item { Spacer(Modifier.height(4.dp)) }
item {
TextButton(
text = stringResource(CoreR.string.module_action_install_external),
onClick = { viewModel.installPressed() },
modifier = Modifier.fillMaxWidth()
)
}
if (uiState.modules.isEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(CoreR.string.module_empty),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
)
}
}
} else {
items(uiState.modules, key = { it.module.id }) { item ->
ModuleCard(item = item, viewModel = viewModel)
}
}
item { Spacer(Modifier.height(4.dp)) }
}
} }
} }
@@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.arch.AsyncLoadViewModel import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
@@ -22,6 +21,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import com.topjohnwu.magisk.ui.navigation.Route
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
@@ -115,7 +115,7 @@ class ModuleViewModel : AsyncLoadViewModel() {
} }
fun runAction(id: String, name: String) { fun runAction(id: String, name: String) {
MainDirections.actionActionFragment(id, name).navigate() navigateTo(Route.Action(id, name))
} }
fun toggleEnabled(item: ModuleItem) { 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.LocaleSetting
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import top.yukonga.miuix.kmp.basic.Card 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.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTitle 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.TextField
import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperArrow
@@ -46,14 +48,22 @@ import com.topjohnwu.magisk.core.R as CoreR
@Composable @Composable
fun SettingsScreen(viewModel: SettingsViewModel) { fun SettingsScreen(viewModel: SettingsViewModel) {
Scaffold { padding -> val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.settings),
scrollBehavior = scrollBehavior
)
}
) { padding ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(padding) .padding(padding)
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.padding(top = 8.dp, bottom = 16.dp) .padding(bottom = 16.dp)
) { ) {
CustomizationSection(viewModel) CustomizationSection(viewModel)
Spacer(Modifier.height(12.dp)) 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.AddHomeIconEvent
import com.topjohnwu.magisk.events.AuthEvent import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -28,7 +29,7 @@ class SettingsViewModel : BaseViewModel() {
val zygiskMismatch get() = Config.zygisk != Info.isZygiskEnabled val zygiskMismatch get() = Config.zygisk != Info.isZygiskEnabled
fun navigateToDenyList() { fun navigateToDenyList() {
SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate() navigateTo(Route.DenyList)
} }
fun requestAddShortcut() { 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,52 +33,70 @@ import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator 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.Slider
import top.yukonga.miuix.kmp.basic.Switch import top.yukonga.miuix.kmp.basic.Switch
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
@Composable @Composable
fun SuperuserScreen(viewModel: SuperuserViewModel) { fun SuperuserScreen(viewModel: SuperuserViewModel) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val scrollBehavior = MiuixScrollBehavior()
if (uiState.loading) { Scaffold(
Box( topBar = {
modifier = Modifier.fillMaxSize(), TopAppBar(
contentAlignment = Alignment.Center title = stringResource(CoreR.string.superuser),
) { scrollBehavior = scrollBehavior
CircularProgressIndicator()
}
return
}
if (uiState.policies.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(CoreR.string.superuser_policy_none),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
) )
} }
return ) { padding ->
} if (uiState.loading) {
Box(
LazyColumn( modifier = Modifier
modifier = Modifier .fillMaxSize()
.fillMaxSize() .padding(padding),
.padding(horizontal = 12.dp), contentAlignment = Alignment.Center
verticalArrangement = Arrangement.spacedBy(8.dp) ) {
) { CircularProgressIndicator()
item { Spacer(Modifier.height(4.dp)) } }
items(uiState.policies, key = { "${it.policy.uid}_${it.packageName}" }) { item -> return@Scaffold
PolicyCard(item = item, viewModel = viewModel) }
if (uiState.policies.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(CoreR.string.superuser_policy_none),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
)
}
return@Scaffold
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item { Spacer(Modifier.height(4.dp)) }
items(uiState.policies, key = { "${it.policy.uid}_${it.packageName}" }) { item ->
PolicyCard(item = item, viewModel = viewModel)
}
item { Spacer(Modifier.height(4.dp)) }
} }
item { Spacer(Modifier.height(4.dp)) }
} }
} }
@@ -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 { dependencies {
implementation(kotlin("gradle-plugin", libs.versions.kotlin.get())) implementation(kotlin("gradle-plugin", libs.versions.kotlin.get()))
implementation("org.jetbrains.kotlin:compose-compiler-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.android.gradle.plugin)
implementation(libs.jgit) implementation(libs.jgit)
} }
+8 -1
View File
@@ -9,9 +9,11 @@ okhttp = "5.3.2"
retrofit = "3.0.0" retrofit = "3.0.0"
room = "2.8.4" room = "2.8.4"
compose-bom = "2026.02.01" compose-bom = "2026.02.01"
lifecycle = "2.9.4" lifecycle = "2.10.0"
activity-compose = "1.12.4" activity-compose = "1.12.4"
miuix = "0.8.5" miuix = "0.8.5"
navigation3 = "1.1.0-alpha05"
navigationevent = "1.0.2"
[libraries] [libraries]
bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version = "1.83" } 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-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-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 = { 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 # Build plugins
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android" } android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android" }