From 9107bb6af39982679eba316854b1372c030a2b4a Mon Sep 17 00:00:00 2001 From: LoveSy Date: Tue, 3 Mar 2026 11:29:48 +0800 Subject: [PATCH 01/58] Add Compose and miuix dependencies Add Jetpack Compose BOM, activity-compose, lifecycle-runtime-compose, lifecycle-viewmodel-compose, and miuix library dependencies. Enable the Compose compiler plugin alongside existing data binding. Made-with: Cursor --- .gitignore | 1 + app/apk/build.gradle.kts | 12 ++++++++++++ app/buildSrc/build.gradle.kts | 1 + app/gradle/libs.versions.toml | 14 ++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/.gitignore b/.gitignore index 98ccb8c50..a9365d9cf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ native/out # Android Studio *.iml .idea +.cursor diff --git a/app/apk/build.gradle.kts b/app/apk/build.gradle.kts index 56241df8f..f6f2a6145 100644 --- a/app/apk/build.gradle.kts +++ b/app/apk/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") kotlin("plugin.parcelize") + kotlin("plugin.compose") alias(libs.plugins.legacy.kapt) alias(libs.plugins.navigation.safeargs) } @@ -19,6 +20,7 @@ kapt { android { buildFeatures { dataBinding = true + compose = true } compileOptions { @@ -57,6 +59,16 @@ dependencies { implementation(libs.appcompat) implementation(libs.material) + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling.preview) + debugImplementation(libs.compose.ui.tooling) + implementation(libs.activity.compose) + implementation(libs.lifecycle.runtime.compose) + implementation(libs.lifecycle.viewmodel.compose) + implementation(libs.miuix) + // Make sure kapt runs with a proper kotlin-stdlib kapt(kotlin("stdlib")) } diff --git a/app/buildSrc/build.gradle.kts b/app/buildSrc/build.gradle.kts index ee770f35e..3ad3294d9 100644 --- a/app/buildSrc/build.gradle.kts +++ b/app/buildSrc/build.gradle.kts @@ -18,6 +18,7 @@ gradlePlugin { dependencies { implementation(kotlin("gradle-plugin", libs.versions.kotlin.get())) + implementation("org.jetbrains.kotlin:compose-compiler-gradle-plugin:${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 5f8c4a23e..2216f7800 100644 --- a/app/gradle/libs.versions.toml +++ b/app/gradle/libs.versions.toml @@ -8,6 +8,10 @@ libsu = "6.0.0" okhttp = "5.3.2" retrofit = "3.0.0" room = "2.8.4" +compose-bom = "2026.02.01" +lifecycle = "2.9.4" +activity-compose = "1.12.4" +miuix = "0.8.5" [libraries] bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version = "1.83" } @@ -57,6 +61,16 @@ rikka-recyclerview = { module = "dev.rikka.rikkax.recyclerview:recyclerview-ktx" rikka-layoutinflater = { module = "dev.rikka.rikkax.layoutinflater:layoutinflater", version.ref = "rikka" } rikka-insets = { module = "dev.rikka.rikkax.insets:insets", version.ref = "rikka" } +# Compose +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } +compose-ui = { module = "androidx.compose.ui:ui" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +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" } + # Build plugins android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android" } From e6c426869414a5b9b07ab50b4e4bc9c4754bc8f6 Mon Sep 17 00:00:00 2001 From: LoveSy Date: Tue, 3 Mar 2026 11:29:53 +0800 Subject: [PATCH 02/58] Add MagiskTheme composable wrapping MiuixTheme Create MagiskTheme that wraps MiuixTheme with light/dark color scheme based on the existing Config.darkTheme preference setting. Made-with: Cursor --- .../topjohnwu/magisk/ui/theme/MagiskTheme.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/MagiskTheme.kt diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/MagiskTheme.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/MagiskTheme.kt new file mode 100644 index 000000000..731cb8143 --- /dev/null +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/MagiskTheme.kt @@ -0,0 +1,22 @@ +package com.topjohnwu.magisk.ui.theme + +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import com.topjohnwu.magisk.core.Config +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.darkColorScheme +import top.yukonga.miuix.kmp.theme.lightColorScheme + +@Composable +fun MagiskTheme( + content: @Composable () -> Unit +) { + val darkTheme = when (Config.darkTheme) { + AppCompatDelegate.MODE_NIGHT_YES -> true + AppCompatDelegate.MODE_NIGHT_NO -> false + else -> isSystemInDarkTheme() + } + val colors = if (darkTheme) darkColorScheme() else lightColorScheme() + MiuixTheme(colors = colors, content = content) +} From 70a0dafb21b59eda5928672e7953983c64b64280 Mon Sep 17 00:00:00 2001 From: LoveSy Date: Tue, 3 Mar 2026 11:30:01 +0800 Subject: [PATCH 03/58] Migrate Home screen to Jetpack Compose with miuix Replace the Home screen's data-binding XML layouts with a Compose UI using miuix components. HomeViewModel now uses StateFlow instead of @Bindable/ObservableHost for UI state. HomeFragment hosts the new HomeScreen composable via ComposeView. Remove old XML layouts (fragment_home_md2, include_home_magisk, include_home_manager, item_developer, item_icon_link) and the DeveloperItem RvItem class that are no longer needed. Made-with: Cursor --- .../topjohnwu/magisk/ui/home/DeveloperItem.kt | 127 ------- .../topjohnwu/magisk/ui/home/HomeFragment.kt | 69 ++-- .../topjohnwu/magisk/ui/home/HomeScreen.kt | 352 ++++++++++++++++++ .../topjohnwu/magisk/ui/home/HomeViewModel.kt | 86 ++--- .../src/main/res/layout/fragment_home_md2.xml | 261 ------------- .../main/res/layout/include_home_magisk.xml | 174 --------- .../main/res/layout/include_home_manager.xml | 182 --------- .../src/main/res/layout/item_developer.xml | 51 --- .../src/main/res/layout/item_icon_link.xml | 39 -- 9 files changed, 417 insertions(+), 924 deletions(-) delete mode 100644 app/apk/src/main/java/com/topjohnwu/magisk/ui/home/DeveloperItem.kt create mode 100644 app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt delete mode 100644 app/apk/src/main/res/layout/fragment_home_md2.xml delete mode 100644 app/apk/src/main/res/layout/include_home_magisk.xml delete mode 100644 app/apk/src/main/res/layout/include_home_manager.xml delete mode 100644 app/apk/src/main/res/layout/item_developer.xml delete mode 100644 app/apk/src/main/res/layout/item_icon_link.xml diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/DeveloperItem.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/DeveloperItem.kt deleted file mode 100644 index 9da7c5095..000000000 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/DeveloperItem.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.topjohnwu.magisk.ui.home - -import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.core.Const -import com.topjohnwu.magisk.databinding.RvItem -import com.topjohnwu.magisk.core.R as CoreR - -interface Dev { - val name: String -} - -private interface JohnImpl : Dev { - override val name get() = "topjohnwu" -} - -private interface VvbImpl : Dev { - override val name get() = "vvb2060" -} - -private interface YUImpl : Dev { - override val name get() = "yujincheng08" -} - -private interface RikkaImpl : Dev { - override val name get() = "RikkaW" -} - -private interface CanyieImpl : Dev { - override val name get() = "canyie" -} - -sealed class DeveloperItem : Dev { - - abstract val items: List - val handle get() = "@${name}" - - object John : DeveloperItem(), JohnImpl { - override val items = - listOf( - object : IconLink.Twitter(), JohnImpl {}, - IconLink.Github.Project - ) - } - - object Vvb : DeveloperItem(), VvbImpl { - override val items = - listOf( - object : IconLink.Twitter(), VvbImpl {}, - object : IconLink.Github.User(), VvbImpl {} - ) - } - - object YU : DeveloperItem(), YUImpl { - override val items = - listOf( - object : IconLink.Twitter() { override val name = "shanasaimoe" }, - object : IconLink.Github.User(), YUImpl {}, - object : IconLink.Sponsor(), YUImpl {} - ) - } - - object Rikka : DeveloperItem(), RikkaImpl { - override val items = - listOf( - object : IconLink.Twitter() { override val name = "rikkawww" }, - object : IconLink.Github.User(), RikkaImpl {} - ) - } - - object Canyie : DeveloperItem(), CanyieImpl { - override val items = - listOf( - object : IconLink.Twitter() { override val name = "canyie2977" }, - object : IconLink.Github.User(), CanyieImpl {} - ) - } -} - -sealed class IconLink : RvItem() { - - abstract val icon: Int - abstract val title: Int - abstract val link: String - - override val layoutRes get() = R.layout.item_icon_link - - abstract class PayPal : IconLink(), Dev { - override val icon get() = CoreR.drawable.ic_paypal - override val title get() = CoreR.string.paypal - override val link get() = "https://paypal.me/$name" - - object Project : PayPal() { - override val name: String get() = "magiskdonate" - } - } - - object Patreon : IconLink() { - override val icon get() = CoreR.drawable.ic_patreon - override val title get() = CoreR.string.patreon - override val link get() = Const.Url.PATREON_URL - } - - abstract class Twitter : IconLink(), Dev { - override val icon get() = CoreR.drawable.ic_twitter - override val title get() = CoreR.string.twitter - override val link get() = "https://twitter.com/$name" - } - - abstract class Github : IconLink() { - override val icon get() = CoreR.drawable.ic_github - override val title get() = CoreR.string.github - - abstract class User : Github(), Dev { - override val link get() = "https://github.com/$name" - } - - object Project : Github() { - override val link get() = Const.Url.SOURCE_CODE_URL - } - } - - abstract class Sponsor : IconLink(), Dev { - override val icon get() = CoreR.drawable.ic_favorite - override val title get() = CoreR.string.github - override val link get() = "https://github.com/sponsors/$name" - } -} 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 index 1b5a76f7b..851e55df3 100644 --- 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 @@ -7,58 +7,55 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView +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 androidx.navigation.findNavController import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.arch.BaseFragment -import com.topjohnwu.magisk.arch.viewModel +import com.topjohnwu.magisk.arch.NavigationActivity +import com.topjohnwu.magisk.arch.VMFactory import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.download.DownloadEngine -import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding +import com.topjohnwu.magisk.ui.theme.MagiskTheme import com.topjohnwu.magisk.core.R as CoreR -import androidx.navigation.findNavController -import com.topjohnwu.magisk.arch.NavigationActivity -class HomeFragment : BaseFragment(), MenuProvider { +class HomeFragment : Fragment(), MenuProvider { - override val layoutRes = R.layout.fragment_home_md2 - override val viewModel by viewModel() + private val viewModel by lazy { + ViewModelProvider(this, VMFactory)[HomeViewModel::class.java] + } override fun onStart() { super.onStart() - activity?.setTitle(CoreR.string.section_home) + (activity as? NavigationActivity<*>)?.setTitle(CoreR.string.section_home) DownloadEngine.observeProgress(this, viewModel::onProgressUpdate) } - private fun checkTitle(text: TextView, icon: ImageView) { - text.post { - if (text.layout?.getEllipsisCount(0) != 0) { - with (icon) { - layoutParams.width = 0 - layoutParams.height = 0 - requestLayout() - } - } - } - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - super.onCreateView(inflater, container, savedInstanceState) - - // If titles are squished, hide icons - with(binding.homeMagiskWrapper) { - checkTitle(homeMagiskTitle, homeMagiskIcon) - } - with(binding.homeManagerWrapper) { - checkTitle(homeManagerTitle, homeManagerIcon) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MagiskTheme { + HomeScreen(viewModel = viewModel) + } + } } + } - return binding.root + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + 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) { @@ -77,14 +74,16 @@ class HomeFragment : BaseFragment(), MenuProvider { it.contentResolver, ) } - R.id.action_reboot -> activity?.let { RebootMenu.inflate(it).show() } - else -> return super.onOptionsItemSelected(item) + R.id.action_reboot -> (activity as? NavigationActivity<*>)?.let { + RebootMenu.inflate(it).show() + } + else -> return false } return true } override fun onResume() { super.onResume() - viewModel.stateManagerProgress = 0 + 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 new file mode 100644 index 000000000..5d37431bd --- /dev/null +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt @@ -0,0 +1,352 @@ +package com.topjohnwu.magisk.ui.home + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.Info +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.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.theme.MiuixTheme + +@Composable +fun HomeScreen(viewModel: HomeViewModel) { + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + + 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() + ) + } + + SupportCard(onLinkClicked = { viewModel.onLinkPressed(it) }) + + DevelopersCard(onLinkClicked = { openLink(context, it) }) + } +} + +@Composable +private fun NoticeCard(onHide: () -> Unit) { + Card(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(CoreR.string.home_notice_content), + style = MiuixTheme.textStyles.body2, + modifier = Modifier.weight(1f) + ) + TextButton( + text = stringResource(CoreR.string.hide), + onClick = onHide + ) + } + } +} + +@Composable +private fun MagiskCard(viewModel: HomeViewModel) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(CoreR.drawable.ic_magisk_outline), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = MiuixTheme.colorScheme.primary + ) + Spacer(Modifier.width(12.dp)) + Text( + text = stringResource(CoreR.string.magisk), + style = MiuixTheme.textStyles.headline2, + color = MiuixTheme.colorScheme.primary, + modifier = Modifier.weight(1f) + ) + when (viewModel.magiskState) { + HomeViewModel.State.OUTDATED -> TextButton( + text = stringResource(CoreR.string.update), + onClick = { viewModel.onMagiskPressed() } + ) + else -> TextButton( + text = stringResource(CoreR.string.install), + onClick = { viewModel.onMagiskPressed() } + ) + } + } + + Spacer(Modifier.height(8.dp)) + + InfoRow( + label = stringResource(CoreR.string.home_installed_version), + value = viewModel.magiskInstalledVersion.ifEmpty { + stringResource(CoreR.string.not_available) + } + ) + InfoRow( + label = stringResource(CoreR.string.zygisk), + value = stringResource(if (Info.isZygiskEnabled) CoreR.string.yes else CoreR.string.no) + ) + InfoRow( + label = "Ramdisk", + value = stringResource(if (Info.ramdisk) CoreR.string.yes else CoreR.string.no) + ) + } + } +} + +@Composable +private fun ManagerCard(viewModel: HomeViewModel, uiState: HomeViewModel.UiState) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.ic_manager), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = MiuixTheme.colorScheme.primary + ) + Spacer(Modifier.width(12.dp)) + Text( + text = stringResource(CoreR.string.home_app_title), + style = MiuixTheme.textStyles.headline2, + color = MiuixTheme.colorScheme.primary, + modifier = Modifier.weight(1f) + ) + when (uiState.appState) { + HomeViewModel.State.OUTDATED -> TextButton( + text = stringResource(CoreR.string.update), + onClick = { viewModel.onManagerPressed() } + ) + HomeViewModel.State.UP_TO_DATE -> TextButton( + text = stringResource(CoreR.string.install), + onClick = { viewModel.onManagerPressed() } + ) + else -> {} + } + } + + Spacer(Modifier.height(8.dp)) + + InfoRow( + label = stringResource(CoreR.string.home_latest_version), + value = uiState.managerRemoteVersion.ifEmpty { + stringResource( + if (uiState.appState == HomeViewModel.State.LOADING) CoreR.string.loading + else CoreR.string.not_available + ) + } + ) + InfoRow( + label = stringResource(CoreR.string.home_installed_version), + value = viewModel.managerInstalledVersion + ) + val context = LocalContext.current + InfoRow( + label = stringResource(CoreR.string.home_package), + value = context.packageName + ) + + if (uiState.managerProgress in 1..99) { + Spacer(Modifier.height(8.dp)) + LinearProgressIndicator( + progress = uiState.managerProgress / 100f, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} + +@Composable +private fun InfoRow(label: String, value: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = label, + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary + ) + Text( + text = value, + style = MiuixTheme.textStyles.body2, + ) + } +} + +@Composable +private fun SupportCard(onLinkClicked: (String) -> Unit) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(CoreR.string.home_support_title), + style = MiuixTheme.textStyles.headline2, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(CoreR.string.home_support_content), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary + ) + Spacer(Modifier.height(8.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + LinkChip( + icon = CoreR.drawable.ic_patreon, + label = stringResource(CoreR.string.patreon), + onClick = { onLinkClicked(com.topjohnwu.magisk.core.Const.Url.PATREON_URL) } + ) + LinkChip( + icon = CoreR.drawable.ic_paypal, + label = stringResource(CoreR.string.paypal), + onClick = { onLinkClicked("https://paypal.me/magiskdonate") } + ) + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun DevelopersCard(onLinkClicked: (String) -> Unit) { + val developers = listOf( + DeveloperInfo("topjohnwu", listOf( + LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/topjohnwu"), + LinkInfo("GitHub", CoreR.drawable.ic_github, com.topjohnwu.magisk.core.Const.Url.SOURCE_CODE_URL), + )), + DeveloperInfo("vvb2060", listOf( + LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/vvb2060"), + LinkInfo("GitHub", CoreR.drawable.ic_github, "https://github.com/vvb2060"), + )), + DeveloperInfo("yujincheng08", listOf( + LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/shanasaimoe"), + LinkInfo("GitHub", CoreR.drawable.ic_github, "https://github.com/yujincheng08"), + LinkInfo("Sponsor", CoreR.drawable.ic_favorite, "https://github.com/sponsors/yujincheng08"), + )), + DeveloperInfo("rikkawww", listOf( + LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/rikkawww"), + LinkInfo("GitHub", CoreR.drawable.ic_github, "https://github.com/rikkawww"), + )), + DeveloperInfo("canyie", listOf( + LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/canyie2977"), + LinkInfo("GitHub", CoreR.drawable.ic_github, "https://github.com/canyie"), + )), + ) + + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(CoreR.string.home_follow_title), + style = MiuixTheme.textStyles.headline2, + ) + Spacer(Modifier.height(8.dp)) + developers.forEach { dev -> + DeveloperRow(dev = dev, onLinkClicked = onLinkClicked) + Spacer(Modifier.height(4.dp)) + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun DeveloperRow(dev: DeveloperInfo, onLinkClicked: (String) -> Unit) { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = "@${dev.name}", + style = MiuixTheme.textStyles.body1, + ) + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + dev.links.forEach { link -> + LinkChip( + icon = link.icon, + label = link.label, + onClick = { onLinkClicked(link.url) } + ) + } + } + } +} + +@Composable +private fun LinkChip(icon: Int, label: String, onClick: () -> Unit) { + Row( + modifier = Modifier + .clickable(onClick = onClick) + .padding(vertical = 6.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + painter = painterResource(icon), + contentDescription = label, + modifier = Modifier.size(18.dp), + tint = MiuixTheme.colorScheme.onSurfaceVariantActions + ) + Text( + text = label, + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantActions + ) + } +} + +private data class DeveloperInfo(val name: String, val links: List) +private data class LinkInfo(val label: String, val icon: Int, val url: String) + +private fun openLink(context: Context, url: String) { + try { + context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) + } catch (_: ActivityNotFoundException) { } +} 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 a82b6cc20..d681252f5 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 @@ -5,13 +5,8 @@ import android.content.Context import android.content.Intent import android.widget.Toast import androidx.core.net.toUri -import androidx.databinding.Bindable -import com.topjohnwu.magisk.BR -import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.arch.ActivityExecutor import com.topjohnwu.magisk.arch.AsyncLoadViewModel import com.topjohnwu.magisk.arch.ContextExecutor -import com.topjohnwu.magisk.arch.UIActivity import com.topjohnwu.magisk.arch.ViewEvent import com.topjohnwu.magisk.core.BuildConfig import com.topjohnwu.magisk.core.Config @@ -21,14 +16,15 @@ import com.topjohnwu.magisk.core.download.Subject.App import com.topjohnwu.magisk.core.ktx.await import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.repository.NetworkService -import com.topjohnwu.magisk.databinding.bindExtra -import com.topjohnwu.magisk.databinding.set 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.utils.asText import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlin.math.roundToInt import com.topjohnwu.magisk.core.R as CoreR @@ -40,14 +36,15 @@ class HomeViewModel( LOADING, INVALID, OUTDATED, UP_TO_DATE } - val magiskTitleBarrierIds = - intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button) - val appTitleBarrierIds = - intArrayOf(R.id.home_manager_icon, R.id.home_manager_title, R.id.home_manager_button) + data class UiState( + val isNoticeVisible: Boolean = Config.safetyNotice, + val appState: State = State.LOADING, + val managerRemoteVersion: String = "", + val managerProgress: Int = 0, + ) - @get:Bindable - var isNoticeVisible = Config.safetyNotice - set(value) = set(value, field, { field = it }, BR.noticeVisible) + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState.asStateFlow() val magiskState get() = when { @@ -57,52 +54,34 @@ class HomeViewModel( else -> State.UP_TO_DATE } - @get:Bindable - var appState = State.LOADING - set(value) = set(value, field, { field = it }, BR.appState) - - val magiskInstalledVersion + val magiskInstalledVersion: String get() = Info.env.run { if (isActive) - ("$versionString ($versionCode)" + if (isDebug) " (D)" else "").asText() + "$versionString ($versionCode)" + if (isDebug) " (D)" else "" else - CoreR.string.not_available.asText() + "" } - @get:Bindable - var managerRemoteVersion = CoreR.string.loading.asText() - set(value) = set(value, field, { field = it }, BR.managerRemoteVersion) - - val managerInstalledVersion + val managerInstalledVersion: String get() = "${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})" + if (BuildConfig.DEBUG) " (D)" else "" - @get:Bindable - var stateManagerProgress = 0 - set(value) = set(value, field, { field = it }, BR.stateManagerProgress) - - val extraBindings = bindExtra { - it.put(BR.viewModel, this) - } - companion object { private var checkedEnv = false } override suspend fun doLoadWork() { - appState = State.LOADING + _uiState.update { it.copy(appState = State.LOADING) } Info.fetchUpdate(svc)?.apply { - appState = when { - BuildConfig.APP_VERSION_CODE < versionCode -> State.OUTDATED - else -> State.UP_TO_DATE - } - val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL - managerRemoteVersion = - ("$version (${versionCode})" + if (isDebug) " (D)" else "").asText() + _uiState.update { + it.copy( + appState = if (BuildConfig.APP_VERSION_CODE < versionCode) State.OUTDATED else State.UP_TO_DATE, + managerRemoteVersion = "$version ($versionCode)" + if (isDebug) " (D)" else "" + ) + } } ?: run { - appState = State.INVALID - managerRemoteVersion = CoreR.string.not_available.asText() + _uiState.update { it.copy(appState = State.INVALID, managerRemoteVersion = "") } } ensureEnv() } @@ -111,7 +90,11 @@ class HomeViewModel( fun onProgressUpdate(progress: Float, subject: Subject) { if (subject is App) - stateManagerProgress = progress.times(100f).roundToInt() + _uiState.update { it.copy(managerProgress = progress.times(100f).roundToInt()) } + } + + fun resetProgress() { + _uiState.update { it.copy(managerProgress = 0) } } fun onLinkPressed(link: String) = object : ViewEvent(), ContextExecutor { @@ -128,7 +111,7 @@ class HomeViewModel( fun onDeletePressed() = UninstallDialog().show() - fun onManagerPressed() = when (appState) { + fun onManagerPressed() = when (_uiState.value.appState) { State.LOADING -> SnackbarEvent(CoreR.string.loading).publish() State.INVALID -> SnackbarEvent(CoreR.string.no_connection).publish() else -> withExternalRW { @@ -144,7 +127,7 @@ class HomeViewModel( fun hideNotice() { Config.safetyNotice = false - isNoticeVisible = false + _uiState.update { it.copy(isNoticeVisible = false) } } private suspend fun ensureEnv() { @@ -156,11 +139,4 @@ class HomeViewModel( } checkedEnv = true } - - val showTest = false - fun onTestPressed() = object : ViewEvent(), ActivityExecutor { - override fun invoke(activity: UIActivity<*>) { - /* Entry point to trigger test events within the app */ - } - }.publish() } diff --git a/app/apk/src/main/res/layout/fragment_home_md2.xml b/app/apk/src/main/res/layout/fragment_home_md2.xml deleted file mode 100644 index a3d2c60cd..000000000 --- a/app/apk/src/main/res/layout/fragment_home_md2.xml +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - - - - - - - - - - - - - -