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