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