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