diff --git a/.github/workflows/build-manager.yml b/.github/workflows/build-manager.yml index 89360617be92..f27cee4e7d8d 100644 --- a/.github/workflows/build-manager.yml +++ b/.github/workflows/build-manager.yml @@ -110,15 +110,7 @@ jobs: cp -f ../x86_64-linux-android/release/ksud ../manager/app/src/main/jniLibs/x86_64/libksud.so - name: Build with Gradle - run: | - { - echo 'org.gradle.parallel=true' - echo 'org.gradle.vfs.watch=true' - echo 'org.gradle.jvmargs=-Xmx2048m' - echo 'android.native.buildOutput=verbose' - } >> gradle.properties - sed -i 's/org.gradle.configuration-cache=true//g' gradle.properties - ./gradlew clean assembleRelease + run: ./gradlew clean assembleRelease - name: Upload build artifact uses: actions/upload-artifact@v4 diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index ce7cf2f1e826..3c4187620dae 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -29,6 +29,7 @@ android { release { isMinifyEnabled = true isShrinkResources = true + vcsInfo.include = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } @@ -40,8 +41,8 @@ android { prefab = true } - kotlinOptions { - jvmTarget = "21" + kotlin { + jvmToolchain(21) } packaging { @@ -102,8 +103,6 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.material.icons.extended) - implementation(libs.androidx.compose.material) - implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) @@ -127,14 +126,11 @@ dependencies { implementation(libs.kotlinx.coroutines.core) - implementation(libs.me.zhanghai.android.appiconloader.coil) - - implementation(libs.sheet.compose.dialogs.core) - implementation(libs.sheet.compose.dialogs.list) - implementation(libs.sheet.compose.dialogs.input) - implementation(libs.markdown) implementation(libs.androidx.webkit) implementation(libs.lsposed.cxx) + + implementation(libs.miuix) + implementation(libs.haze) } \ No newline at end of file diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml index 11cda5f21cd1..025709ccba1a 100644 --- a/manager/app/src/main/AndroidManifest.xml +++ b/manager/app/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ android:name=".KernelSUApplication" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" - android:enableOnBackInvokedCallback="true" + android:enableOnBackInvokedCallback="false" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" @@ -19,7 +19,8 @@ + android:theme="@style/Theme.KernelSU" + android:windowSoftInputMode="adjustResize"> diff --git a/manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt b/manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt index e09cc135a044..f6453857ad21 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt @@ -2,10 +2,6 @@ package me.weishu.kernelsu import android.app.Application import android.system.Os -import coil.Coil -import coil.ImageLoader -import me.zhanghai.android.appiconloader.coil.AppIconFetcher -import me.zhanghai.android.appiconloader.coil.AppIconKeyer import okhttp3.Cache import okhttp3.OkHttpClient import java.io.File @@ -21,17 +17,6 @@ class KernelSUApplication : Application() { super.onCreate() ksuApp = this - val context = this - val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size) - Coil.setImageLoader( - ImageLoader.Builder(context) - .components { - add(AppIconKeyer()) - add(AppIconFetcher.Factory(iconSize, false, context)) - } - .build() - ) - val webroot = File(dataDir, "webroot") if (!webroot.exists()) { webroot.mkdir() @@ -50,6 +35,4 @@ class KernelSUApplication : Application() { ) }.build() } - - } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/KsuService.java b/manager/app/src/main/java/me/weishu/kernelsu/ui/KsuService.java index 2ebcc786391d..b953dd49f796 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/KsuService.java +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/KsuService.java @@ -29,15 +29,6 @@ public class KsuService extends RootService { private static final String TAG = "KsuService"; - class Stub extends IKsuInterface.Stub { - @Override - public ParcelableListSlice getPackages(int flags) { - List list = getInstalledPackagesAll(flags); - Log.i(TAG, "getPackages: " + list.size()); - return new ParcelableListSlice<>(list); - } - } - @Override public IBinder onBind(@NonNull Intent intent) { return new Stub(); @@ -74,4 +65,13 @@ List getInstalledPackagesAsUser(int flags, int userId) { return new ArrayList<>(); } + + class Stub extends IKsuInterface.Stub { + @Override + public ParcelableListSlice getPackages(int flags) { + List list = getInstalledPackagesAll(flags); + Log.i(TAG, "getPackages: " + list.size()); + return new ParcelableListSlice<>(list); + } + } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt index 6db05aa6c216..1a92ebe59581 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt @@ -3,52 +3,49 @@ package me.weishu.kernelsu.ui import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.displayCutout -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.union -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.NavGraphs -import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState -import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeSource +import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp -import me.weishu.kernelsu.ui.screen.BottomBarDestination +import me.weishu.kernelsu.ui.component.BottomBar +import me.weishu.kernelsu.ui.screen.HomePager +import me.weishu.kernelsu.ui.screen.ModulePager +import me.weishu.kernelsu.ui.screen.SuperUserPager import me.weishu.kernelsu.ui.theme.KernelSUTheme -import me.weishu.kernelsu.ui.util.LocalSnackbarHost -import me.weishu.kernelsu.ui.util.rootAvailable import me.weishu.kernelsu.ui.util.install +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.theme.MiuixTheme class MainActivity : ComponentActivity() { @@ -68,108 +65,104 @@ class MainActivity : ComponentActivity() { setContent { KernelSUTheme { val navController = rememberNavController() - val snackBarHostState = remember { SnackbarHostState() } - val bottomBarRoutes = remember { - BottomBarDestination.entries.map { it.direction.route }.toSet() - } - Scaffold( - bottomBar = { BottomBar(navController) }, - contentWindowInsets = WindowInsets(0, 0, 0, 0) - ) { innerPadding -> - CompositionLocalProvider( - LocalSnackbarHost provides snackBarHostState, - ) { - DestinationsNavHost( - modifier = Modifier.padding(innerPadding), - navGraph = NavGraphs.root, - navController = navController, - defaultTransitions = object : NavHostAnimatedDestinationStyle() { - override val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition = { - // If the target is a detail page (not a bottom navigation page), slide in from the right - if (targetState.destination.route !in bottomBarRoutes) { - slideInHorizontally(initialOffsetX = { it }) - } else { - // Otherwise (switching between bottom navigation pages), use fade in - fadeIn(animationSpec = tween(340)) - } + + Scaffold { innerPadding -> + DestinationsNavHost( + modifier = Modifier, + navGraph = NavGraphs.root, + navController = navController, + defaultTransitions = object : NavHostAnimatedDestinationStyle() { + override val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition = + { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) } - override val exitTransition: AnimatedContentTransitionScope.() -> ExitTransition = { - // If navigating from the home page (bottom navigation page) to a detail page, slide out to the left - if (initialState.destination.route in bottomBarRoutes && targetState.destination.route !in bottomBarRoutes) { - slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut() - } else { - // Otherwise (switching between bottom navigation pages), use fade out - fadeOut(animationSpec = tween(340)) - } + override val exitTransition: AnimatedContentTransitionScope.() -> ExitTransition = + { + slideOutHorizontally( + targetOffsetX = { -it / 5 }, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) } - override val popEnterTransition: AnimatedContentTransitionScope.() -> EnterTransition = { - // If returning to the home page (bottom navigation page), slide in from the left - if (targetState.destination.route in bottomBarRoutes) { - slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn() - } else { - // Otherwise (e.g., returning between multiple detail pages), use default fade in - fadeIn(animationSpec = tween(340)) - } + override val popEnterTransition: AnimatedContentTransitionScope.() -> EnterTransition = + { + slideInHorizontally( + initialOffsetX = { -it / 5 }, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) } - override val popExitTransition: AnimatedContentTransitionScope.() -> ExitTransition = { - // If returning from a detail page (not a bottom navigation page), scale down and fade out - if (initialState.destination.route !in bottomBarRoutes) { - scaleOut(targetScale = 0.9f) + fadeOut() - } else { - // Otherwise, use default fade out - fadeOut(animationSpec = tween(340)) - } + override val popExitTransition: AnimatedContentTransitionScope.() -> ExitTransition = + { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) } - } - ) - } + } + ) } } } } } + +val LocalPagerState = compositionLocalOf { error("No pager state") } +val LocalHandlePageChange = compositionLocalOf<(Int) -> Unit> { error("No handle page change") } + @Composable -private fun BottomBar(navController: NavHostController) { - val navigator = navController.rememberDestinationsNavigator() - val isManager = Natives.becomeManager(ksuApp.packageName) - val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() - NavigationBar( - tonalElevation = 8.dp, - windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom - ) +@Destination(start = true) +fun MainScreen(navController: DestinationsNavigator) { + val activity = LocalActivity.current + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 }) + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = MiuixTheme.colorScheme.background, + tint = HazeTint(MiuixTheme.colorScheme.background.copy(0.8f)) + ) + val handlePageChange: (Int) -> Unit = remember(pagerState, coroutineScope) { + { page -> + coroutineScope.launch { pagerState.animateScrollToPage(page) } + } + } + + BackHandler { + if (pagerState.currentPage != 0) { + coroutineScope.launch { + pagerState.animateScrollToPage(0) + } + } else { + activity?.finishAndRemoveTask() + } + } + + CompositionLocalProvider( + LocalPagerState provides pagerState, + LocalHandlePageChange provides handlePageChange ) { - BottomBarDestination.entries.forEach { destination -> - if (!fullFeatured && destination.rootRequired) return@forEach - val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) - NavigationBarItem( - selected = isCurrentDestOnBackStack, - onClick = { - if (isCurrentDestOnBackStack) { - navigator.popBackStack(destination.direction, false) - } - navigator.navigate(destination.direction) { - popUpTo(NavGraphs.root) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - icon = { - if (isCurrentDestOnBackStack) { - Icon(destination.iconSelected, stringResource(destination.label)) - } else { - Icon(destination.iconNotSelected, stringResource(destination.label)) - } - }, - label = { Text(stringResource(destination.label)) }, - alwaysShowLabel = false - ) + Scaffold( + bottomBar = { + BottomBar(hazeState, hazeStyle) + }, + ) { innerPadding -> + HorizontalPager( + modifier = Modifier.hazeSource(state = hazeState), + state = pagerState, + beyondViewportPageCount = 1, + userScrollEnabled = false + ) { + when (it) { + 0 -> HomePager(pagerState, navController, innerPadding.calculateBottomPadding()) + 1 -> SuperUserPager(navController, innerPadding.calculateBottomPadding()) + 2 -> ModulePager(navController, innerPadding.calculateBottomPadding()) + } + } } } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt deleted file mode 100644 index 803447391af3..000000000000 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt +++ /dev/null @@ -1,125 +0,0 @@ -package me.weishu.kernelsu.ui.component - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -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.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.fromHtml -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import me.weishu.kernelsu.BuildConfig -import me.weishu.kernelsu.R - -@Preview -@Composable -fun AboutCard() { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - ) { - AboutCardContent() - } - } -} - -@Composable -fun AboutDialog(dismiss: () -> Unit) { - Dialog( - onDismissRequest = { dismiss() } - ) { - AboutCard() - } -} - -@Composable -private fun AboutCardContent() { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Row { - Surface( - modifier = Modifier.size(40.dp), - color = colorResource(id = R.color.ic_launcher_background), - shape = CircleShape - ) { - Image( - painter = painterResource(id = R.drawable.ic_launcher_foreground), - contentDescription = "icon", - modifier = Modifier.scale(1.4f) - ) - } - - Spacer(modifier = Modifier.width(12.dp)) - - Column { - - Text( - stringResource(id = R.string.app_name), - style = MaterialTheme.typography.titleSmall, - fontSize = 18.sp - ) - Text( - BuildConfig.VERSION_NAME, - style = MaterialTheme.typography.bodySmall, - fontSize = 14.sp - ) - - Spacer(modifier = Modifier.height(8.dp)) - - val annotatedString = AnnotatedString.Companion.fromHtml( - htmlString = stringResource( - id = R.string.about_source_code, - "GitHub", - "Telegram" - ), - linkStyles = TextLinkStyles( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ), - pressedStyle = SpanStyle( - color = MaterialTheme.colorScheme.primary, - background = MaterialTheme.colorScheme.secondaryContainer, - textDecoration = TextDecoration.Underline - ) - ) - ) - Text( - text = annotatedString, - style = TextStyle( - fontSize = 14.sp - ) - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AppIconImage.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AppIconImage.kt new file mode 100644 index 000000000000..a2c1130961ed --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AppIconImage.kt @@ -0,0 +1,57 @@ +package me.weishu.kernelsu.ui.component + +import android.content.pm.PackageInfo +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape + +@Composable +fun AppIconImage( + packageInfo: PackageInfo, + label: String, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + var icon by remember(packageInfo.packageName) { mutableStateOf(null) } + + LaunchedEffect(packageInfo.packageName) { + withContext(Dispatchers.IO) { + val drawable = packageInfo.applicationInfo?.loadIcon(context.packageManager) + val bitmap = drawable?.toBitmap()?.asImageBitmap() + icon = bitmap + } + } + + icon.let { imageBitmap -> + imageBitmap?.let { + Image( + bitmap = it, + contentDescription = label, + modifier = modifier + ) + } + } ?: Box( + modifier = modifier + .clip(G2RoundedCornerShape(12.dp)) + .background(colorScheme.secondaryContainer), + contentAlignment = Alignment.Center + ) {} +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/BottomBar.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/BottomBar.kt new file mode 100644 index 000000000000..bee248cc66dc --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/BottomBar.kt @@ -0,0 +1,68 @@ +package me.weishu.kernelsu.ui.component + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Cottage +import androidx.compose.material.icons.rounded.Extension +import androidx.compose.material.icons.rounded.Security +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.hazeEffect +import me.weishu.kernelsu.Natives +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ksuApp +import me.weishu.kernelsu.ui.LocalHandlePageChange +import me.weishu.kernelsu.ui.LocalPagerState +import me.weishu.kernelsu.ui.util.rootAvailable +import top.yukonga.miuix.kmp.basic.NavigationBar +import top.yukonga.miuix.kmp.basic.NavigationItem + + +@Composable +fun BottomBar( + hazeState: HazeState, + hazeStyle: HazeStyle +) { + val isManager = Natives.becomeManager(ksuApp.packageName) + val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() + + val page = LocalPagerState.current.targetPage + val handlePageChange = LocalHandlePageChange.current + + if (!fullFeatured) return + + val item = BottomBarDestination.entries.mapIndexed { index, destination -> + NavigationItem( + label = stringResource(destination.label), + icon = destination.icon, + ) + } + + NavigationBar( + modifier = Modifier + .hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + }, + color = Color.Transparent, + items = item, + selected = page, + onClick = handlePageChange + ) +} + +enum class BottomBarDestination( + @get:StringRes val label: Int, + val icon: ImageVector, +) { + Home(R.string.home, Icons.Rounded.Cottage), + SuperUser(R.string.superuser, Icons.Rounded.Security), + Module(R.string.module, Icons.Rounded.Extension) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/ChooseKmiDialog.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/ChooseKmiDialog.kt new file mode 100644 index 000000000000..e758dbe216b1 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/ChooseKmiDialog.kt @@ -0,0 +1,74 @@ +package me.weishu.kernelsu.ui.component + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.util.getSupportedKmis +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme + +@Composable +fun ChooseKmiDialog( + showDialog: MutableState, + onSelected: (String?) -> Unit +) { + val supportedKmi by produceState(initialValue = emptyList()) { + value = getSupportedKmis() + } + val options = supportedKmi.map { it } + + SuperDialog( + show = showDialog, + insideMargin = DpSize(0.dp, 0.dp), + onDismissRequest = { + showDialog.value = false + }, + content = { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp, bottom = 12.dp), + text = stringResource(R.string.select_kmi), + fontSize = MiuixTheme.textStyles.title4.fontSize, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = colorScheme.onSurface + ) + options.forEachIndexed { index, type -> + SuperArrow( + title = type, + onClick = { + onSelected(type) + showDialog.value = false + }, + insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) + ) + } + TextButton( + text = stringResource(id = android.R.string.cancel), + onClick = { + showDialog.value = false + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 24.dp) + .padding(horizontal = 24.dp) + ) + } + ) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt index 27adc3f03ccc..0c7cb6829561 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt @@ -8,39 +8,58 @@ import android.text.method.LinkMovementMethod import android.util.Log import android.view.ViewGroup import android.widget.TextView +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import io.noties.markwon.Markwon import io.noties.markwon.utils.NoCopySpannableFactory -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.parcelize.Parcelize +import me.weishu.kernelsu.R +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme import kotlin.coroutines.resume private const val TAG = "DialogComponent" interface ConfirmDialogVisuals : Parcelable { val title: String - val content: String + val content: String? val isMarkdown: Boolean val confirm: String? val dismiss: String? @@ -49,7 +68,7 @@ interface ConfirmDialogVisuals : Parcelable { @Parcelize private data class ConfirmDialogVisualsImpl( override val title: String, - override val content: String, + override val content: String?, override val isMarkdown: Boolean, override val confirm: String?, override val dismiss: String?, @@ -81,7 +100,7 @@ interface ConfirmDialogHandle : DialogHandle { fun showConfirm( title: String, - content: String, + content: String? = null, markdown: Boolean = false, confirm: String? = null, dismiss: String? = null @@ -89,7 +108,7 @@ interface ConfirmDialogHandle : DialogHandle { suspend fun awaitConfirm( title: String, - content: String, + content: String? = null, markdown: Boolean = false, confirm: String? = null, dismiss: String? = null @@ -153,7 +172,10 @@ interface ConfirmCallback { val isEmpty: Boolean get() = onConfirm == null && onDismiss == null companion object { - operator fun invoke(onConfirmProvider: () -> NullableCallback, onDismissProvider: () -> NullableCallback): ConfirmCallback { + operator fun invoke( + onConfirmProvider: () -> NullableCallback, + onDismissProvider: () -> NullableCallback + ): ConfirmCallback { return object : ConfirmCallback { override val onConfirm: NullableCallback get() = onConfirmProvider() @@ -244,7 +266,7 @@ private class ConfirmDialogHandleImpl( override fun showConfirm( title: String, - content: String, + content: String?, markdown: Boolean, confirm: String?, dismiss: String? @@ -257,7 +279,7 @@ private class ConfirmDialogHandleImpl( override suspend fun awaitConfirm( title: String, - content: String, + content: String?, markdown: Boolean, confirm: String?, dismiss: String? @@ -293,23 +315,12 @@ private class ConfirmDialogHandleImpl( } } -private class CustomDialogHandleImpl( - visible: MutableState, - coroutineScope: CoroutineScope -) : DialogHandleBase(visible, coroutineScope) { - override val dialogType: String get() = "CustomDialog" -} - @Composable fun rememberLoadingDialog(): LoadingDialogHandle { - val visible = remember { - mutableStateOf(false) - } + val visible = remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() - if (visible.value) { - LoadingDialog() - } + LoadingDialog(visible) return remember { LoadingDialogHandleImpl(visible, coroutineScope) @@ -337,7 +348,8 @@ private fun rememberConfirmDialog(visuals: ConfirmDialogVisuals, callback: Confi ConfirmDialog( handle.visuals, confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } }, - dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } } + dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } }, + showDialog = visible ) } @@ -364,69 +376,86 @@ fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle { } @Composable -fun rememberCustomDialog(composable: @Composable (dismiss: () -> Unit) -> Unit): DialogHandle { - val visible = rememberSaveable { - mutableStateOf(false) - } - val coroutineScope = rememberCoroutineScope() - if (visible.value) { - composable { visible.value = false } - } - return remember { - CustomDialogHandleImpl(visible, coroutineScope) - } -} - -@Composable -private fun LoadingDialog() { - Dialog( +private fun LoadingDialog(showDialog: MutableState) { + SuperDialog( + show = showDialog, onDismissRequest = {}, - properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false) - ) { - Surface( - modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp) - ) { + content = { Box( - contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart ) { - CircularProgressIndicator() + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + InfiniteProgressIndicator( + color = MiuixTheme.colorScheme.onBackground + ) + Text( + modifier = Modifier.padding(start = 12.dp), + text = stringResource(R.string.processing), + fontWeight = FontWeight.Medium + ) + } } } - } + ) } @Composable -private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) { - AlertDialog( +private fun ConfirmDialog( + visuals: ConfirmDialogVisuals, + confirm: () -> Unit, + dismiss: () -> Unit, + showDialog: MutableState +) { + SuperDialog( + show = showDialog, + title = visuals.title, onDismissRequest = { - dismiss() - }, - title = { - Text(text = visuals.title) - }, - text = { - if (visuals.isMarkdown) { - MarkdownContent(content = visuals.content) - } else { - Text(text = visuals.content) - } + showDialog.value = false }, - confirmButton = { - TextButton(onClick = confirm) { - Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = dismiss) { - Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel)) + content = { + Column { + visuals.content?.let { + if (visuals.isMarkdown) { + MarkdownContent(content = visuals.content!!) + } else { + Text(text = visuals.content!!) + } + } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.padding(top = 12.dp) + ) { + TextButton( + text = visuals.dismiss ?: stringResource(id = android.R.string.cancel), + onClick = { + dismiss() + showDialog.value = false + }, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(20.dp)) + TextButton( + text = visuals.confirm ?: stringResource(id = android.R.string.ok), + onClick = { + confirm() + showDialog.value = false + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } } - }, + } ) } @Composable private fun MarkdownContent(content: String) { - val contentColor = LocalContentColor.current + val contentColor = MiuixTheme.colorScheme.onBackground.toArgb() AndroidView( factory = { context -> @@ -447,7 +476,7 @@ private fun MarkdownContent(content: String) { .wrapContentHeight(), update = { Markwon.create(it.context).setMarkdown(it, content) - it.setTextColor(contentColor.toArgb()) + it.setTextColor(contentColor) } ) -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/DropdownItem.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/DropdownItem.kt new file mode 100644 index 000000000000..9f20d52ddef0 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/DropdownItem.kt @@ -0,0 +1,48 @@ +package me.weishu.kernelsu.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.extra.DropdownColors +import top.yukonga.miuix.kmp.extra.DropdownDefaults +import top.yukonga.miuix.kmp.theme.MiuixTheme + +@Composable +fun DropdownItem( + text: String, + optionSize: Int, + index: Int, + dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(), + onSelectedIndexChange: (Int) -> Unit +) { + val currentOnSelectedIndexChange = rememberUpdatedState(onSelectedIndexChange) + val additionalTopPadding = if (index == 0) 20f.dp else 12f.dp + val additionalBottomPadding = if (index == optionSize - 1) 20f.dp else 12f.dp + + Row( + modifier = Modifier + .clickable { currentOnSelectedIndexChange.value(index) } + .background(dropdownColors.containerColor) + .padding(horizontal = 20.dp) + .padding( + top = additionalTopPadding, + bottom = additionalBottomPadding + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + fontSize = MiuixTheme.textStyles.body1.fontSize, + fontWeight = FontWeight.Medium, + color = dropdownColors.contentColor, + ) + } +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/EditText.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/EditText.kt new file mode 100644 index 000000000000..7e29f5838e04 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/EditText.kt @@ -0,0 +1,199 @@ +package me.weishu.kernelsu.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import kotlin.math.max + +@Composable +fun EditText( + title: String, + summary: String? = null, + textValue: MutableState, + onTextValueChange: (String) -> Unit = {}, + textHint: String = "", + enabled: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + titleColor: BasicComponentColors = EditTextDefaults.titleColor(), + summaryColor: BasicComponentColors = EditTextDefaults.summaryColor(), + rightActionColor: BasicComponentColors = EditTextDefaults.rightActionColors(), + isError: Boolean = false, +) { + val interactionSource = remember { MutableInteractionSource() } + val coroutineScope = rememberCoroutineScope() + val focused = interactionSource.collectIsFocusedAsState().value + val focusRequester = remember { FocusRequester() } + if (focused) { + focusRequester.requestFocus() + } + + Box( + modifier = Modifier + .clickable( + indication = null, + interactionSource = null + ) { + if (enabled) { + coroutineScope.launch { + interactionSource.emit(FocusInteraction.Focus()) + } + } + } + .heightIn(min = 56.dp) + .fillMaxWidth() + .padding(EditTextDefaults.InsideMargin), + ) { + Layout( + content = { + Text( + text = title, + fontSize = MiuixTheme.textStyles.headline1.fontSize, + fontWeight = FontWeight.Medium, + color = titleColor.color(enabled) + ) + summary?.let { + Text( + text = it, + fontSize = MiuixTheme.textStyles.body2.fontSize, + color = summaryColor.color(enabled) + ) + } + BasicTextField( + value = textValue.value, + onValueChange = { + onTextValueChange(it) + }, + modifier = Modifier + .focusRequester(focusRequester) + .semantics { + onClick { + focusRequester.requestFocus() + true + } + }, + enabled = enabled, + textStyle = MiuixTheme.textStyles.main.copy( + textAlign = TextAlign.End, + color = if (isError) { + Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) + } else { + rightActionColor.color(enabled) + } + ), + keyboardOptions = keyboardOptions, + cursorBrush = SolidColor(colorScheme.primary), + interactionSource = interactionSource, + decorationBox = + @Composable { innerTextField -> + Box( + contentAlignment = Alignment.CenterEnd + ) { + Text( + text = if (textValue.value.isEmpty()) textHint else "", + color = rightActionColor.color(enabled), + textAlign = TextAlign.End, + softWrap = false, + maxLines = 1 + ) + innerTextField() + } + } + ) + } + ) { measurables, constraints -> + val leftConstraints = constraints.copy(maxWidth = constraints.maxWidth / 2) + val hasSummary = measurables.size > 2 + val titleText = measurables[0].measure(leftConstraints) + val summaryText = (if (hasSummary) measurables[1] else null)?.measure(leftConstraints) + val leftWidth = max(titleText.width, (summaryText?.width ?: 0)) + val leftHeight = titleText.height + (summaryText?.height ?: 0) + val rightWidth = constraints.maxWidth - leftWidth - 16.dp.roundToPx() + val rightConstraints = constraints.copy(maxWidth = rightWidth) + val inputField = (if (hasSummary) measurables[2] else measurables[1]).measure(rightConstraints) + val totalHeight = max(leftHeight, inputField.height) + layout(constraints.maxWidth, totalHeight) { + val titleY = (totalHeight - leftHeight) / 2 + titleText.placeRelative(0, titleY) + summaryText?.placeRelative(0, titleY + titleText.height) + inputField.placeRelative(constraints.maxWidth - inputField.width, (totalHeight - inputField.height) / 2) + } + } + } +} + +object EditTextDefaults { + val InsideMargin = PaddingValues(16.dp) + + @Composable + fun titleColor( + color: Color = colorScheme.onSurface, + disabledColor: Color = colorScheme.disabledOnSecondaryVariant + ): BasicComponentColors { + return BasicComponentColors( + color = color, + disabledColor = disabledColor + ) + } + + @Composable + fun summaryColor( + color: Color = colorScheme.onSurfaceVariantSummary, + disabledColor: Color = colorScheme.disabledOnSecondaryVariant + ): BasicComponentColors { + return BasicComponentColors( + color = color, + disabledColor = disabledColor + ) + } + + @Composable + fun rightActionColors( + color: Color = colorScheme.onSurfaceVariantActions, + disabledColor: Color = colorScheme.disabledOnSecondaryVariant, + ): BasicComponentColors { + return BasicComponentColors( + color = color, + disabledColor = disabledColor + ) + } +} + +@Immutable +class BasicComponentColors( + private val color: Color, + private val disabledColor: Color +) { + @Stable + fun color(enabled: Boolean): Color = if (enabled) color else disabledColor +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/KeyEventBlocker.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/KeyEventBlocker.kt index b3268131268a..5e392efdb764 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/KeyEventBlocker.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/KeyEventBlocker.kt @@ -25,4 +25,4 @@ fun KeyEventBlocker(predicate: (KeyEvent) -> Boolean) { LaunchedEffect(Unit) { requester.requestFocus() } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/KsuValidCheck.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/KsuValidCheck.kt index badf1d0fb79e..bc68262a6a92 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/KsuValidCheck.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/KsuValidCheck.kt @@ -1,17 +1,17 @@ package me.weishu.kernelsu.ui.component - + import androidx.compose.runtime.Composable import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp - + @Composable fun KsuIsValid( content: @Composable () -> Unit ) { val isManager = Natives.becomeManager(ksuApp.packageName) val ksuVersion = if (isManager) Natives.version else null - + if (ksuVersion != null) { content() } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt deleted file mode 100644 index b388cb49d44a..000000000000 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt +++ /dev/null @@ -1,158 +0,0 @@ -package me.weishu.kernelsu.ui.component - -import android.util.Log -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp - -private const val TAG = "SearchBar" - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SearchAppBar( - title: @Composable () -> Unit, - searchText: String, - onSearchTextChange: (String) -> Unit, - onClearClick: () -> Unit, - onBackClick: (() -> Unit)? = null, - onConfirm: (() -> Unit)? = null, - dropdownContent: @Composable (() -> Unit)? = null, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val keyboardController = LocalSoftwareKeyboardController.current - val focusRequester = remember { FocusRequester() } - var onSearch by remember { mutableStateOf(false) } - - if (onSearch) { - LaunchedEffect(Unit) { focusRequester.requestFocus() } - } - DisposableEffect(Unit) { - onDispose { - keyboardController?.hide() - } - } - - TopAppBar( - title = { - Box { - AnimatedVisibility( - modifier = Modifier.align(Alignment.CenterStart), - visible = !onSearch, - enter = fadeIn(), - exit = fadeOut(), - content = { title() } - ) - - AnimatedVisibility( - visible = onSearch, - enter = fadeIn(), - exit = fadeOut() - ) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(top = 2.dp, bottom = 2.dp, end = if (onBackClick != null) 0.dp else 14.dp) - .focusRequester(focusRequester) - .onFocusChanged { focusState -> - if (focusState.isFocused) onSearch = true - Log.d(TAG, "onFocusChanged: $focusState") - }, - value = searchText, - onValueChange = onSearchTextChange, - trailingIcon = { - IconButton( - onClick = { - onSearch = false - keyboardController?.hide() - onClearClick() - }, - content = { Icon(Icons.Filled.Close, null) } - ) - }, - maxLines = 1, - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - onConfirm?.invoke() - }) - ) - } - } - }, - navigationIcon = { - if (onBackClick != null) { - IconButton( - onClick = onBackClick, - content = { Icon(Icons.AutoMirrored.Outlined.ArrowBack, null) } - ) - } - }, - actions = { - AnimatedVisibility( - visible = !onSearch - ) { - IconButton( - onClick = { onSearch = true }, - content = { Icon(Icons.Filled.Search, null) } - ) - } - - if (dropdownContent != null) { - dropdownContent() - } - - }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Preview -@Composable -private fun SearchAppBarPreview() { - var searchText by remember { mutableStateOf("") } - SearchAppBar( - title = { Text("Search text") }, - searchText = searchText, - onSearchTextChange = { searchText = it }, - onClearClick = { searchText = "" } - ) -} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SendLogDialog.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SendLogDialog.kt new file mode 100644 index 000000000000..307f9ee81bd8 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SendLogDialog.kt @@ -0,0 +1,152 @@ +package me.weishu.kernelsu.ui.component + +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Save +import androidx.compose.material.icons.rounded.Share +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.weishu.kernelsu.BuildConfig +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.util.getBugreportFile +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +fun SendLogDialog( + showDialog: MutableState, + loadingDialog: LoadingDialogHandle, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val exportBugreportLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("application/gzip") + ) { uri: Uri? -> + if (uri == null) return@rememberLauncherForActivityResult + scope.launch(Dispatchers.IO) { + loadingDialog.show() + context.contentResolver.openOutputStream(uri)?.use { output -> + getBugreportFile(context).inputStream().use { + it.copyTo(output) + } + } + loadingDialog.hide() + Toast.makeText(context, context.getString(R.string.log_saved), Toast.LENGTH_SHORT).show() + } + } + SuperDialog( + show = showDialog, + insideMargin = DpSize(0.dp, 0.dp), + onDismissRequest = { + showDialog.value = false + }, + content = { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp, bottom = 12.dp), + text = stringResource(R.string.send_log), + fontSize = MiuixTheme.textStyles.title4.fontSize, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = colorScheme.onSurface + ) + SuperArrow( + title = stringResource(id = R.string.save_log), + leftAction = { + Icon( + Icons.Rounded.Save, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = colorScheme.onSurface + ) + }, + onClick = { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") + val current = LocalDateTime.now().format(formatter) + exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") + showDialog.value = false + }, + insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) + ) + SuperArrow( + title = stringResource(id = R.string.send_log), + leftAction = { + Icon( + Icons.Rounded.Share, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = colorScheme.onSurface + ) + }, + onClick = { + scope.launch { + showDialog.value = false + val bugreport = loadingDialog.withLoading { + withContext(Dispatchers.IO) { + getBugreportFile(context) + } + } + + val uri: Uri = + FileProvider.getUriForFile( + context, + "${BuildConfig.APPLICATION_ID}.fileprovider", + bugreport + ) + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + setDataAndType(uri, "application/gzip") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + context.startActivity( + Intent.createChooser( + shareIntent, + context.getString(R.string.send_log) + ) + ) + } + }, + insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) + ) + TextButton( + text = stringResource(id = android.R.string.cancel), + onClick = { + showDialog.value = false + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 24.dp) + .padding(horizontal = 24.dp) + ) + } + ) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt deleted file mode 100644 index e537175fd2c4..000000000000 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt +++ /dev/null @@ -1,74 +0,0 @@ -package me.weishu.kernelsu.ui.component - -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.selection.toggleable -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.semantics.Role - -@Composable -fun SwitchItem( - icon: ImageVector? = null, - title: String, - summary: String? = null, - checked: Boolean, - enabled: Boolean = true, - onCheckedChange: (Boolean) -> Unit -) { - val interactionSource = remember { MutableInteractionSource() } - - ListItem( - modifier = Modifier - .toggleable( - value = checked, - interactionSource = interactionSource, - role = Role.Switch, - enabled = enabled, - indication = LocalIndication.current, - onValueChange = onCheckedChange - ), - headlineContent = { - Text(title) - }, - leadingContent = icon?.let { - { Icon(icon, title) } - }, - trailingContent = { - Switch( - checked = checked, - enabled = enabled, - onCheckedChange = onCheckedChange, - interactionSource = interactionSource - ) - }, - supportingContent = { - if (summary != null) { - Text(summary) - } - } - ) -} - -@Composable -fun RadioItem( - title: String, - selected: Boolean, - onClick: () -> Unit, -) { - ListItem( - headlineContent = { - Text(title) - }, - leadingContent = { - RadioButton(selected = selected, onClick = onClick) - } - ) -} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperDropdown.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperDropdown.kt new file mode 100644 index 000000000000..ba10f49a4880 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperDropdown.kt @@ -0,0 +1,199 @@ +package me.weishu.kernelsu.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.basic.BasicComponent +import top.yukonga.miuix.kmp.basic.BasicComponentColors +import top.yukonga.miuix.kmp.basic.BasicComponentDefaults +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.extra.DropDownMode +import top.yukonga.miuix.kmp.extra.DropdownColors +import top.yukonga.miuix.kmp.extra.DropdownDefaults +import top.yukonga.miuix.kmp.extra.DropdownImpl +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.basic.ArrowUpDownIntegrated +import top.yukonga.miuix.kmp.theme.MiuixTheme + +@Composable +fun SuperDropdown( + items: List, + selectedIndex: Int, + title: String, + titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(), + summary: String? = null, + summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(), + leftAction: @Composable (() -> Unit)? = null, + dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(), + mode: DropDownMode = DropDownMode.Normal, + modifier: Modifier = Modifier, + insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin, + maxHeight: Dp? = null, + enabled: Boolean = true, + showValue: Boolean = true, + onClick: (() -> Unit)? = null, + onSelectedIndexChange: ((Int) -> Unit)?, +) { + val currentOnClick by rememberUpdatedState(onClick) + val currentOnSelectedIndexChange by rememberUpdatedState(onSelectedIndexChange) + + val interactionSource = remember { MutableInteractionSource() } + val isDropdownExpanded = remember { mutableStateOf(false) } + val hapticFeedback = LocalHapticFeedback.current + + val itemsNotEmpty = items.isNotEmpty() + val actualEnabled = enabled && itemsNotEmpty + + val onSurfaceVariantActionsColor = MiuixTheme.colorScheme.onSurfaceVariantActions + val disabledOnSecondaryVariantColor = MiuixTheme.colorScheme.disabledOnSecondaryVariant + + val actionColor = remember(actualEnabled, onSurfaceVariantActionsColor, disabledOnSecondaryVariantColor) { + if (actualEnabled) onSurfaceVariantActionsColor + else disabledOnSecondaryVariantColor + } + + var alignLeft by rememberSaveable { mutableStateOf(true) } + + val basicComponentModifier = remember(modifier, actualEnabled) { + modifier + .pointerInput(actualEnabled) { + if (!actualEnabled) return@pointerInput + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + if (event.type != PointerEventType.Move) { + val eventChange = event.changes.first() + if (eventChange.pressed) { + alignLeft = eventChange.position.x < (size.width / 2) + } + } + } + } + } + } + + val rememberPopup: @Composable () -> Unit = + remember( + itemsNotEmpty, isDropdownExpanded, mode, alignLeft, maxHeight, + items, selectedIndex, dropdownColors, hapticFeedback, currentOnSelectedIndexChange + ) { + @Composable { + if (itemsNotEmpty) { + ListPopup( + show = isDropdownExpanded, + alignment = if ((mode == DropDownMode.AlwaysOnRight || !alignLeft)) + PopupPositionProvider.Align.Right + else + PopupPositionProvider.Align.Left, + onDismissRequest = { + isDropdownExpanded.value = false + }, + maxHeight = maxHeight + ) { + ListPopupColumn { + items.forEachIndexed { index, string -> + DropdownImpl( + text = string, + optionSize = items.size, + isSelected = selectedIndex == index, + dropdownColors = dropdownColors, + onSelectedIndexChange = { selectedIdx -> + hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm) + currentOnSelectedIndexChange?.invoke(selectedIdx) + isDropdownExpanded.value = false + }, + index = index + ) + } + } + } + } + } + } + + val rememberedRightActions: @Composable RowScope.() -> Unit = + remember(showValue, itemsNotEmpty, items, selectedIndex, actionColor) { + @Composable { + if (showValue && itemsNotEmpty) { + val rightTextModifier = remember { Modifier.widthIn(max = 130.dp) } + Text( + modifier = rightTextModifier, + text = items[selectedIndex], + fontSize = MiuixTheme.textStyles.body2.fontSize, + color = actionColor, + textAlign = TextAlign.End, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + } + val imageColorFilter = remember(actionColor) { ColorFilter.tint(actionColor) } + val arrowImageModifier = remember { + Modifier + .padding(start = 8.dp) + .size(10.dp, 16.dp) + .align(Alignment.CenterVertically) + } + Image( + modifier = arrowImageModifier, + imageVector = MiuixIcons.Basic.ArrowUpDownIntegrated, + colorFilter = imageColorFilter, + contentDescription = null + ) + } + } + + val rememberedOnClick: () -> Unit = remember(actualEnabled, currentOnClick, isDropdownExpanded, hapticFeedback) { + { + if (actualEnabled) { + currentOnClick?.invoke() + isDropdownExpanded.value = !isDropdownExpanded.value + if (isDropdownExpanded.value) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) + } + } + } + } + + BasicComponent( + modifier = basicComponentModifier, + interactionSource = interactionSource, + insideMargin = insideMargin, + title = title, + titleColor = titleColor, + summary = summary, + summaryColor = summaryColor, + leftAction = { + rememberPopup() + leftAction?.invoke() + }, + rightActions = rememberedRightActions, + onClick = rememberedOnClick, + holdDownState = isDropdownExpanded.value, + enabled = actualEnabled + ) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperEditArrow.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperEditArrow.kt new file mode 100644 index 000000000000..8e7ef6847d4d --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperEditArrow.kt @@ -0,0 +1,132 @@ +package me.weishu.kernelsu.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.component.filter.FilterNumber +import top.yukonga.miuix.kmp.basic.BasicComponentColors +import top.yukonga.miuix.kmp.basic.BasicComponentDefaults +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.basic.TextField +import top.yukonga.miuix.kmp.extra.RightActionColors +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperArrowDefaults +import top.yukonga.miuix.kmp.extra.SuperDialog + +@Composable +fun SuperEditArrow( + title: String, + titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(), + defaultValue: Int = -1, + summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(), + leftAction: @Composable (() -> Unit)? = null, + rightActionColor: RightActionColors = SuperArrowDefaults.rightActionColors(), + modifier: Modifier = Modifier, + insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin, + enabled: Boolean = true, + onValueChange: ((Int) -> Unit)? = null +) { + val showDialog = remember { mutableStateOf(false) } + val dialogTextFieldValue = remember { mutableIntStateOf(defaultValue) } + + SuperArrow( + title = title, + titleColor = titleColor, + summary = dialogTextFieldValue.intValue.toString(), + summaryColor = summaryColor, + leftAction = leftAction, + rightActionColor = rightActionColor, + modifier = modifier, + insideMargin = insideMargin, + onClick = { + showDialog.value = true + }, + holdDownState = showDialog.value, + enabled = enabled + ) + + EditDialog( + title, + showDialog, + dialogTextFieldValue = dialogTextFieldValue.intValue, + ) { + dialogTextFieldValue.intValue = it + onValueChange?.invoke(dialogTextFieldValue.intValue) + } + +} + +@Composable +private fun EditDialog( + title: String, + showDialog: MutableState, + dialogTextFieldValue: Int, + onValueChange: (Int) -> Unit, +) { + val inputTextFieldValue = remember { mutableIntStateOf(dialogTextFieldValue) } + val filter = remember(key1 = inputTextFieldValue.intValue) { FilterNumber(dialogTextFieldValue) } + + SuperDialog( + title = title, + show = showDialog, + onDismissRequest = { + showDialog.value = false + filter.setInputValue(dialogTextFieldValue.toString()) + } + ) { + TextField( + modifier = Modifier.padding(bottom = 16.dp), + value = filter.getInputValue(), + maxLines = 1, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + onValueChange = filter.onValueChange() + ) + Row( + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + text = stringResource(android.R.string.cancel), + onClick = { + showDialog.value = false + filter.setInputValue(dialogTextFieldValue.toString()) + }, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(20.dp)) + TextButton( + text = stringResource(R.string.confirm), + onClick = { + showDialog.value = false + with(filter.getInputValue().text) { + if (isEmpty()) { + onValueChange(0) + filter.setInputValue("0") + } else { + onValueChange(this@with.toInt()) + } + + } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } + } +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperSearchBar.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperSearchBar.kt new file mode 100644 index 000000000000..03196f549ed8 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SuperSearchBar.kt @@ -0,0 +1,377 @@ +package me.weishu.kernelsu.ui.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.InputField +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.basic.Search +import top.yukonga.miuix.kmp.icon.icons.basic.SearchCleanup +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.BackHandler +import top.yukonga.miuix.kmp.utils.overScrollVertical + +// Search Status Class +@Stable +class SearchStatus(val label: String) { + var searchText by mutableStateOf("") + var current by mutableStateOf(Status.COLLAPSED) + + var offsetY by mutableStateOf(0.dp) + var resultStatus by mutableStateOf(ResultStatus.DEFAULT) + + fun isExpand() = current == Status.EXPANDED + fun isCollapsed() = current == Status.COLLAPSED + fun shouldExpand() = current == Status.EXPANDED || current == Status.EXPANDING + fun shouldCollapsed() = current == Status.COLLAPSED || current == Status.COLLAPSING + fun isAnimatingExpand() = current == Status.EXPANDING + + // 动画完成回调 + fun onAnimationComplete() { + current = when (current) { + Status.EXPANDING -> Status.EXPANDED + Status.COLLAPSING -> { + searchText = "" + Status.COLLAPSED + } + + else -> current + } + } + + @Composable + fun TopAppBarAnim( + modifier: Modifier = Modifier, + visible: Boolean = shouldCollapsed(), + content: @Composable() () -> Unit + ) { + val topAppBarAlpha = animateFloatAsState( + if (visible) 1f else 0f, + animationSpec = tween(if (visible) 550 else 0, easing = FastOutSlowInEasing), + ) + Box( + modifier = modifier.alpha(topAppBarAlpha.value), + ) { + content() + } + } + + enum class Status { EXPANDED, EXPANDING, COLLAPSED, COLLAPSING } + enum class ResultStatus { DEFAULT, EMPTY, LOAD, SHOW } +} + +// Search Box Composable +@Composable +fun SearchStatus.SearchBox( + collapseBar: @Composable (SearchStatus, Dp, PaddingValues) -> Unit = { searchStatus, topPadding, innerPadding -> + SearchBarFake(searchStatus.label, topPadding, innerPadding) + }, + searchBarTopPadding: Dp = 12.dp, + contentPadding: PaddingValues = PaddingValues(0.dp), + content: @Composable (MutableState) -> Unit +) { + val searchStatus = this + val density = LocalDensity.current + + animateFloatAsState(if (searchStatus.shouldCollapsed()) 1f else 0f) + + val offsetY = remember { mutableIntStateOf(0) } + val boxHeight = remember { mutableStateOf(0.dp) } + + Box( + modifier = Modifier + .fillMaxWidth() + .zIndex(10f) + .alpha(if (searchStatus.isCollapsed()) 1f else 0f) + .offset(y = contentPadding.calculateTopPadding()) + .onGloballyPositioned { + it.positionInWindow().y.apply { + offsetY.intValue = (this@apply * 0.9).toInt() + with(density) { + searchStatus.offsetY = this@apply.toDp() + boxHeight.value = it.size.height.toDp() + } + } + } + .pointerInput(Unit) { + detectTapGestures { searchStatus.current = SearchStatus.Status.EXPANDING } + } + .background(colorScheme.background) + ) { + collapseBar(searchStatus, searchBarTopPadding, contentPadding) + } + Box { + AnimatedVisibility( + visible = searchStatus.shouldCollapsed(), + enter = fadeIn(tween(300, easing = LinearOutSlowInEasing)) + slideInVertically( + tween( + 300, + easing = LinearOutSlowInEasing + ) + ) { -offsetY.intValue }, + exit = fadeOut(tween(300, easing = LinearOutSlowInEasing)) + slideOutVertically( + tween( + 300, + easing = LinearOutSlowInEasing + ) + ) { -offsetY.intValue } + ) { + content(boxHeight) + } + } +} + +// Search Pager Composable +@Composable +fun SearchStatus.SearchPager( + defaultResult: @Composable () -> Unit, + expandBar: @Composable (SearchStatus, Dp) -> Unit = { searchStatus, padding -> + SearchBar(searchStatus, padding) + }, + searchBarTopPadding: Dp = 12.dp, + result: LazyListScope.() -> Unit +) { + val searchStatus = this + val systemBarsPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding() + val topPadding by animateDpAsState( + if (searchStatus.shouldExpand()) systemBarsPadding + 5.dp else searchStatus.offsetY, + animationSpec = tween(300, easing = LinearOutSlowInEasing) + ) { + searchStatus.onAnimationComplete() + } + val backgroundAlpha by animateFloatAsState( + if (searchStatus.shouldExpand()) 1f else 0f, + animationSpec = tween(200, easing = FastOutSlowInEasing) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .zIndex(5f) + .background(colorScheme.background.copy(alpha = backgroundAlpha)) + .semantics { onClick { false } } + .then( + if (!searchStatus.isCollapsed()) Modifier.pointerInput(Unit) { } else Modifier + ) + ) { + Row( + Modifier + .fillMaxWidth() + .padding(top = topPadding) + .alpha(if (searchStatus.isCollapsed()) 0f else 1f), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedVisibility( + visible = !searchStatus.isCollapsed(), + modifier = Modifier.weight(1f), + enter = fadeIn(), + exit = fadeOut() + ) { + expandBar(searchStatus, searchBarTopPadding) + } + AnimatedVisibility( + visible = searchStatus.isExpand() || searchStatus.isAnimatingExpand(), + enter = expandHorizontally() + slideInHorizontally(initialOffsetX = { it }), + exit = shrinkHorizontally() + slideOutHorizontally(targetOffsetX = { it }) + ) { + BackHandler(enabled = true) { + searchStatus.current = SearchStatus.Status.COLLAPSING + } + Text( + text = stringResource(android.R.string.cancel), + fontWeight = FontWeight.Bold, + color = colorScheme.primary, + modifier = Modifier + .padding(start = 4.dp, end = 16.dp, top = searchBarTopPadding) + .clickable( + interactionSource = null, + enabled = searchStatus.isExpand(), + indication = null + ) { searchStatus.current = SearchStatus.Status.COLLAPSING } + ) + } + } + AnimatedVisibility( + visible = searchStatus.isExpand(), + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + enter = fadeIn(), + exit = fadeOut() + ) { + when (searchStatus.resultStatus) { + SearchStatus.ResultStatus.DEFAULT -> defaultResult() + SearchStatus.ResultStatus.EMPTY -> {} + SearchStatus.ResultStatus.LOAD -> {} + SearchStatus.ResultStatus.SHOW -> LazyColumn( + Modifier + .fillMaxSize() + .padding(top = 12.dp) + .overScrollVertical(), + ) { + result() + } + } + } + } +} + +@Composable +fun SearchBar( + searchStatus: SearchStatus, + searchBarTopPadding: Dp = 12.dp, +) { + val focusRequester = remember { FocusRequester() } + var expanded by rememberSaveable { mutableStateOf(false) } + + InputField( + query = searchStatus.searchText, + onQueryChange = { searchStatus.searchText = it }, + label = "", + leadingIcon = { + Icon( + imageVector = MiuixIcons.Basic.Search, + contentDescription = "back", + modifier = Modifier + .size(44.dp) + .padding(start = 16.dp, end = 8.dp), + tint = colorScheme.onSurfaceContainerHigh, + ) + }, + trailingIcon = { + AnimatedVisibility( + searchStatus.searchText.isNotEmpty(), + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + ) { + Icon( + imageVector = MiuixIcons.Basic.SearchCleanup, + tint = colorScheme.onSurface, + contentDescription = "Clean", + modifier = Modifier + .size(44.dp) + .padding(start = 8.dp, end = 16.dp) + .clickable( + interactionSource = null, + indication = null + ) { + searchStatus.searchText = "" + }, + ) + } + }, + modifier = Modifier + .padding(horizontal = 12.dp) + .padding(top = searchBarTopPadding) + .focusRequester(focusRequester), + onSearch = { it }, + expanded = searchStatus.shouldExpand(), + onExpandedChange = { + searchStatus.current = if (it) SearchStatus.Status.EXPANDED else SearchStatus.Status.COLLAPSED + } + ) + LaunchedEffect(Unit) { + if (!expanded && searchStatus.shouldExpand()) { + focusRequester.requestFocus() + expanded = true + } + } +} + +@Composable +fun SearchBarFake( + label: String, + searchBarTopPadding: Dp = 12.dp, + innerPadding: PaddingValues = PaddingValues(0.dp) +) { + val layoutDirection = LocalLayoutDirection.current + InputField( + query = "", + onQueryChange = { }, + label = label, + leadingIcon = { + Icon( + imageVector = MiuixIcons.Basic.Search, + contentDescription = "Clean", + modifier = Modifier + .size(44.dp) + .padding(start = 16.dp, end = 8.dp), + tint = colorScheme.onSurfaceContainerHigh, + ) + }, + modifier = Modifier + .padding(horizontal = 12.dp) + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ) + .padding(top = searchBarTopPadding, bottom = 6.dp), + onSearch = { it }, + enabled = false, + expanded = false, + onExpandedChange = { } + ) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/UninstallDialog.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/UninstallDialog.kt new file mode 100644 index 000000000000..bee9bc8bcba9 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/UninstallDialog.kt @@ -0,0 +1,115 @@ +package me.weishu.kernelsu.ui.component + +import android.widget.Toast +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.screen.FlashIt +import me.weishu.kernelsu.ui.screen.UninstallType +import me.weishu.kernelsu.ui.screen.UninstallType.NONE +import me.weishu.kernelsu.ui.screen.UninstallType.PERMANENT +import me.weishu.kernelsu.ui.screen.UninstallType.RESTORE_STOCK_IMAGE +import me.weishu.kernelsu.ui.screen.UninstallType.TEMPORARY +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme + +@Composable +fun UninstallDialog( + showDialog: MutableState, + navigator: DestinationsNavigator, +) { + val context = LocalContext.current + val options = listOf( + // TEMPORARY, + PERMANENT, + RESTORE_STOCK_IMAGE + ) + val showTodo = { + Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() + } + val run = { type: UninstallType -> + when (type) { + PERMANENT -> navigator.navigate(FlashScreenDestination(FlashIt.FlashUninstall)) { + popUpTo(FlashScreenDestination) { + inclusive = true + } + launchSingleTop = true + } + + RESTORE_STOCK_IMAGE -> navigator.navigate(FlashScreenDestination(FlashIt.FlashRestore)) { + popUpTo(FlashScreenDestination) { + inclusive = true + } + launchSingleTop = true + } + + TEMPORARY -> showTodo() + NONE -> Unit + } + } + + SuperDialog( + show = showDialog, + insideMargin = DpSize(0.dp, 0.dp), + onDismissRequest = { + showDialog.value = false + }, + content = { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp, bottom = 12.dp), + text = stringResource(R.string.uninstall), + fontSize = MiuixTheme.textStyles.title4.fontSize, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = MiuixTheme.colorScheme.onSurface + ) + options.forEachIndexed { index, type -> + SuperArrow( + onClick = { + showDialog.value = false + run(type) + }, + title = stringResource(type.title), + summary = if (type.message != 0) stringResource(type.message) else null, + leftAction = { + Icon( + imageVector = type.icon, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = MiuixTheme.colorScheme.onSurface + ) + }, + insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) + ) + } + TextButton( + text = stringResource(id = android.R.string.cancel), + onClick = { + showDialog.value = false + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 24.dp) + .padding(horizontal = 24.dp) + ) + } + ) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/BaseFieldFilter.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/BaseFieldFilter.kt new file mode 100644 index 000000000000..e0adc71e86fd --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/BaseFieldFilter.kt @@ -0,0 +1,51 @@ +package me.weishu.kernelsu.ui.component.filter + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue + +open class BaseFieldFilter() { + private var inputValue = mutableStateOf(TextFieldValue()) + + constructor(value: String) : this() { + inputValue.value = TextFieldValue(value, TextRange(value.lastIndex + 1)) + } + + protected open fun onFilter(inputTextFieldValue: TextFieldValue, lastTextFieldValue: TextFieldValue): TextFieldValue { + return TextFieldValue() + } + + protected open fun computePos(): Int { + // TODO + return 0 + } + + protected fun getNewTextRange( + lastTextFiled: TextFieldValue, + inputTextFieldValue: TextFieldValue + ): TextRange? { + return null + } + + protected fun getNewText( + lastTextFiled: TextFieldValue, + inputTextFieldValue: TextFieldValue + ): TextRange? { + + return null + } + + fun setInputValue(value: String) { + inputValue.value = TextFieldValue(value, TextRange(value.lastIndex + 1)) + } + + fun getInputValue(): TextFieldValue { + return inputValue.value + } + + fun onValueChange(): (TextFieldValue) -> Unit { + return { + inputValue.value = onFilter(it, inputValue.value) + } + } +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/FilterNumber.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/FilterNumber.kt new file mode 100644 index 000000000000..2e795e736e40 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/FilterNumber.kt @@ -0,0 +1,82 @@ +package me.weishu.kernelsu.ui.component.filter + +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue + +class FilterNumber( + private val value: Int, + private val minValue: Int = Int.MIN_VALUE, + private val maxValue: Int = Int.MAX_VALUE, +) : BaseFieldFilter(value.toString()) { + + override fun onFilter( + inputTextFieldValue: TextFieldValue, + lastTextFieldValue: TextFieldValue + ): TextFieldValue { + return filterInputNumber(inputTextFieldValue, lastTextFieldValue, minValue, maxValue) + } + + private fun filterInputNumber( + inputTextFieldValue: TextFieldValue, + lastInputTextFieldValue: TextFieldValue, + minValue: Int = Int.MIN_VALUE, + maxValue: Int = Int.MAX_VALUE, + ): TextFieldValue { + val inputString = inputTextFieldValue.text + lastInputTextFieldValue.text + + val newString = StringBuilder() + val supportNegative = minValue < 0 + var isNegative = false + + // 只允许负号在首位,并且只允许一个负号 + if (supportNegative && inputString.isNotEmpty() && inputString.first() == '-') { + isNegative = true + newString.append('-') + } + + for ((i, c) in inputString.withIndex()) { + if (i == 0 && isNegative) continue // 首字符已经处理 + when (c) { + in '0'..'9' -> { + newString.append(c) + // 检查是否超出范围 + val tempText = newString.toString() + // 只在不是单独 '-' 时做判断(因为 '-' toInt 会异常) + if (tempText != "-" && tempText.isNotEmpty()) { + try { + val tempValue = tempText.toInt() + if (tempValue > maxValue || tempValue < minValue) { + newString.deleteCharAt(newString.lastIndex) + } + } catch (e: NumberFormatException) { + // 超出int范围 + newString.deleteCharAt(newString.lastIndex) + } + } + } + // 忽略其他字符(包括点号) + } + } + + val textRange: TextRange + if (inputTextFieldValue.selection.collapsed) { // 表示的是光标范围 + if (inputTextFieldValue.selection.end != inputTextFieldValue.text.length) { // 光标没有指向末尾 + var newPosition = inputTextFieldValue.selection.end + (newString.length - inputString.length) + if (newPosition < 0) { + newPosition = inputTextFieldValue.selection.end + } + textRange = TextRange(newPosition) + } else { // 光标指向了末尾 + textRange = TextRange(newString.length) + } + } else { + textRange = TextRange(newString.length) + } + + return lastInputTextFieldValue.copy( + text = newString.toString(), + selection = textRange + ) + } +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfig.kt index 065ff6d0cf02..e3eb07ac6120 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfig.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfig.kt @@ -1,8 +1,6 @@ package me.weishu.kernelsu.ui.component.profile import androidx.compose.foundation.layout.Column -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -13,7 +11,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.SwitchItem +import me.weishu.kernelsu.ui.component.EditText +import top.yukonga.miuix.kmp.extra.SuperSwitch @Composable fun AppProfileConfig( @@ -25,14 +24,15 @@ fun AppProfileConfig( ) { Column(modifier = modifier) { if (!fixedName) { - OutlinedTextField( - label = { Text(stringResource(R.string.profile_name)) }, - value = profile.name, - onValueChange = { onProfileChange(profile.copy(name = it)) } + EditText( + title = stringResource(R.string.profile_name), + textValue = remember { mutableStateOf(profile.name) }, + onTextValueChange = { onProfileChange(profile.copy(name = it)) }, + enabled = enabled, ) } - SwitchItem( + SuperSwitch( title = stringResource(R.string.profile_umount_modules), summary = stringResource(R.string.profile_umount_modules_summary), checked = if (enabled) { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt index a6a2a45ea1f7..427223dd83f3 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt @@ -1,62 +1,46 @@ package me.weishu.kernelsu.ui.component.profile -import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +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.fillMaxSize +import androidx.compose.foundation.layout.PaddingValues +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.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowDropUp -import androidx.compose.material3.AssistChip -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MenuAnchorType -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.core.text.isDigitsOnly -import com.maxkeppeker.sheets.core.models.base.Header -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.input.InputDialog -import com.maxkeppeler.sheets.input.models.InputHeader -import com.maxkeppeler.sheets.input.models.InputSelection -import com.maxkeppeler.sheets.input.models.InputTextField -import com.maxkeppeler.sheets.input.models.InputTextFieldType -import com.maxkeppeler.sheets.input.models.ValidationResult -import com.maxkeppeler.sheets.list.ListDialog -import com.maxkeppeler.sheets.list.models.ListOption -import com.maxkeppeler.sheets.list.models.ListSelection import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.profile.Capabilities import me.weishu.kernelsu.profile.Groups -import me.weishu.kernelsu.ui.component.rememberCustomDialog +import me.weishu.kernelsu.ui.component.SuperEditArrow import me.weishu.kernelsu.ui.util.isSepolicyValid +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.basic.TextField +import top.yukonga.miuix.kmp.extra.CheckboxLocation +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperCheckbox +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme -@OptIn(ExperimentalMaterial3Api::class) @Composable fun RootProfileConfig( modifier: Modifier = Modifier, @@ -64,94 +48,49 @@ fun RootProfileConfig( profile: Natives.Profile, onProfileChange: (Natives.Profile) -> Unit, ) { - Column(modifier = modifier) { + Column( + modifier = modifier + ) { if (!fixedName) { - OutlinedTextField( - label = { Text(stringResource(R.string.profile_name)) }, + TextField( + label = stringResource(R.string.profile_name), value = profile.name, onValueChange = { onProfileChange(profile.copy(name = it)) } ) } - /* - var expanded by remember { mutableStateOf(false) } - val currentNamespace = when (profile.namespace) { - Natives.Profile.Namespace.INHERITED.ordinal -> stringResource(R.string.profile_namespace_inherited) - Natives.Profile.Namespace.GLOBAL.ordinal -> stringResource(R.string.profile_namespace_global) - Natives.Profile.Namespace.INDIVIDUAL.ordinal -> stringResource(R.string.profile_namespace_individual) - else -> stringResource(R.string.profile_namespace_inherited) - } - ListItem(headlineContent = { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded } - ) { - OutlinedTextField( - modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) - .fillMaxWidth(), - readOnly = true, - label = { Text(stringResource(R.string.profile_namespace)) }, - value = currentNamespace, - onValueChange = {}, - trailingIcon = { - if (expanded) Icon(Icons.Filled.ArrowDropUp, null) - else Icon(Icons.Filled.ArrowDropDown, null) - }, - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.profile_namespace_inherited)) }, - onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INHERITED.ordinal)) - expanded = false - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.profile_namespace_global)) }, - onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.GLOBAL.ordinal)) - expanded = false - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.profile_namespace_individual)) }, - onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INDIVIDUAL.ordinal)) - expanded = false - }, - ) - } - } - }) - */ - - UidPanel(uid = profile.uid, label = "uid", onUidChange = { + SuperEditArrow( + title = "UID", + defaultValue = profile.uid, + ) { onProfileChange( profile.copy( uid = it, rootUseDefault = false ) ) - }) - UidPanel(uid = profile.gid, label = "gid", onUidChange = { + } + + SuperEditArrow( + title = "GID", + defaultValue = profile.gid, + ) { onProfileChange( profile.copy( gid = it, rootUseDefault = false ) ) - }) + + } val selectedGroups = profile.groups.ifEmpty { listOf(0) }.let { e -> e.mapNotNull { g -> Groups.entries.find { it.gid == g } } } + GroupsPanel(selectedGroups) { onProfileChange( profile.copy( @@ -183,15 +122,15 @@ fun RootProfileConfig( ) ) }) - } } -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun GroupsPanel(selected: List, closeSelection: (selection: Set) -> Unit) { - val selectGroupsDialog = rememberCustomDialog { dismiss: () -> Unit -> - val groups = Groups.entries.toTypedArray().sortedWith( + val showDialog = remember { mutableStateOf(false) } + + val groups = remember { + Groups.entries.toTypedArray().sortedWith( compareBy { if (selected.contains(it)) 0 else 1 } .then(compareBy { when (it) { @@ -202,286 +141,257 @@ fun GroupsPanel(selected: List, closeSelection: (selection: Set) } }) .then(compareBy { it.name }) - - ) - val options = groups.map { value -> - ListOption( - titleText = value.display, - subtitleText = value.desc, - selected = selected.contains(value), - ) - } - - val selection = HashSet(selected) - ListDialog( - state = rememberUseCaseState(visible = true, onFinishedRequest = { - closeSelection(selection) - }, onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_groups), - ), - selection = ListSelection.Multiple( - showCheckBoxes = true, - options = options, - maxChoices = 32, // Kernel only supports 32 groups at most - ) { indecies, _ -> - // Handle selection - selection.clear() - indecies.forEach { index -> - val group = groups[index] - selection.add(group) - } - } ) } - OutlinedCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { + val currentSelection = remember { mutableStateOf(selected.toSet()) } - Column( - modifier = Modifier - .fillMaxSize() - .clickable { - selectGroupsDialog.show() - } - .padding(16.dp) - ) { - Text(stringResource(R.string.profile_groups)) - FlowRow { - selected.forEach { group -> - AssistChip( - modifier = Modifier.padding(3.dp), - onClick = { /*TODO*/ }, - label = { Text(group.display) }) + SuperDialog( + show = showDialog, + title = stringResource(R.string.profile_groups), + summary = "${currentSelection.value.size} / 32", + insideMargin = DpSize(0.dp, 24.dp), + onDismissRequest = { showDialog.value = false } + ) { + Column(modifier = Modifier.heightIn(max = 500.dp)) { + LazyColumn(modifier = Modifier.weight(1f, fill = false)) { + items(groups) { group -> + SuperCheckbox( + title = group.display, + summary = group.desc, + insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp), + checkboxLocation = CheckboxLocation.Right, + checked = currentSelection.value.contains(group), + holdDownState = currentSelection.value.contains(group), + onCheckedChange = { isChecked -> + val newSelection = currentSelection.value.toMutableSet() + if (isChecked) { + if (newSelection.size < 32) newSelection.add(group) + } else { + newSelection.remove(group) + } + currentSelection.value = newSelection + } + ) } } + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier.padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + onClick = { + currentSelection.value = selected.toSet() + showDialog.value = false + }, + text = stringResource(android.R.string.cancel), + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(20.dp)) + TextButton( + onClick = { + closeSelection(currentSelection.value) + showDialog.value = false + }, + text = stringResource(R.string.confirm), + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } } + } + val tag = if (selected.isEmpty()) { + "None" + } else { + selected.joinToString(separator = ",", transform = { it.display }) } + SuperArrow( + title = stringResource(R.string.profile_groups), + summary = tag, + onClick = { + showDialog.value = true + }, + ) + } -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun CapsPanel( selected: Collection, closeSelection: (selection: Set) -> Unit ) { - val selectCapabilitiesDialog = rememberCustomDialog { dismiss -> - val caps = Capabilities.entries.toTypedArray().sortedWith( + val showDialog = remember { mutableStateOf(false) } + + val caps = remember { + Capabilities.entries.toTypedArray().sortedWith( compareBy { if (selected.contains(it)) 0 else 1 } .then(compareBy { it.name }) ) - val options = caps.map { value -> - ListOption( - titleText = value.display, - subtitleText = value.desc, - selected = selected.contains(value), - ) - } - - val selection = HashSet(selected) - ListDialog( - state = rememberUseCaseState(visible = true, onFinishedRequest = { - closeSelection(selection) - }, onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_capabilities), - ), - selection = ListSelection.Multiple( - showCheckBoxes = true, - options = options - ) { indecies, _ -> - // Handle selection - selection.clear() - indecies.forEach { index -> - val group = caps[index] - selection.add(group) - } - } - ) } - OutlinedCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - - Column( - modifier = Modifier - .fillMaxSize() - .clickable { - selectCapabilitiesDialog.show() + val currentSelection = remember { mutableStateOf(selected.toSet()) } + + SuperDialog( + show = showDialog, + title = stringResource(R.string.profile_capabilities), + insideMargin = DpSize(0.dp, 24.dp), + onDismissRequest = { showDialog.value = false }, + content = { + Column(modifier = Modifier.heightIn(max = 500.dp)) { + LazyColumn(modifier = Modifier.weight(1f, fill = false)) { + items(caps) { cap -> + SuperCheckbox( + title = cap.display, + summary = cap.desc, + insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp), + checkboxLocation = CheckboxLocation.Right, + checked = currentSelection.value.contains(cap), + holdDownState = currentSelection.value.contains(cap), + onCheckedChange = { isChecked -> + val newSelection = currentSelection.value.toMutableSet() + if (isChecked) { + newSelection.add(cap) + } else { + newSelection.remove(cap) + } + currentSelection.value = newSelection + } + ) + } } - .padding(16.dp) - ) { - Text(stringResource(R.string.profile_capabilities)) - FlowRow { - selected.forEach { group -> - AssistChip( - modifier = Modifier.padding(3.dp), - onClick = { /*TODO*/ }, - label = { Text(group.display) }) + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier.padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + onClick = { + showDialog.value = false + currentSelection.value = selected.toSet() + }, + text = stringResource(android.R.string.cancel), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(20.dp)) + TextButton( + onClick = { + closeSelection(currentSelection.value) + showDialog.value = false + }, + text = stringResource(R.string.confirm), + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) } } } + ) + val tag = if (selected.isEmpty()) { + "None" + } else { + selected.joinToString(separator = ",", transform = { it.display }) } -} - -@Composable -private fun UidPanel(uid: Int, label: String, onUidChange: (Int) -> Unit) { - - ListItem(headlineContent = { - var isError by remember { - mutableStateOf(false) - } - var lastValidUid by remember { - mutableIntStateOf(uid) + SuperArrow( + title = stringResource(R.string.profile_capabilities), + summary = tag, + onClick = { + showDialog.value = true } - val keyboardController = LocalSoftwareKeyboardController.current + ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - label = { Text(label) }, - value = uid.toString(), - isError = isError, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - }), - onValueChange = { - if (it.isEmpty()) { - onUidChange(0) - return@OutlinedTextField - } - val valid = isTextValidUid(it) - - val targetUid = if (valid) it.toInt() else lastValidUid - if (valid) { - lastValidUid = it.toInt() - } - - onUidChange(targetUid) - - isError = !valid - } - ) - }) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SELinuxPanel( profile: Natives.Profile, onSELinuxChange: (domain: String, rules: String) -> Unit ) { - val editSELinuxDialog = rememberCustomDialog { dismiss -> - var domain by remember { mutableStateOf(profile.context) } - var rules by remember { mutableStateOf(profile.rules) } + val showDialog = remember { mutableStateOf(false) } - val inputOptions = listOf( - InputTextField( - text = domain, - header = InputHeader( - title = stringResource(id = R.string.profile_selinux_domain), - ), - type = InputTextFieldType.OUTLINED, - required = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, - imeAction = ImeAction.Next - ), - resultListener = { - domain = it ?: "" - }, - validationListener = { value -> - // value can be a-zA-Z0-9_ - val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$") - if (value?.matches(regex) == true) ValidationResult.Valid - else ValidationResult.Invalid("Domain must be in the format of \"user:role:type:level\"") - } - ), - InputTextField( - text = rules, - header = InputHeader( - title = stringResource(id = R.string.profile_selinux_rules), - ), - type = InputTextFieldType.OUTLINED, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, - ), - singleLine = false, - resultListener = { - rules = it ?: "" - }, - validationListener = { value -> - if (isSepolicyValid(value)) ValidationResult.Valid - else ValidationResult.Invalid("SELinux rules is invalid!") - } - ) - ) + var domain by remember { mutableStateOf(profile.context) } + var rules by remember { mutableStateOf(profile.rules) } - InputDialog( - state = rememberUseCaseState(visible = true, - onFinishedRequest = { - onSELinuxChange(domain, rules) - }, - onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_selinux_context), - ), - selection = InputSelection( - input = inputOptions, - onPositiveClick = { result -> - // Handle selection - }, - ) - ) + val isDomainValid = remember(domain) { + val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$") + domain.matches(regex) } + val isRulesValid = remember(rules) { isSepolicyValid(rules) } - ListItem(headlineContent = { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .clickable { - editSELinuxDialog.show() - }, - enabled = false, - colors = OutlinedTextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.onSurface, - disabledBorderColor = MaterialTheme.colorScheme.outline, - disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant - ), - label = { Text(text = stringResource(R.string.profile_selinux_context)) }, - value = profile.context, - onValueChange = { } - ) - }) -} - -@Preview -@Composable -private fun RootProfileConfigPreview() { - var profile by remember { mutableStateOf(Natives.Profile("")) } - RootProfileConfig(fixedName = true, profile = profile) { - profile = it + SuperDialog( + show = showDialog, + title = stringResource(R.string.profile_selinux_context), + onDismissRequest = { showDialog.value = false } + ) { + Column(modifier = Modifier.heightIn(max = 500.dp)) { + Column(modifier = Modifier.weight(1f, fill = false)) { + TextField( + value = domain, + onValueChange = { domain = it }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + label = stringResource(id = R.string.profile_selinux_domain), + backgroundColor = colorScheme.surfaceContainer, + borderColor = if (isDomainValid) { + colorScheme.primary + } else { + Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, + imeAction = ImeAction.Next + ), + singleLine = true + ) + TextField( + value = rules, + onValueChange = { rules = it }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + label = stringResource(id = R.string.profile_selinux_rules), + backgroundColor = colorScheme.surfaceContainer, + borderColor = if (isRulesValid) { + colorScheme.primary + } else { + Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, + ), + singleLine = false + ) + } + Spacer(Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + onClick = { showDialog.value = false }, + text = stringResource(android.R.string.cancel), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(20.dp)) + TextButton( + onClick = { + onSELinuxChange(domain, rules) + showDialog.value = false + }, + text = stringResource(R.string.confirm), + enabled = isDomainValid && isRulesValid, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } + } } -} -private fun isTextValidUid(text: String): Boolean { - return text.isNotEmpty() && text.isDigitsOnly() && text.toInt() >= 0 && text.toInt() <= Int.MAX_VALUE + SuperArrow( + title = stringResource(R.string.profile_selinux_context), + summary = profile.context, + onClick = { showDialog.value = true } + ) } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfig.kt index b60e8ea46495..4224eb35dfa1 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfig.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfig.kt @@ -1,20 +1,9 @@ package me.weishu.kernelsu.ui.component.profile -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ReadMore -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowDropUp -import androidx.compose.material.icons.filled.Create -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.MenuAnchorType -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text +import androidx.compose.material.icons.rounded.Create import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -23,17 +12,22 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.util.listAppProfileTemplates import me.weishu.kernelsu.ui.util.setSepolicy import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.extra.DropDownMode +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperDropdown +import top.yukonga.miuix.kmp.theme.MiuixTheme /** * @author weishu * @date 2023/10/21. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun TemplateConfig( profile: Natives.Profile, @@ -48,70 +42,54 @@ fun TemplateConfig( val profileTemplates = listAppProfileTemplates() val noTemplates = profileTemplates.isEmpty() - ListItem(headlineContent = { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it }, - ) { - OutlinedTextField( - modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) - .fillMaxWidth(), - readOnly = true, - label = { Text(stringResource(R.string.profile_template)) }, - value = template.ifEmpty { "None" }, - onValueChange = {}, - trailingIcon = { - if (noTemplates) { - IconButton( - onClick = onManageTemplate - ) { - Icon(Icons.Filled.Create, null) - } - } else if (expanded) Icon(Icons.Filled.ArrowDropUp, null) - else Icon(Icons.Filled.ArrowDropDown, null) + if (noTemplates) { + SuperArrow( + title = stringResource(R.string.app_profile_template_create), + leftAction = { + Icon( + Icons.Rounded.Create, + null, + modifier = Modifier.padding(end = 16.dp), + tint = MiuixTheme.colorScheme.onBackground + ) + }, + onClick = onManageTemplate, + ) + } else { + Column { + SuperDropdown( + title = stringResource(R.string.profile_template), + items = profileTemplates, + selectedIndex = profileTemplates.indexOf(template).takeIf { it >= 0 } ?: 0, + onSelectedIndexChange = { index -> + if (index < 0 || index >= profileTemplates.size) return@SuperDropdown + template = profileTemplates[index] + val templateInfo = getTemplateInfoById(template) + if (templateInfo != null && setSepolicy(template, templateInfo.rules.joinToString("\n"))) { + onProfileChange( + profile.copy( + rootTemplate = template, + rootUseDefault = false, + uid = templateInfo.uid, + gid = templateInfo.gid, + groups = templateInfo.groups, + capabilities = templateInfo.capabilities, + context = templateInfo.context, + namespace = templateInfo.namespace, + ) + ) + } }, + onClick = { + expanded = !expanded + }, + mode = DropDownMode.AlwaysOnRight, + maxHeight = 280.dp + ) + SuperArrow( + title = stringResource(R.string.app_profile_template_view), + onClick = { onViewTemplate(template) } ) - if (profileTemplates.isEmpty()) { - return@ExposedDropdownMenuBox - } - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - profileTemplates.forEach { tid -> - val templateInfo = - getTemplateInfoById(tid) ?: return@forEach - DropdownMenuItem( - text = { Text(tid) }, - onClick = { - template = tid - if (setSepolicy(tid, templateInfo.rules.joinToString("\n"))) { - onProfileChange( - profile.copy( - rootTemplate = tid, - rootUseDefault = false, - uid = templateInfo.uid, - gid = templateInfo.gid, - groups = templateInfo.groups, - capabilities = templateInfo.capabilities, - context = templateInfo.context, - namespace = templateInfo.namespace, - ) - ) - } - expanded = false - }, - trailingIcon = { - IconButton(onClick = { - onViewTemplate(tid) - }) { - Icon(Icons.AutoMirrored.Filled.ReadMore, null) - } - } - ) - } - } } - }) + } } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/About.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/About.kt new file mode 100644 index 000000000000..6e6d3d183bae --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/About.kt @@ -0,0 +1,186 @@ +package me.weishu.kernelsu.ui.screen + +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.FixedScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.dropUnlessResumed +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import me.weishu.kernelsu.BuildConfig +import me.weishu.kernelsu.R +import top.yukonga.miuix.kmp.basic.Card +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.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical + +@Composable +@Destination +fun AboutScreen(navigator: DestinationsNavigator) { + val uriHandler = LocalUriHandler.current + val scrollBehavior = MiuixScrollBehavior() + + val htmlString = stringResource( + id = R.string.about_source_code, + "GitHub", + "Telegram" + ) + val result = extractLinks(htmlString) + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.about), + navigationIcon = { + IconButton( + modifier = Modifier.padding(start = 16.dp), + onClick = dropUnlessResumed { navigator.popBackStack() } + ) { + Icon( + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } + }, + scrollBehavior = scrollBehavior + ) + }, + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> + LazyColumn( + modifier = Modifier + .height(getWindowSize().height.dp) + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .padding(horizontal = 12.dp), + contentPadding = innerPadding, + overscrollEffect = null, + ) { + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(vertical = 48.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(80.dp) + .clip(G2RoundedCornerShape(16.dp)) + .background(Color.White) + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = "icon", + contentScale = FixedScale(1f) + ) + } + Text( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.app_name), + fontWeight = FontWeight.Medium, + fontSize = 26.sp + ) + Text( + text = BuildConfig.VERSION_NAME, + fontSize = 14.sp + ) + } + } + item { + Card( + modifier = Modifier.padding(bottom = 12.dp) + ) { + result.forEach { + SuperArrow( + title = it.fullText, + onClick = { + uriHandler.openUri(it.url) + } + ) + } + } + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) + } + } + } +} + +data class LinkInfo( + val fullText: String, + val url: String +) + +fun extractLinks(html: String): List { + val regex = Regex( + """([^<>\n\r]+?)\s*\s*]*\bhref\s*=\s*(['"]?)([^'"\s>]+)\2[^>]*>([^<]+)\s*\s*(.*?)\s*(?= + try { + val before = match.groupValues[1].trim() + val url = match.groupValues[3].trim() + val title = match.groupValues[4].trim() + val after = match.groupValues[5].trim() + + val fullText = "$before $title $after" + Log.d("ggc", "extractLinks: $fullText -> $url") + LinkInfo(fullText, url) + } catch (e: Exception) { + Log.e("ggc", "匹配失败: ${e.message}") + null + } + }.toList() +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt index 9cfc06862d3c..2639bbac9c4b 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt @@ -1,42 +1,27 @@ package me.weishu.kernelsu.ui.screen -import androidx.annotation.StringRes +import android.widget.Toast import androidx.compose.animation.Crossfade -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.Android -import androidx.compose.material.icons.filled.Security -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material.icons.rounded.AccountCircle +import androidx.compose.material.icons.rounded.Security import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -44,20 +29,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination @@ -66,11 +46,12 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.SwitchItem +import me.weishu.kernelsu.ui.component.AppIconImage +import me.weishu.kernelsu.ui.component.DropdownItem +import me.weishu.kernelsu.ui.component.SuperDropdown import me.weishu.kernelsu.ui.component.profile.AppProfileConfig import me.weishu.kernelsu.ui.component.profile.RootProfileConfig import me.weishu.kernelsu.ui.component.profile.TemplateConfig -import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.forceStopApp import me.weishu.kernelsu.ui.util.getSepolicy import me.weishu.kernelsu.ui.util.launchApp @@ -78,23 +59,41 @@ import me.weishu.kernelsu.ui.util.restartApp import me.weishu.kernelsu.ui.util.setSepolicy import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperSwitch +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.ImmersionMore +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/5/16. */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination @Composable +@Destination fun AppProfileScreen( navigator: DestinationsNavigator, appInfo: SuperUserViewModel.AppInfo, ) { val context = LocalContext.current - val snackBarHost = LocalSnackbarHost.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = MiuixScrollBehavior() val scope = rememberCoroutineScope() - val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label) + val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label).format(appInfo.uid) val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label) val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label) @@ -111,59 +110,77 @@ fun AppProfileScreen( topBar = { TopBar( onBack = dropUnlessResumed { navigator.popBackStack() }, - scrollBehavior = scrollBehavior + packageName = packageName, + scrollBehavior = scrollBehavior, ) }, - snackbarHost = { SnackbarHost(hostState = snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - AppProfileInner( + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> + LazyColumn( modifier = Modifier - .padding(paddingValues) - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()), - packageName = appInfo.packageName, - appLabel = appInfo.label, - appIcon = { - AsyncImage( - model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(), - contentDescription = appInfo.label, - modifier = Modifier - .padding(4.dp) - .width(48.dp) - .height(48.dp) - ) - }, - profile = profile, - onViewTemplate = { - getTemplateInfoById(it)?.let { info -> - navigator.navigate(TemplateEditorScreenDestination(info)) - } - }, - onManageTemplate = { - navigator.navigate(AppProfileTemplateScreenDestination()) - }, - onProfileChange = { - scope.launch { - if (it.allowSu) { - // sync with allowlist.c - forbid_system_uid - if (appInfo.uid < 2000 && appInfo.uid != 1000) { - snackBarHost.showSnackbar(suNotAllowed) - return@launch + .height(getWindowSize().height.dp) + .padding(top = 16.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = innerPadding, + overscrollEffect = null + ) { + item { + AppProfileInner( + packageName = appInfo.packageName, + appLabel = appInfo.label, + appIcon = { + AppIconImage( + packageInfo = appInfo.packageInfo, + label = appInfo.label, + modifier = Modifier + .size(60.dp) + ) + }, + profile = profile, + onViewTemplate = { + getTemplateInfoById(it)?.let { info -> + navigator.navigate(TemplateEditorScreenDestination(info)) { + launchSingleTop = true + } } - if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) { - snackBarHost.showSnackbar(failToUpdateSepolicy) - return@launch + }, + onManageTemplate = { + navigator.navigate(AppProfileTemplateScreenDestination()) { + launchSingleTop = true } - } - if (!Natives.setAppProfile(it)) { - snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid)) - } else { - profile = it - } - } - }, - ) + }, + onProfileChange = { + scope.launch { + if (it.allowSu) { + // sync with allowlist.c - forbid_system_uid + if (appInfo.uid < 2000 && appInfo.uid != 1000) { + Toast.makeText(context, suNotAllowed, Toast.LENGTH_SHORT).show() + return@launch + } + if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) { + Toast.makeText(context, failToUpdateSepolicy, Toast.LENGTH_SHORT).show() + return@launch + } + } + if (!Natives.setAppProfile(it)) { + Toast.makeText(context, failToUpdateAppProfile, Toast.LENGTH_SHORT).show() + } else { + profile = it + } + } + }, + ) + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) + } + } } } @@ -180,26 +197,66 @@ private fun AppProfileInner( ) { val isRootGranted = profile.allowSu - Column(modifier = modifier) { - AppMenuBox(packageName) { - ListItem( - headlineContent = { Text(appLabel) }, - supportingContent = { Text(packageName) }, - leadingContent = appIcon, - ) + Column( + modifier = modifier + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + ) { + + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + appIcon() + Column( + modifier = Modifier.padding(start = 16.dp), + ) { + Text( + text = appLabel, + fontSize = 17.5.sp, + color = colorScheme.onSurface, + fontWeight = FontWeight(500) + ) + Text( + text = packageName, + fontSize = 16.sp, + color = colorScheme.onSurfaceVariantSummary + ) + } + } } - SwitchItem( - icon = Icons.Filled.Security, - title = stringResource(id = R.string.superuser), - checked = isRootGranted, - onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, - ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + ) { + SuperSwitch( + leftAction = { + Icon( + imageVector = Icons.Rounded.Security, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = colorScheme.onBackground + ) + }, + title = stringResource(id = R.string.superuser), + checked = isRootGranted, + onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, + ) + } Crossfade(targetState = isRootGranted, label = "") { current -> Column( - modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */) + modifier = Modifier.padding(bottom = 12.dp) ) { + //SmallTitle(text = stringResource(R.string.profile)) if (current) { val initialMode = if (profile.rootUseDefault) { Mode.Default @@ -218,20 +275,31 @@ private fun AppProfileInner( } mode = it } - Crossfade(targetState = mode, label = "") { currentMode -> - if (currentMode == Mode.Template) { - TemplateConfig( - profile = profile, - onViewTemplate = onViewTemplate, - onManageTemplate = onManageTemplate, - onProfileChange = onProfileChange - ) - } else if (mode == Mode.Custom) { - RootProfileConfig( - fixedName = true, - profile = profile, - onProfileChange = onProfileChange - ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + ) { + Crossfade(targetState = mode, label = "") { currentMode -> + if (currentMode == Mode.Default) { + Spacer(Modifier.height(0.dp)) + } + if (currentMode == Mode.Template) { + TemplateConfig( + profile = profile, + onViewTemplate = onViewTemplate, + onManageTemplate = onManageTemplate, + onProfileChange = onProfileChange + ) + } else if (mode == Mode.Custom) { + RootProfileConfig( + fixedName = true, + profile = profile, + onProfileChange = onProfileChange + ) + } } } } else { @@ -239,14 +307,21 @@ private fun AppProfileInner( ProfileBox(mode, false) { onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default))) } - Crossfade(targetState = mode, label = "") { currentMode -> - val modifyEnabled = currentMode == Mode.Custom - AppProfileConfig( - fixedName = true, - profile = profile, - enabled = modifyEnabled, - onProfileChange = onProfileChange - ) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + ) { + Crossfade(targetState = mode, label = "") { currentMode -> + val modifyEnabled = currentMode == Mode.Custom + AppProfileConfig( + fixedName = true, + profile = profile, + enabled = modifyEnabled, + onProfileChange = onProfileChange + ) + } } } } @@ -254,29 +329,76 @@ private fun AppProfileInner( } } -private enum class Mode(@StringRes private val res: Int) { - Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom); - - val text: String - @Composable get() = stringResource(res) +private enum class Mode() { + Default(), + Template(), + Custom(); } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( onBack: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior? = null + packageName: String, + scrollBehavior: ScrollBehavior, ) { TopAppBar( - title = { - Text(stringResource(R.string.profile)) - }, + title = stringResource(R.string.profile), navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + ) { + Icon( + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } + }, + actions = { + val showTopPopup = remember { mutableStateOf(false) } + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { showTopPopup.value = true }, + holdDownState = showTopPopup.value + ) { + Icon( + imageVector = MiuixIcons.Useful.ImmersionMore, + tint = colorScheme.onSurface, + contentDescription = stringResource(id = R.string.settings) + ) + } + ListPopup( + show = showTopPopup, + onDismissRequest = { showTopPopup.value = false }, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + ) { + ListPopupColumn { + val items = listOf( + stringResource(id = R.string.launch_app), + stringResource(id = R.string.force_stop_app), + stringResource(id = R.string.restart_app) + ) + + items.forEachIndexed { index, text -> + DropdownItem( + text = text, + optionSize = items.size, + index = index, + onSelectedIndexChange = { selectedIndex -> + when (selectedIndex) { + 0 -> launchApp(packageName) + 1 -> forceStopApp(packageName) + 2 -> restartApp(packageName) + } + showTopPopup.value = false + } + ) + } + } + } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } @@ -287,107 +409,50 @@ private fun ProfileBox( hasTemplate: Boolean, onModeChange: (Mode) -> Unit, ) { - ListItem( - headlineContent = { Text(stringResource(R.string.profile)) }, - supportingContent = { Text(mode.text) }, - leadingContent = { Icon(Icons.Filled.AccountCircle, null) }, - ) - HorizontalDivider(thickness = Dp.Hairline) - ListItem(headlineContent = { - Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly - ) { - FilterChip( - selected = mode == Mode.Default, - label = { Text(stringResource(R.string.profile_default)) }, - onClick = { onModeChange(Mode.Default) }, - ) - if (hasTemplate) { - FilterChip( - selected = mode == Mode.Template, - label = { Text(stringResource(R.string.profile_template)) }, - onClick = { onModeChange(Mode.Template) }, - ) + val defaultText = stringResource(R.string.profile_default) + val templateText = stringResource(R.string.profile_template) + val customText = stringResource(R.string.profile_custom) + val list = + remember(hasTemplate, defaultText, templateText, customText) { + buildList { + add(defaultText) + if (hasTemplate) { + add(templateText) + } + add(customText) } - FilterChip( - selected = mode == Mode.Custom, - label = { Text(stringResource(R.string.profile_custom)) }, - onClick = { onModeChange(Mode.Custom) }, - ) } - }) -} - -@Composable -private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) { - var expanded by remember { mutableStateOf(false) } - var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) } - val density = LocalDensity.current - - BoxWithConstraints( - Modifier - .fillMaxSize() - .pointerInput(Unit) { - detectTapGestures { - touchPoint = it - expanded = true - } + val modesAndTitles = remember(hasTemplate, defaultText, templateText, customText) { + buildList { + add(Mode.Default to defaultText) + if (hasTemplate) { + add(Mode.Template to templateText) } - ) { - - content() - - val (offsetX, offsetY) = with(density) { - (touchPoint.x.toDp()) to (touchPoint.y.toDp()) + add(Mode.Custom to customText) } - - DropdownMenu( - expanded = expanded, - offset = DpOffset(offsetX, -offsetY), - onDismissRequest = { - expanded = false + } + val selectedIndex = modesAndTitles.indexOfFirst { it.first == mode } + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + ) { + SuperDropdown( + title = stringResource(R.string.profile), + items = list, + leftAction = { + Icon( + Icons.Rounded.AccountCircle, + modifier = Modifier.padding(end = 16.dp), + contentDescription = null, + tint = colorScheme.onBackground + ) }, + selectedIndex = if (selectedIndex == -1) 0 else selectedIndex, ) { - DropdownMenuItem( - text = { Text(stringResource(id = R.string.launch_app)) }, - onClick = { - expanded = false - launchApp(packageName) - }, - ) - DropdownMenuItem( - text = { Text(stringResource(id = R.string.force_stop_app)) }, - onClick = { - expanded = false - forceStopApp(packageName) - }, - ) - DropdownMenuItem( - text = { Text(stringResource(id = R.string.restart_app)) }, - onClick = { - expanded = false - restartApp(packageName) - }, - ) + onModeChange(modesAndTitles[it].first) } } - - } - -@Preview -@Composable -private fun AppProfilePreview() { - var profile by remember { mutableStateOf(Natives.Profile("")) } - AppProfileInner( - packageName = "icu.nullptr.test", - appLabel = "Test", - appIcon = { Icon(Icons.Filled.Android, null) }, - profile = profile, - onProfileChange = { - profile = it - }, - ) -} - diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt deleted file mode 100644 index c9637ed2e979..000000000000 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt +++ /dev/null @@ -1,24 +0,0 @@ -package me.weishu.kernelsu.ui.screen - -import androidx.annotation.StringRes -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.outlined.* -import androidx.compose.ui.graphics.vector.ImageVector -import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination -import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination -import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDestination -import com.ramcosta.composedestinations.spec.DirectionDestinationSpec -import me.weishu.kernelsu.R - -enum class BottomBarDestination( - val direction: DirectionDestinationSpec, - @StringRes val label: Int, - val iconSelected: ImageVector, - val iconNotSelected: ImageVector, - val rootRequired: Boolean, -) { - Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false), - SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true), - Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true) -} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt index c6be680e2ddf..475bcea47eeb 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt @@ -1,22 +1,24 @@ package me.weishu.kernelsu.ui.screen import android.os.Environment +import android.widget.Toast import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -27,9 +29,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph @@ -39,8 +44,17 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.KeyEventBlocker -import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.runModuleAction +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +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.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Save +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.scrollEndHaptic import java.io.File import java.text.SimpleDateFormat import java.util.Date @@ -50,9 +64,9 @@ import java.util.Locale @Destination fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String) { var text by rememberSaveable { mutableStateOf("") } - var tempText : String + var tempText: String val logContent = rememberSaveable { StringBuilder() } - val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current val scope = rememberCoroutineScope() val scrollState = rememberScrollState() var actionResult: Boolean @@ -98,51 +112,76 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String "KernelSU_module_action_log_${date}.log" ) file.writeText(logContent.toString()) - snackBarHost.showSnackbar("Log saved to ${file.absolutePath}") + Toast.makeText(context, "Log saved to ${file.absolutePath}", Toast.LENGTH_SHORT).show() } - } + }, ) }, - snackbarHost = { SnackbarHost(snackBarHost) } + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } Column( modifier = Modifier .fillMaxSize(1f) - .padding(innerPadding) + .scrollEndHaptic() + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateStartPadding(layoutDirection), + ) .verticalScroll(scrollState), ) { LaunchedEffect(text) { scrollState.animateScrollTo(scrollState.maxValue) } + Spacer(Modifier.height(innerPadding.calculateTopPadding())) Text( modifier = Modifier.padding(8.dp), text = text, - fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontSize = 12.sp, fontFamily = FontFamily.Monospace, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, + ) + Spacer( + Modifier.height( + 12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) ) } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) { - TopAppBar( - title = { Text(stringResource(R.string.action)) }, +private fun TopBar( + onBack: () -> Unit = {}, + onSave: () -> Unit = {}, +) { + SmallTopAppBar( + title = stringResource(R.string.action), navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + ) { + Icon( + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } }, actions = { - IconButton(onClick = onSave) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onSave + ) { Icon( - imageVector = Icons.Filled.Save, + imageVector = MiuixIcons.Useful.Save, contentDescription = stringResource(id = R.string.save_log), + tint = colorScheme.onBackground ) } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt index e61b88d32795..88a87b22d7ef 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt @@ -3,31 +3,29 @@ package me.weishu.kernelsu.ui.screen import android.net.Uri import android.os.Environment import android.os.Parcelable +import android.widget.Toast +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material.icons.rounded.Refresh import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -36,18 +34,19 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -56,12 +55,22 @@ import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.KeyEventBlocker import me.weishu.kernelsu.ui.util.FlashResult import me.weishu.kernelsu.ui.util.LkmSelection -import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.flashModule import me.weishu.kernelsu.ui.util.installBoot import me.weishu.kernelsu.ui.util.reboot import me.weishu.kernelsu.ui.util.restoreBoot import me.weishu.kernelsu.ui.util.uninstallPermanently +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.Scaffold +import top.yukonga.miuix.kmp.basic.SmallTopAppBar +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Save +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.scrollEndHaptic import java.io.File import java.text.SimpleDateFormat import java.util.Date @@ -94,20 +103,20 @@ fun flashModulesSequentially( return FlashResult(0, "", true) } -@OptIn(ExperimentalMaterial3Api::class) @Composable @Destination -fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { - +fun FlashScreen( + navigator: DestinationsNavigator, + flashIt: FlashIt +) { var text by rememberSaveable { mutableStateOf("") } var tempText: String val logContent = rememberSaveable { StringBuilder() } var showFloatAction by rememberSaveable { mutableStateOf(false) } - val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current val scope = rememberCoroutineScope() val scrollState = rememberScrollState() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) var flashing by rememberSaveable { mutableStateOf(FlashingStatus.FLASHING) } @@ -144,9 +153,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { topBar = { TopBar( flashing, - onBack = dropUnlessResumed { - navigator.popBackStack() - }, + onBack = dropUnlessResumed { navigator.popBackStack() }, onSave = { scope.launch { val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) @@ -156,16 +163,22 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { "KernelSU_install_log_${date}.log" ) file.writeText(logContent.toString()) - snackBarHost.showSnackbar("Log saved to ${file.absolutePath}") + Toast.makeText(context, "Log saved to ${file.absolutePath}", Toast.LENGTH_SHORT).show() } }, - scrollBehavior = scrollBehavior ) }, floatingActionButton = { if (showFloatAction) { val reboot = stringResource(id = R.string.reboot) - ExtendedFloatingActionButton( + FloatingActionButton( + modifier = Modifier + .padding( + bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 20.dp, + end = 20.dp + ) + .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), onClick = { scope.launch { withContext(Dispatchers.IO) { @@ -173,33 +186,51 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { } } }, - icon = { Icon(Icons.Filled.Refresh, reboot) }, - text = { Text(text = reboot) }, + shadowElevation = 0.dp, + content = { + Icon( + Icons.Rounded.Refresh, + reboot, + Modifier.size(40.dp), + tint = Color.White + ) + }, ) } }, - snackbarHost = { SnackbarHost(hostState = snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } + Column( modifier = Modifier .fillMaxSize(1f) - .padding(innerPadding) - .nestedScroll(scrollBehavior.nestedScrollConnection) + .scrollEndHaptic() + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateStartPadding(layoutDirection), + ) .verticalScroll(scrollState), ) { LaunchedEffect(text) { scrollState.animateScrollTo(scrollState.maxValue) } + Spacer(Modifier.height(innerPadding.calculateTopPadding())) Text( modifier = Modifier.padding(8.dp), text = text, - fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontSize = 12.sp, fontFamily = FontFamily.Monospace, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, + ) + Spacer( + Modifier.height( + 12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) ) } } @@ -241,46 +272,43 @@ fun flashIt( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( status: FlashingStatus, onBack: () -> Unit = {}, onSave: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null ) { - TopAppBar( - title = { - Text( - stringResource( - when (status) { - FlashingStatus.FLASHING -> R.string.flashing - FlashingStatus.SUCCESS -> R.string.flash_success - FlashingStatus.FAILED -> R.string.flash_failed - } - ) - ) - }, + SmallTopAppBar( + title = stringResource( + when (status) { + FlashingStatus.FLASHING -> R.string.flashing + FlashingStatus.SUCCESS -> R.string.flash_success + FlashingStatus.FAILED -> R.string.flash_failed + } + ), navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + ) { + Icon( + MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } }, actions = { - IconButton(onClick = onSave) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onSave + ) { Icon( - imageVector = Icons.Filled.Save, - contentDescription = "Localized description" + imageVector = MiuixIcons.Useful.Save, + contentDescription = stringResource(id = R.string.save_log), + tint = colorScheme.onBackground ) } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior ) } - -@Preview -@Composable -fun InstallPreview() { - InstallScreen(EmptyDestinationsNavigator) -} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt index cb228e96f582..9dba14d5e08b 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt @@ -5,20 +5,45 @@ import android.os.Build import android.os.PowerManager import android.system.Os import androidx.annotation.StringRes -import androidx.compose.animation.* -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.pager.PagerState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Archive -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.outlined.Block -import androidx.compose.material.icons.outlined.CheckCircle -import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.rounded.CheckCircleOutline +import androidx.compose.material.icons.rounded.ErrorOutline +import androidx.compose.material.icons.rounded.Link +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -26,84 +51,158 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.content.pm.PackageInfoCompat -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import me.weishu.kernelsu.* +import me.weishu.kernelsu.KernelVersion +import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.rememberConfirmDialog +import me.weishu.kernelsu.getKernelVersion +import me.weishu.kernelsu.ksuApp +import me.weishu.kernelsu.ui.component.DropdownItem import me.weishu.kernelsu.ui.component.KsuIsValid -import me.weishu.kernelsu.ui.util.* +import me.weishu.kernelsu.ui.component.rememberConfirmDialog +import me.weishu.kernelsu.ui.util.checkNewVersion +import me.weishu.kernelsu.ui.util.getModuleCount +import me.weishu.kernelsu.ui.util.getSELinuxStatus +import me.weishu.kernelsu.ui.util.getSuperuserCount import me.weishu.kernelsu.ui.util.module.LatestVersionInfo +import me.weishu.kernelsu.ui.util.reboot +import me.weishu.kernelsu.ui.util.rootAvailable +import top.yukonga.miuix.kmp.basic.BasicComponent +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.CardDefaults +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.ScrollBehavior +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.icons.useful.Reboot +import top.yukonga.miuix.kmp.icon.icons.useful.Save +import top.yukonga.miuix.kmp.icon.icons.useful.Settings +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.PressFeedbackType +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic -@OptIn(ExperimentalMaterial3Api::class) -@Destination(start = true) @Composable -fun HomeScreen(navigator: DestinationsNavigator) { +fun HomePager( + pagerState: PagerState, + navigator: DestinationsNavigator, + bottomInnerPadding: Dp +) { val kernelVersion = getKernelVersion() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() Scaffold( topBar = { TopBar( - kernelVersion, + kernelVersion = kernelVersion, onSettingsClick = { - navigator.navigate(SettingScreenDestination) + navigator.navigate(SettingScreenDestination) { + popUpTo(SettingScreenDestination) { + inclusive = true + } + launchSingleTop = true + } }, onInstallClick = { - navigator.navigate(InstallScreenDestination) + navigator.navigate(InstallScreenDestination) { + popUpTo(InstallScreenDestination) { + inclusive = true + } + launchSingleTop = true + } }, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - Column( + LazyColumn( modifier = Modifier - .padding(innerPadding) + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .padding(horizontal = 12.dp), + contentPadding = innerPadding, + overscrollEffect = null, ) { - val isManager = Natives.becomeManager(ksuApp.packageName) - val ksuVersion = if (isManager) Natives.version else null - val lkmMode = ksuVersion?.let { - if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null - } + item { + val coroutineScope = rememberCoroutineScope() + val isManager = Natives.becomeManager(ksuApp.packageName) + val ksuVersion = if (isManager) Natives.version else null + val lkmMode = ksuVersion?.let { + if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null + } - StatusCard(kernelVersion, ksuVersion, lkmMode) { - navigator.navigate(InstallScreenDestination) - } - if (isManager && Natives.requireNewKernel()) { - WarningCard( - stringResource(id = R.string.require_kernel_version).format( - ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL + Column( + modifier = Modifier.padding(vertical = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (isManager && Natives.requireNewKernel()) { + WarningCard( + stringResource(id = R.string.require_kernel_version).format( + ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL + ) + ) + } + if (ksuVersion != null && !rootAvailable()) { + WarningCard( + stringResource(id = R.string.grant_root_failed) + ) + } + StatusCard( + kernelVersion, ksuVersion, lkmMode, + onClickInstall = { + navigator.navigate(InstallScreenDestination) { + launchSingleTop = true + } + }, + onClickSuperuser = { + coroutineScope.launch { + pagerState.animateScrollToPage(1) + } + }, + onclickModule = { + coroutineScope.launch { + pagerState.animateScrollToPage(2) + } + } ) - ) - } - if (ksuVersion != null && !rootAvailable()) { - WarningCard( - stringResource(id = R.string.grant_root_failed) - ) - } - val checkUpdate = - LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("check_update", true) - if (checkUpdate) { - UpdateCard() + + val checkUpdate = + LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) + .getBoolean("check_update", true) + if (checkUpdate) { + UpdateCard() + } + InfoCard() + DonateCard() + LearnMoreCard() + } + Spacer(Modifier.height(bottomInnerPadding)) } - InfoCard() - DonateCard() - LearnMoreCard() - Spacer(Modifier) } } } @@ -135,7 +234,7 @@ fun UpdateCard() { val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) }) WarningCard( message = stringResource(id = R.string.new_version_available).format(newVersionCode), - MaterialTheme.colorScheme.outlineVariant + colorScheme.outline ) { if (changelog.isEmpty()) { uriHandler.openUri(newVersionUrl) @@ -152,71 +251,108 @@ fun UpdateCard() { } @Composable -fun RebootDropdownItem(@StringRes id: Int, reason: String = "") { - DropdownMenuItem(text = { - Text(stringResource(id)) - }, onClick = { - reboot(reason) - }) +fun RebootDropdownItem( + @StringRes id: Int, reason: String = "", + showTopPopup: MutableState, + optionSize: Int, + index: Int, +) { + DropdownItem( + text = stringResource(id), + optionSize = optionSize, + onSelectedIndexChange = { + reboot(reason) + showTopPopup.value = false + }, + index = index + ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( kernelVersion: KernelVersion, onInstallClick: () -> Unit, onSettingsClick: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior? = null + scrollBehavior: ScrollBehavior, ) { TopAppBar( - title = { Text(stringResource(R.string.app_name)) }, + title = stringResource(R.string.app_name), + navigationIcon = { + IconButton( + modifier = Modifier.padding(start = 16.dp), + onClick = onSettingsClick + ) { + Icon( + imageVector = MiuixIcons.Useful.Settings, + contentDescription = stringResource(id = R.string.settings), + tint = colorScheme.onBackground + ) + } + }, actions = { if (kernelVersion.isGKI()) { - IconButton(onClick = onInstallClick) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onInstallClick, + ) { Icon( - imageVector = Icons.Filled.Archive, - contentDescription = stringResource(id = R.string.install) + imageVector = MiuixIcons.Useful.Save, + contentDescription = stringResource(id = R.string.install), + tint = colorScheme.onBackground ) } } - - var showDropdown by remember { mutableStateOf(false) } - KsuIsValid() { - IconButton(onClick = { - showDropdown = true - }) { + val showTopPopup = remember { mutableStateOf(false) } + KsuIsValid { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { showTopPopup.value = true }, + holdDownState = showTopPopup.value + ) { Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = stringResource(id = R.string.reboot) + imageVector = MiuixIcons.Useful.Reboot, + contentDescription = stringResource(id = R.string.reboot), + tint = colorScheme.onBackground ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false - }) { - - RebootDropdownItem(id = R.string.reboot) - - val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { - RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") + } + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + showTopPopup.value = false + } + ) { + val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? + + @Suppress("DEPRECATION") + val isRebootingUserspaceSupported = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true + + ListPopupColumn { + val rebootOptions = mutableListOf( + Pair(R.string.reboot, ""), + Pair(R.string.reboot_recovery, "recovery"), + Pair(R.string.reboot_bootloader, "bootloader"), + Pair(R.string.reboot_download, "download"), + Pair(R.string.reboot_edl, "edl") + ) + if (isRebootingUserspaceSupported) { + rebootOptions.add(1, Pair(R.string.reboot_userspace, "userspace")) + } + rebootOptions.forEachIndexed { idx, (id, reason) -> + RebootDropdownItem( + id = id, + reason = reason, + showTopPopup = showTopPopup, + optionSize = rebootOptions.size, + index = idx + ) } - RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery") - RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader") - RebootDropdownItem(id = R.string.reboot_download, reason = "download") - RebootDropdownItem(id = R.string.reboot_edl, reason = "edl") } } } - - IconButton(onClick = onSettingsClick) { - Icon( - imageVector = Icons.Filled.Settings, - contentDescription = stringResource(id = R.string.settings) - ) - } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } @@ -226,91 +362,198 @@ private fun StatusCard( kernelVersion: KernelVersion, ksuVersion: Int?, lkmMode: Boolean?, - onClickInstall: () -> Unit = {} + onClickInstall: () -> Unit = {}, + onClickSuperuser: () -> Unit = {}, + onclickModule: () -> Unit = {}, ) { - ElevatedCard( - colors = CardDefaults.elevatedCardColors(containerColor = run { - if (ksuVersion != null) MaterialTheme.colorScheme.secondaryContainer - else MaterialTheme.colorScheme.errorContainer - }) + Column( + modifier = Modifier ) { - Row(modifier = Modifier - .fillMaxWidth() - .clickable { - if (kernelVersion.isGKI()) { - onClickInstall() + when { + ksuVersion != null -> { + val safeMode = when { + Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]" + else -> "" } - } - .padding(24.dp), verticalAlignment = Alignment.CenterVertically) { - when { - ksuVersion != null -> { - val safeMode = when { - Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]" - else -> "" - } - - val workingMode = when (lkmMode) { - null -> "" - true -> " " - else -> " " - } - val workingText = - "${stringResource(id = R.string.home_working)}$workingMode$safeMode" + val workingMode = when (lkmMode) { + null -> "" + true -> " " + else -> " " + } - Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working)) - Column(Modifier.padding(start = 20.dp)) { - Text( - text = workingText, - style = MaterialTheme.typography.titleMedium - ) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_working_version, ksuVersion), - style = MaterialTheme.typography.bodyMedium - ) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource( - R.string.home_superuser_count, getSuperuserCount() - ), style = MaterialTheme.typography.bodyMedium - ) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_module_count, getModuleCount()), - style = MaterialTheme.typography.bodyMedium - ) + val workingText = "${stringResource(id = R.string.home_working)}$workingMode$safeMode" + + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Card( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + colors = CardDefaults.defaultColors( + color = if (isSystemInDarkTheme()) Color(0xFF1A3825) else Color(0xFFDFFAE4) + ), + onClick = { + if (kernelVersion.isGKI()) onClickInstall() + }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Tilt + ) { + Box( + modifier = Modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .fillMaxSize() + .offset(38.dp, 45.dp), + contentAlignment = Alignment.BottomEnd + ) { + Icon( + modifier = Modifier.size(170.dp), + imageVector = Icons.Rounded.CheckCircleOutline, + tint = Color(0xFF36D167), + contentDescription = null + ) + } + Column( + modifier = Modifier + .fillMaxSize() + .padding(all = 16.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = workingText, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(2.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.home_working_version, ksuVersion), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ) + } + } + } + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + insideMargin = PaddingValues(16.dp), + onClick = { onClickSuperuser() }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Tilt + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.superuser), + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + color = colorScheme.onSurfaceVariantSummary, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = getSuperuserCount().toString(), + fontSize = 26.sp, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface, + ) + } + } + Spacer(Modifier.height(12.dp)) + Card( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + insideMargin = PaddingValues(16.dp), + onClick = { onclickModule() }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Tilt + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.module), + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + color = colorScheme.onSurfaceVariantSummary, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = getModuleCount().toString(), + fontSize = 26.sp, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface, + ) + } + } } } + } - kernelVersion.isGKI() -> { - Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed)) - Column(Modifier.padding(start = 20.dp)) { - Text( - text = stringResource(R.string.home_not_installed), - style = MaterialTheme.typography.titleMedium - ) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_click_to_install), - style = MaterialTheme.typography.bodyMedium - ) - } + kernelVersion.isGKI() -> { + Card( + onClick = { + if (kernelVersion.isGKI()) onClickInstall() + }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Sink + ) { + BasicComponent( + title = stringResource(R.string.home_not_installed), + summary = stringResource(R.string.home_click_to_install), + leftAction = { + Icon( + Icons.Rounded.ErrorOutline, + stringResource(R.string.home_not_installed), + modifier = Modifier + .padding(end = 16.dp), + tint = colorScheme.onBackground, + ) + } + ) } + } - else -> { - Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported)) - Column(Modifier.padding(start = 20.dp)) { - Text( - text = stringResource(R.string.home_unsupported), - style = MaterialTheme.typography.titleMedium - ) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_unsupported_reason), - style = MaterialTheme.typography.bodyMedium - ) - } + else -> { + Card( + onClick = { + if (kernelVersion.isGKI()) onClickInstall() + }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Sink + ) { + BasicComponent( + title = stringResource(R.string.home_unsupported), + summary = stringResource(R.string.home_unsupported_reason), + leftAction = { + Icon( + Icons.Rounded.ErrorOutline, + stringResource(R.string.home_unsupported), + modifier = Modifier + .padding(end = 16.dp), + tint = colorScheme.onBackground, + ) + } + ) } } } @@ -319,21 +562,29 @@ private fun StatusCard( @Composable fun WarningCard( - message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null + message: String, + color: Color = if (isSystemInDarkTheme()) Color(0XFF310808) else Color(0xFFF8E2E2), + onClick: (() -> Unit)? = null ) { - ElevatedCard( - colors = CardDefaults.elevatedCardColors( - containerColor = color - ) + Card( + onClick = { + onClick?.invoke() + }, + colors = CardDefaults.defaultColors( + color = color + ), + showIndication = onClick != null, + pressFeedbackType = PressFeedbackType.Tilt ) { Row( modifier = Modifier .fillMaxWidth() - .then(onClick?.let { Modifier.clickable { it() } } ?: Modifier) - .padding(24.dp) + .padding(16.dp) ) { Text( - text = message, style = MaterialTheme.typography.bodyMedium + text = message, + color = Color(0xFFF72727), + fontSize = 14.sp ) } } @@ -344,26 +595,25 @@ fun LearnMoreCard() { val uriHandler = LocalUriHandler.current val url = stringResource(R.string.home_learn_kernelsu_url) - ElevatedCard { - - Row(modifier = Modifier - .fillMaxWidth() - .clickable { - uriHandler.openUri(url) - } - .padding(24.dp), verticalAlignment = Alignment.CenterVertically) { - Column { - Text( - text = stringResource(R.string.home_learn_kernelsu), - style = MaterialTheme.typography.titleSmall - ) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_click_to_learn_kernelsu), - style = MaterialTheme.typography.bodyMedium + Card( + modifier = Modifier + .fillMaxWidth(), + ) { + BasicComponent( + title = stringResource(R.string.home_learn_kernelsu), + summary = stringResource(R.string.home_click_to_learn_kernelsu), + rightActions = { + Icon( + modifier = Modifier.size(28.dp), + imageVector = Icons.Rounded.Link, + tint = colorScheme.onSurface, + contentDescription = null ) + }, + onClick = { + uriHandler.openUri(url) } - } + ) } } @@ -371,63 +621,76 @@ fun LearnMoreCard() { fun DonateCard() { val uriHandler = LocalUriHandler.current - ElevatedCard { - - Row(modifier = Modifier - .fillMaxWidth() - .clickable { - uriHandler.openUri("https://patreon.com/weishu") - } - .padding(24.dp), verticalAlignment = Alignment.CenterVertically) { - Column { - Text( - text = stringResource(R.string.home_support_title), - style = MaterialTheme.typography.titleSmall - ) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_support_content), - style = MaterialTheme.typography.bodyMedium + Card( + modifier = Modifier + .fillMaxWidth(), + ) { + BasicComponent( + title = stringResource(R.string.home_support_title), + summary = stringResource(R.string.home_support_content), + rightActions = { + Icon( + modifier = Modifier.size(28.dp), + imageVector = Icons.Rounded.Link, + tint = colorScheme.onSurface, + contentDescription = null ) - } - } + }, + onClick = { + uriHandler.openUri("https://patreon.com/weishu") + }, + insideMargin = PaddingValues(18.dp) + ) } } @Composable private fun InfoCard() { - val context = LocalContext.current - - ElevatedCard { + @Composable + fun InfoText( + title: String, + content: String, + bottomPadding: Dp = 24.dp + ) { + Text( + text = title, + fontSize = MiuixTheme.textStyles.headline1.fontSize, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + Text( + text = content, + fontSize = MiuixTheme.textStyles.body2.fontSize, + color = colorScheme.onSurfaceVariantSummary, + modifier = Modifier.padding(top = 2.dp, bottom = bottomPadding) + ) + } + Card { + val context = LocalContext.current + val uname = Os.uname() + val managerVersion = getManagerVersion(context) Column( modifier = Modifier .fillMaxWidth() - .padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp) + .padding(16.dp) ) { - val contents = StringBuilder() - val uname = Os.uname() - - @Composable - fun InfoCardItem(label: String, content: String) { - contents.appendLine(label).appendLine(content).appendLine() - Text(text = label, style = MaterialTheme.typography.bodyLarge) - Text(text = content, style = MaterialTheme.typography.bodyMedium) - } - - InfoCardItem(stringResource(R.string.home_kernel), uname.release) - - Spacer(Modifier.height(16.dp)) - val managerVersion = getManagerVersion(context) - InfoCardItem( - stringResource(R.string.home_manager_version), - "${managerVersion.first} (${managerVersion.second})" + InfoText( + title = stringResource(R.string.home_kernel), + content = uname.release + ) + InfoText( + title = stringResource(R.string.home_manager_version), + content = "${managerVersion.first} (${managerVersion.second})" + ) + InfoText( + title = stringResource(R.string.home_fingerprint), + content = Build.FINGERPRINT + ) + InfoText( + title = stringResource(R.string.home_selinux_status), + content = getSELinuxStatus(), + bottomPadding = 0.dp ) - - Spacer(Modifier.height(16.dp)) - InfoCardItem(stringResource(R.string.home_fingerprint), Build.FINGERPRINT) - - Spacer(Modifier.height(16.dp)) - InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus()) } } } @@ -437,26 +700,3 @@ fun getManagerVersion(context: Context): Pair { val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo) return Pair(packageInfo.versionName!!, versionCode) } - -@Preview -@Composable -private fun StatusCardPreview() { - Column { - StatusCard(KernelVersion(5, 10, 101), 1, null) - StatusCard(KernelVersion(5, 10, 101), 20000, true) - StatusCard(KernelVersion(5, 10, 101), null, true) - StatusCard(KernelVersion(4, 10, 101), null, false) - } -} - -@Preview -@Composable -private fun WarningCardPreview() { - Column { - WarningCard(message = "Warning message") - WarningCard( - message = "Warning message ", - MaterialTheme.colorScheme.outlineVariant, - onClick = {}) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt index fb12df6b4a09..79a78f33dd6e 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt @@ -10,72 +10,73 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.selection.toggleable -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.FileUpload -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed -import com.maxkeppeker.sheets.core.models.base.Header -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.list.ListDialog -import com.maxkeppeler.sheets.list.models.ListOption -import com.maxkeppeler.sheets.list.models.ListSelection import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.DialogHandle +import me.weishu.kernelsu.ui.component.ChooseKmiDialog import me.weishu.kernelsu.ui.component.rememberConfirmDialog -import me.weishu.kernelsu.ui.component.rememberCustomDialog import me.weishu.kernelsu.ui.util.LkmSelection import me.weishu.kernelsu.ui.util.getCurrentKmi -import me.weishu.kernelsu.ui.util.getSupportedKmis import me.weishu.kernelsu.ui.util.isAbDevice import me.weishu.kernelsu.ui.util.isInitBoot import me.weishu.kernelsu.ui.util.rootAvailable +import top.yukonga.miuix.kmp.basic.Button +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.Card +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.ScrollBehavior +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperCheckbox +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Move +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2024/3/12. */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination @Composable +@Destination fun InstallScreen(navigator: DestinationsNavigator) { var installMethod by remember { mutableStateOf(null) @@ -92,13 +93,16 @@ fun InstallScreen(navigator: DestinationsNavigator) { lkm = lkmSelection, ota = method is InstallMethod.DirectInstallToInactiveSlot ) - navigator.navigate(FlashScreenDestination(flashIt)) + navigator.navigate(FlashScreenDestination(flashIt)) { + launchSingleTop = true + } } } val currentKmi by produceState(initialValue = "") { value = getCurrentKmi() } - val selectKmiDialog = rememberSelectKmiDialog { kmi -> + val showChooseKmiDialog = rememberSaveable { mutableStateOf(false) } + val chooseKmiDialog = ChooseKmiDialog(showChooseKmiDialog) { kmi -> kmi?.let { lkmSelection = LkmSelection.KmiString(it) onInstall() @@ -108,7 +112,8 @@ fun InstallScreen(navigator: DestinationsNavigator) { val onClickNext = { if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) { // no lkm file selected and cannot get current kmi - selectKmiDialog.show() + showChooseKmiDialog.value = true + chooseKmiDialog } else { onInstall() } @@ -129,51 +134,72 @@ fun InstallScreen(navigator: DestinationsNavigator) { }) } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() Scaffold( topBar = { TopBar( onBack = dropUnlessResumed { navigator.popBackStack() }, onLkmUpload = onLkmUpload, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - Column( + LazyColumn( modifier = Modifier - .padding(innerPadding) + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) + .padding(top = 12.dp) + .padding(horizontal = 16.dp), + contentPadding = innerPadding, + overscrollEffect = null, ) { - SelectInstallMethod { method -> - installMethod = method - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - (lkmSelection as? LkmSelection.LkmUri)?.let { - Text( - stringResource( - id = R.string.selected_lkm, - it.uri.lastPathSegment ?: "(file)" + item { + Card( + modifier = Modifier + .fillMaxWidth(), + ) { + SelectInstallMethod { method -> + installMethod = method + } + } + Column( + modifier = Modifier + .fillMaxWidth() + ) { + (lkmSelection as? LkmSelection.LkmUri)?.let { + Text( + stringResource( + id = R.string.selected_lkm, + it.uri.lastPathSegment ?: "(file)" + ) ) - ) + } + Button( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), + enabled = installMethod != null, + colors = ButtonDefaults.buttonColorsPrimary(), + onClick = { onClickNext() } + ) { + Text( + stringResource(id = R.string.install_next), + color = colorScheme.onPrimary, + fontSize = MiuixTheme.textStyles.body1.fontSize + ) + } } - Button(modifier = Modifier.fillMaxWidth(), - enabled = installMethod != null, - onClick = { - onClickNext() - }) { - Text( - stringResource(id = R.string.install_next), - fontSize = MaterialTheme.typography.bodyMedium.fontSize + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) - } + ) } } } @@ -182,7 +208,7 @@ fun InstallScreen(navigator: DestinationsNavigator) { sealed class InstallMethod { data class SelectFile( val uri: Uri? = null, - @StringRes override val label: Int = R.string.select_file, + @get:StringRes override val label: Int = R.string.select_file, override val summary: String? ) : InstallMethod() @@ -207,8 +233,7 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) { val selectFileTip = stringResource( id = R.string.select_file_tip, if (isInitBoot()) "init_boot" else "boot" ) - val radioOptions = - mutableListOf(InstallMethod.SelectFile(summary = selectFileTip)) + val radioOptions = mutableListOf(InstallMethod.SelectFile(summary = selectFileTip)) if (rootAvailable) { radioOptions.add(InstallMethod.DirectInstall) @@ -230,10 +255,13 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) { } } - val confirmDialog = rememberConfirmDialog(onConfirm = { - selectedOption = InstallMethod.DirectInstallToInactiveSlot - onSelected(InstallMethod.DirectInstallToInactiveSlot) - }, onDismiss = null) + val confirmDialog = rememberConfirmDialog( + onConfirm = { + selectedOption = InstallMethod.DirectInstallToInactiveSlot + onSelected(InstallMethod.DirectInstallToInactiveSlot) + }, + onDismiss = null + ) val dialogTitle = stringResource(id = android.R.string.dialog_alert_title) val dialogContent = stringResource(id = R.string.install_inactive_slot_warning) @@ -274,89 +302,51 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) { interactionSource = interactionSource ) ) { - RadioButton( - selected = option.javaClass == selectedOption?.javaClass, - onClick = { + SuperCheckbox( + title = stringResource(id = option.label), + summary = option.summary, + checked = option.javaClass == selectedOption?.javaClass, + onCheckedChange = { onClick(option) }, - interactionSource = interactionSource ) - Column( - modifier = Modifier.padding(vertical = 12.dp) - ) { - Text( - text = stringResource(id = option.label), - fontSize = MaterialTheme.typography.titleMedium.fontSize, - fontFamily = MaterialTheme.typography.titleMedium.fontFamily, - fontStyle = MaterialTheme.typography.titleMedium.fontStyle - ) - option.summary?.let { - Text( - text = it, - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - fontStyle = MaterialTheme.typography.bodySmall.fontStyle - ) - } - } } } } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle { - return rememberCustomDialog { dismiss -> - val supportedKmi by produceState(initialValue = emptyList()) { - value = getSupportedKmis() - } - val options = supportedKmi.map { value -> - ListOption( - titleText = value - ) - } - - var selection by remember { mutableStateOf(null) } - ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = { - onSelected(selection) - }, onCloseRequest = { - dismiss() - }), header = Header.Default( - title = stringResource(R.string.select_kmi), - ), selection = ListSelection.Single( - showRadioButtons = true, - options = options, - ) { _, option -> - selection = option.titleText - }) - } -} - -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( onBack: () -> Unit = {}, onLkmUpload: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null + scrollBehavior: ScrollBehavior, ) { TopAppBar( - title = { Text(stringResource(R.string.install)) }, navigationIcon = { + title = stringResource(R.string.install), + navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } - }, actions = { - IconButton(onClick = onLkmUpload) { - Icon(Icons.Filled.FileUpload, contentDescription = null) + ) { + Icon( + MiuixIcons.Useful.Back, + tint = colorScheme.onSurface, + contentDescription = null, + ) + } + }, + actions = { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onLkmUpload + ) { + Icon( + MiuixIcons.Useful.Move, + tint = colorScheme.onSurface, + contentDescription = null + ) } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } - -@Composable -@Preview -fun SelectInstallPreview() { - InstallScreen(EmptyDestinationsNavigator) -} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index e2c4b9b8e2b9..e4355d143ed5 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt @@ -4,12 +4,16 @@ import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import android.net.Uri -import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.border +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -18,105 +22,111 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Wysiwyg -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.outlined.PlayArrow -import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Code +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.edit +import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.component.ConfirmResult -import me.weishu.kernelsu.ui.component.SearchAppBar import me.weishu.kernelsu.ui.component.rememberConfirmDialog import me.weishu.kernelsu.ui.component.rememberLoadingDialog import me.weishu.kernelsu.ui.util.DownloadListener -import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.download +import me.weishu.kernelsu.ui.util.getFileName import me.weishu.kernelsu.ui.util.hasMagisk -import me.weishu.kernelsu.ui.util.reboot import me.weishu.kernelsu.ui.util.toggleModule import me.weishu.kernelsu.ui.util.uninstallModule -import me.weishu.kernelsu.ui.util.getFileName import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel import me.weishu.kernelsu.ui.webui.WebUIActivity +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.FloatingActionButton +import top.yukonga.miuix.kmp.basic.HorizontalDivider +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.PullToRefresh +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.basic.rememberPullToRefreshState +import top.yukonga.miuix.kmp.extra.DropdownImpl +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.ImmersionMore +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic -@OptIn(ExperimentalMaterial3Api::class) -@Destination @Composable -fun ModuleScreen(navigator: DestinationsNavigator) { +fun ModulePager( + navigator: DestinationsNavigator, + bottomInnerPadding: Dp +) { val viewModel = viewModel() val context = LocalContext.current - val snackBarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) @@ -133,71 +143,113 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val hideInstallButton = isSafeMode || hasMagisk - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() val webUILauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { viewModel.fetchModuleList() } + val listState = rememberLazyListState() + var fabVisible by remember { mutableStateOf(true) } + var scrollDistance by remember { mutableFloatStateOf(0f) } + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val isScrolledToEnd = + (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1 + && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.size + ?: 0) < listState.layoutInfo.viewportEndOffset) + val delta = available.y + if (!isScrolledToEnd) { + scrollDistance += delta + if (scrollDistance < -50f) { + if (fabVisible) fabVisible = false + scrollDistance = 0f + } else if (scrollDistance > 50f) { + if (!fabVisible) fabVisible = true + scrollDistance = 0f + } + } + return Offset.Zero + } + } + } + val offsetHeight by animateDpAsState( + targetValue = if (fabVisible) 0.dp else 180.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(), + animationSpec = tween(durationMillis = 350) + ) + Scaffold( topBar = { - SearchAppBar( - title = { Text(stringResource(R.string.module)) }, - searchText = viewModel.search, - onSearchTextChange = { viewModel.search = it }, - onClearClick = { viewModel.search = "" }, - dropdownContent = { - var showDropdown by remember { mutableStateOf(false) } - + TopAppBar( + title = stringResource(R.string.module), + actions = { + val showTopPopup = remember { mutableStateOf(false) } + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + showTopPopup.value = false + } + ) { + ListPopupColumn { + DropdownImpl( + text = stringResource(R.string.module_sort_action_first), + optionSize = 2, + isSelected = viewModel.sortActionFirst, + onSelectedIndexChange = { + viewModel.sortActionFirst = + !viewModel.sortActionFirst + prefs.edit { + putBoolean( + "module_sort_action_first", + viewModel.sortActionFirst + ) + } + scope.launch { + viewModel.fetchModuleList() + } + showTopPopup.value = false + }, + index = 0 + ) + DropdownImpl( + text = stringResource(R.string.module_sort_enabled_first), + optionSize = 2, + isSelected = viewModel.sortEnabledFirst, + onSelectedIndexChange = { + viewModel.sortEnabledFirst = + !viewModel.sortEnabledFirst + prefs.edit { + putBoolean( + "module_sort_enabled_first", + viewModel.sortEnabledFirst + ) + } + scope.launch { + viewModel.fetchModuleList() + } + showTopPopup.value = false + }, + index = 1 + ) + } + } IconButton( - onClick = { showDropdown = true }, + modifier = Modifier.padding(end = 16.dp), + onClick = { showTopPopup.value = true }, + holdDownState = showTopPopup.value ) { Icon( - imageVector = Icons.Filled.MoreVert, + imageVector = MiuixIcons.Useful.ImmersionMore, + tint = colorScheme.onSurface, contentDescription = stringResource(id = R.string.settings) ) - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false - }) { - DropdownMenuItem(text = { - Text(stringResource(R.string.module_sort_action_first)) - }, trailingIcon = { - Checkbox(viewModel.sortActionFirst, null) - }, onClick = { - viewModel.sortActionFirst = - !viewModel.sortActionFirst - prefs.edit() - .putBoolean( - "module_sort_action_first", - viewModel.sortActionFirst - ) - .apply() - scope.launch { - viewModel.fetchModuleList() - } - }) - DropdownMenuItem(text = { - Text(stringResource(R.string.module_sort_enabled_first)) - }, trailingIcon = { - Checkbox(viewModel.sortEnabledFirst, null) - }, onClick = { - viewModel.sortEnabledFirst = - !viewModel.sortEnabledFirst - prefs.edit() - .putBoolean( - "module_sort_enabled_first", - viewModel.sortEnabledFirst - ) - .apply() - scope.launch { - viewModel.fetchModuleList() - } - }) - } } }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollBehavior ) }, floatingActionButton = { @@ -206,7 +258,9 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val confirmTitle = stringResource(R.string.module) var zipUris by remember { mutableStateOf>(emptyList()) } val confirmDialog = rememberConfirmDialog(onConfirm = { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(zipUris))) + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(zipUris))) { + launchSingleTop = true + } viewModel.markNeedRefresh() }) val selectZipLauncher = rememberLauncherForActivityResult( @@ -221,28 +275,34 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val uris = mutableListOf() if (clipData != null) { for (i in 0 until clipData.itemCount) { - clipData.getItemAt(i)?.uri?.let { uris.add(it) } + clipData.getItemAt(i)?.uri?.let { it -> uris.add(it) } } } else { - data.data?.let { uris.add(it) } + data.data?.let { it -> uris.add(it) } } if (uris.size == 1) { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(listOf(uris.first())))) - } else if (uris.size > 1) { + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(listOf(uris.first())))) { + launchSingleTop = true + } + } else if (uris.size > 1) { // multiple files selected - val moduleNames = uris.mapIndexed { index, uri -> "\n${index + 1}. ${uri.getFileName(context)}" }.joinToString("") + val moduleNames = + uris.mapIndexed { index, uri -> "\n${index + 1}. ${uri.getFileName(context)}" }.joinToString("") val confirmContent = context.getString(R.string.module_install_prompt_with_name, moduleNames) zipUris = uris confirmDialog.showConfirm( title = confirmTitle, - content = confirmContent, - markdown = true + content = confirmContent ) } } - - ExtendedFloatingActionButton( + FloatingActionButton( + modifier = Modifier + .offset(y = offsetHeight) + .padding(bottom = bottomInnerPadding + 20.dp, end = 20.dp) + .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), + shadowElevation = 0.dp, onClick = { // Select the zip files to install val intent = Intent(Intent.ACTION_GET_CONTENT).apply { @@ -251,21 +311,26 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } selectZipLauncher.launch(intent) }, - icon = { Icon(Icons.Filled.Add, moduleInstall) }, - text = { Text(text = moduleInstall) }, + content = { + Icon( + Icons.Rounded.Add, + moduleInstall, + modifier = Modifier.size(40.dp), + tint = Color.White + ) + }, ) } }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - snackbarHost = { SnackbarHost(hostState = snackBarHost) } + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - when { hasMagisk -> { Box( modifier = Modifier .fillMaxSize() - .padding(24.dp), + .padding(12.dp), contentAlignment = Alignment.Center ) { Text( @@ -279,46 +344,53 @@ fun ModuleScreen(navigator: DestinationsNavigator) { ModuleList( navigator, viewModel = viewModel, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - boxModifier = Modifier.padding(innerPadding), + modifier = Modifier + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(nestedScrollConnection) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .padding(horizontal = 12.dp), onInstallModule = { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(listOf(it)))) + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(listOf(it)))) { + launchSingleTop = true + } }, onClickModule = { id, name, hasWebUi -> if (hasWebUi) { webUILauncher.launch( Intent(context, WebUIActivity::class.java) - .setData(Uri.parse("kernelsu://webui/$id")) + .setData("kernelsu://webui/$id".toUri()) .putExtra("id", id) .putExtra("name", name) ) } }, context = context, - snackBarHost = snackBarHost + innerPadding = innerPadding, + bottomInnerPadding = bottomInnerPadding ) } } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ModuleList( navigator: DestinationsNavigator, viewModel: ModuleViewModel, modifier: Modifier = Modifier, - boxModifier: Modifier = Modifier, onInstallModule: (Uri) -> Unit, onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit, context: Context, - snackBarHost: SnackbarHostState + innerPadding: PaddingValues, + bottomInnerPadding: Dp ) { val failedEnable = stringResource(R.string.module_failed_to_enable) val failedDisable = stringResource(R.string.module_failed_to_disable) val failedUninstall = stringResource(R.string.module_uninstall_failed) val successUninstall = stringResource(R.string.module_uninstall_success) - val reboot = stringResource(R.string.reboot) + stringResource(R.string.reboot) val rebootToApply = stringResource(R.string.reboot_to_apply) val moduleStr = stringResource(R.string.module) val uninstall = stringResource(R.string.uninstall) @@ -423,38 +495,43 @@ private fun ModuleList( } else { failedUninstall.format(module.name) } - val actionLabel = if (success) { - reboot - } else { - null - } - val result = snackBarHost.showSnackbar( - message = message, - actionLabel = actionLabel, - duration = SnackbarDuration.Long - ) - if (result == SnackbarResult.ActionPerformed) { - reboot() - } + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } - PullToRefreshBox( - modifier = boxModifier, - onRefresh = { + + var isRefreshing by rememberSaveable { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(350) viewModel.fetchModuleList() - }, - isRefreshing = viewModel.isRefreshing + isRefreshing = false + } + } + val refreshTexts = listOf( + stringResource(R.string.refresh_pulling), + stringResource(R.string.refresh_release), + stringResource(R.string.refresh_refresh), + stringResource(R.string.refresh_complete), + ) + PullToRefresh( + isRefreshing = isRefreshing, + pullToRefreshState = pullToRefreshState, + onRefresh = { isRefreshing = true }, + refreshTexts = refreshTexts, + contentPadding = PaddingValues(top = innerPadding.calculateTopPadding() + 12.dp) ) { + val layoutDirection = LocalLayoutDirection.current + LazyColumn( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = remember { - PaddingValues( - start = 16.dp, - top = 16.dp, - end = 16.dp, - bottom = 16.dp + 56.dp + 16.dp + 48.dp + 6.dp /* Scaffold Fab Spacing + Fab container height + SnackBar height */ - ) - }, + modifier = modifier.height(getWindowSize().height.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + 12.dp, + bottom = bottomInnerPadding + 12.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), + overscrollEffect = null, ) { when { !viewModel.isOverlayAvailable -> { @@ -510,18 +587,10 @@ private fun ModuleList( } if (success) { viewModel.fetchModuleList() - - val result = snackBarHost.showSnackbar( - message = rebootToApply, - actionLabel = reboot, - duration = SnackbarDuration.Long - ) - if (result == SnackbarResult.ActionPerformed) { - reboot() - } + Toast.makeText(context, rebootToApply, Toast.LENGTH_SHORT).show() } else { val message = if (module.enabled) failedDisable else failedEnable - snackBarHost.showSnackbar(message.format(module.name)) + Toast.makeText(context, message.format(module.name), Toast.LENGTH_SHORT).show() } } }, @@ -539,9 +608,6 @@ private fun ModuleList( onClickModule(it.id, it.name, it.hasWebUi) } ) - - // fix last item shadow incomplete in LazyColumn - Spacer(Modifier.height(1.dp)) } } } @@ -562,204 +628,190 @@ fun ModuleItem( onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, onClick: (ModuleViewModel.ModuleInfo) -> Unit ) { - ElevatedCard( - modifier = Modifier.fillMaxWidth() + val textDecoration = if (!module.remove) null else TextDecoration.LineThrough + val viewModel = viewModel() + + Card( + modifier = Modifier.fillMaxWidth(), + insideMargin = PaddingValues(16.dp) ) { - val textDecoration = if (!module.remove) null else TextDecoration.LineThrough - val interactionSource = remember { MutableInteractionSource() } - val indication = LocalIndication.current - val viewModel = viewModel() - - Column( - modifier = Modifier - .run { - if (module.hasWebUi) { - toggleable( - value = module.enabled, - enabled = !module.remove && module.enabled, - interactionSource = interactionSource, - role = Role.Button, - indication = indication, - onValueChange = { onClick(module) } - ) - } else { - this - } - } - .padding(22.dp, 18.dp, 22.dp, 12.dp) + Row( + modifier = Modifier, + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + Column( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) ) { val moduleVersion = stringResource(id = R.string.module_version) val moduleAuthor = stringResource(id = R.string.module_author) - Column( - modifier = Modifier.fillMaxWidth(0.8f) - ) { - Text( - text = module.name, - fontSize = MaterialTheme.typography.titleMedium.fontSize, - fontWeight = FontWeight.SemiBold, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.titleMedium.fontFamily, - textDecoration = textDecoration, - ) - - Text( - text = "$moduleVersion: ${module.version}", - fontSize = MaterialTheme.typography.bodySmall.fontSize, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - textDecoration = textDecoration - ) - - Text( - text = "$moduleAuthor: ${module.author}", - fontSize = MaterialTheme.typography.bodySmall.fontSize, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - textDecoration = textDecoration - ) - } + Text( + text = module.name, + fontSize = 17.sp, + fontWeight = FontWeight(550), + color = colorScheme.onSurface, + textDecoration = textDecoration, + ) - Spacer(modifier = Modifier.weight(1f)) + Text( + text = "$moduleVersion: ${module.version}", + fontSize = 14.sp, + modifier = Modifier.padding(top = 1.dp), + fontWeight = FontWeight.Medium, + color = colorScheme.onSurfaceVariantSummary, + textDecoration = textDecoration, + ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - Switch( - enabled = !module.update, - checked = module.enabled, - onCheckedChange = onCheckChanged, - interactionSource = if (!module.hasWebUi) interactionSource else null - ) - } + Text( + text = "$moduleAuthor: ${module.author}", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurfaceVariantSummary, + textDecoration = textDecoration, + ) } - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = module.description, - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontWeight = MaterialTheme.typography.bodySmall.fontWeight, - overflow = TextOverflow.Ellipsis, - maxLines = 4, - textDecoration = textDecoration + Switch( + modifier = Modifier, + checked = module.enabled, + onCheckedChange = onCheckChanged ) + } - Spacer(modifier = Modifier.height(16.dp)) - - HorizontalDivider(thickness = Dp.Hairline) + Text( + text = module.description, + fontSize = 14.5.sp, + color = colorScheme.onSurfaceVariantSummary, + modifier = Modifier.padding(top = 2.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 4, + textDecoration = textDecoration + ) - Spacer(modifier = Modifier.height(4.dp)) + HorizontalDivider( + modifier = Modifier.padding(top = 10.dp, bottom = 10.dp), + thickness = 0.5.dp, + color = colorScheme.outline.copy(alpha = 0.5f) + ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedVisibility( + visible = module.enabled && !module.remove, + enter = fadeIn(), + exit = fadeOut(), ) { - if (module.hasActionScript) { - FilledTonalButton( - modifier = Modifier.defaultMinSize(52.dp, 32.dp), - enabled = !module.remove && module.enabled, + IconButton( + backgroundColor = colorScheme.secondaryContainer.copy(alpha = 0.8f), + minHeight = 35.dp, + minWidth = 35.dp, onClick = { - navigator.navigate(ExecuteModuleActionScreenDestination(module.id)) + navigator.navigate(ExecuteModuleActionScreenDestination(module.id)) { + launchSingleTop = true + } viewModel.markNeedRefresh() }, - contentPadding = ButtonDefaults.TextButtonContentPadding ) { Icon( modifier = Modifier.size(20.dp), - imageVector = Icons.Outlined.PlayArrow, - contentDescription = null + imageVector = Icons.Rounded.PlayArrow, + tint = colorScheme.onSurface.copy(alpha = if (isSystemInDarkTheme()) 0.7f else 0.9f), + contentDescription = stringResource(R.string.action) ) - if (!module.hasWebUi && updateUrl.isEmpty()) { - Text( - modifier = Modifier.padding(start = 7.dp), - text = stringResource(R.string.action), - fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - fontSize = MaterialTheme.typography.labelMedium.fontSize - ) - } } - - Spacer(modifier = Modifier.weight(0.1f, true)) } if (module.hasWebUi) { - FilledTonalButton( - modifier = Modifier.defaultMinSize(52.dp, 32.dp), - enabled = !module.remove && module.enabled, + IconButton( + backgroundColor = colorScheme.secondaryContainer.copy(alpha = 0.8f), + minHeight = 35.dp, + minWidth = 35.dp, onClick = { onClick(module) }, - interactionSource = interactionSource, - contentPadding = ButtonDefaults.TextButtonContentPadding ) { Icon( modifier = Modifier.size(20.dp), - imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, - contentDescription = null + imageVector = Icons.Rounded.Code, + tint = colorScheme.onSurface.copy(alpha = if (isSystemInDarkTheme()) 0.7f else 0.9f), + contentDescription = stringResource(R.string.open) ) - if (!module.hasActionScript && updateUrl.isEmpty()) { - Text( - modifier = Modifier.padding(start = 7.dp), - fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - fontSize = MaterialTheme.typography.labelMedium.fontSize, - text = stringResource(R.string.open) - ) - } } } + } - Spacer(modifier = Modifier.weight(1f, true)) + Spacer(modifier = Modifier.weight(1f, true)) - if (updateUrl.isNotEmpty()) { - Button( - modifier = Modifier.defaultMinSize(52.dp, 32.dp), - enabled = !module.remove, - onClick = { onUpdate(module) }, - shape = ButtonDefaults.textShape, - contentPadding = ButtonDefaults.TextButtonContentPadding + if (updateUrl.isNotEmpty()) { + IconButton( + backgroundColor = Color(if (isSystemInDarkTheme()) 0xFF25354E else 0xFFEAF2FF), + enabled = !module.remove, + minHeight = 35.dp, + minWidth = 35.dp, + onClick = { onUpdate(module) }, + ) { + Row( + modifier = Modifier.then( + if (updateUrl.isNotEmpty()) { + Modifier.padding(horizontal = 10.dp) + } else { + Modifier + } + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), ) { Icon( modifier = Modifier.size(20.dp), - imageVector = Icons.Outlined.Download, - contentDescription = null + imageVector = Icons.Rounded.Download, + tint = Color(0xFF0D84FF), + contentDescription = stringResource(R.string.module_update), + ) + Text( + text = stringResource(R.string.module_update), + color = Color(0xFF0D84FF), + fontWeight = FontWeight.Medium, + fontSize = 14.sp ) - if (!module.hasActionScript || !module.hasWebUi) { - Text( - modifier = Modifier.padding(start = 7.dp), - fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - fontSize = MaterialTheme.typography.labelMedium.fontSize, - text = stringResource(R.string.module_update) - ) - } } - - Spacer(modifier = Modifier.weight(0.1f, true)) } + } - FilledTonalButton( - modifier = Modifier.defaultMinSize(52.dp, 32.dp), - enabled = !module.remove, - onClick = { onUninstall(module) }, - contentPadding = ButtonDefaults.TextButtonContentPadding + IconButton( + enabled = !module.remove, + minHeight = 35.dp, + minWidth = 35.dp, + onClick = { onUninstall(module) }, + backgroundColor = colorScheme.secondaryContainer.copy(alpha = 0.8f), + ) { + Row( + modifier = Modifier.then( + if (updateUrl.isEmpty()) { + Modifier.padding(horizontal = 10.dp) + } else { + Modifier + } + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { Icon( - modifier = Modifier.size(20.dp), + modifier = Modifier + .size(20.dp), imageVector = Icons.Outlined.Delete, + tint = colorScheme.onSurface.copy(alpha = if (isSystemInDarkTheme()) 0.7f else 0.9f), contentDescription = null ) - if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) { + if (updateUrl.isEmpty()) { Text( - modifier = Modifier.padding(start = 7.dp), - fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - fontSize = MaterialTheme.typography.labelMedium.fontSize, - text = stringResource(R.string.uninstall) + modifier = Modifier.padding(end = 3.dp), + text = stringResource(R.string.uninstall), + color = colorScheme.onSurface.copy(alpha = if (isSystemInDarkTheme()) 0.7f else 0.9f), + fontWeight = FontWeight.Medium, + fontSize = 15.sp ) } } @@ -767,23 +819,3 @@ fun ModuleItem( } } } - -@Preview -@Composable -fun ModuleItemPreview() { - val module = ModuleViewModel.ModuleInfo( - id = "id", - name = "name", - version = "version", - versionCode = 1, - author = "author", - description = "I am a test module and i do nothing but show a very long description", - enabled = true, - update = true, - remove = false, - updateJson = "", - hasWebUi = false, - hasActionScript = false - ) - ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {}) -} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt index 2524a8098364..10069c505169 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt @@ -1,518 +1,392 @@ package me.weishu.kernelsu.ui.screen import android.content.Context -import android.content.Intent -import android.net.Uri -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.Undo -import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material.icons.filled.Compress -import androidx.compose.material.icons.filled.ContactPage -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.DeleteForever -import androidx.compose.material.icons.filled.DeveloperMode -import androidx.compose.material.icons.filled.Fence -import androidx.compose.material.icons.filled.FolderDelete -import androidx.compose.material.icons.filled.RemoveModerator -import androidx.compose.material.icons.filled.Save -import androidx.compose.material.icons.filled.Share -import androidx.compose.material.icons.filled.Update -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material.icons.rounded.Adb +import androidx.compose.material.icons.rounded.BugReport +import androidx.compose.material.icons.rounded.Compress +import androidx.compose.material.icons.rounded.ContactPage +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.DeleteForever +import androidx.compose.material.icons.rounded.DeveloperMode +import androidx.compose.material.icons.rounded.Fence +import androidx.compose.material.icons.rounded.FolderDelete +import androidx.compose.material.icons.rounded.RemoveModerator +import androidx.compose.material.icons.rounded.RestartAlt +import androidx.compose.material.icons.rounded.Update import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.LineHeightStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.FileProvider +import androidx.core.content.edit import androidx.lifecycle.compose.dropUnlessResumed -import com.maxkeppeker.sheets.core.models.base.Header -import com.maxkeppeker.sheets.core.models.base.IconSource -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.list.ListDialog -import com.maxkeppeler.sheets.list.models.ListOption -import com.maxkeppeler.sheets.list.models.ListSelection import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination -import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.AboutDialog import me.weishu.kernelsu.ui.component.ConfirmResult -import me.weishu.kernelsu.ui.component.DialogHandle -import me.weishu.kernelsu.ui.component.SwitchItem import me.weishu.kernelsu.ui.component.KsuIsValid +import me.weishu.kernelsu.ui.component.SendLogDialog +import me.weishu.kernelsu.ui.component.UninstallDialog import me.weishu.kernelsu.ui.component.rememberConfirmDialog -import me.weishu.kernelsu.ui.component.rememberCustomDialog import me.weishu.kernelsu.ui.component.rememberLoadingDialog -import me.weishu.kernelsu.ui.util.LocalSnackbarHost -import me.weishu.kernelsu.ui.util.getBugreportFile import me.weishu.kernelsu.ui.util.shrinkModules -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter +import top.yukonga.miuix.kmp.basic.Card +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.ScrollBehavior +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperSwitch +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/1/1. */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination @Composable +@Destination fun SettingScreen(navigator: DestinationsNavigator) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val snackBarHost = LocalSnackbarHost.current + val scrollBehavior = MiuixScrollBehavior() Scaffold( topBar = { TopBar( - onBack = dropUnlessResumed { - navigator.popBackStack() - }, - scrollBehavior = scrollBehavior + onBack = dropUnlessResumed { navigator.popBackStack() }, + scrollBehavior = scrollBehavior, ) }, - snackbarHost = { SnackbarHost(snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - val aboutDialog = rememberCustomDialog { - AboutDialog(it) - } + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> val loadingDialog = rememberLoadingDialog() val shrinkDialog = rememberConfirmDialog() - Column( + val showUninstallDialog = rememberSaveable { mutableStateOf(false) } + val uninstallDialog = UninstallDialog(showUninstallDialog, navigator) + val showSendLogDialog = rememberSaveable { mutableStateOf(false) } + val sendLogDialog = SendLogDialog(showSendLogDialog, loadingDialog) + + LazyColumn( modifier = Modifier - .padding(paddingValues) + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) + .padding(horizontal = 12.dp), + contentPadding = innerPadding, + overscrollEffect = null, ) { + item { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val context = LocalContext.current - val scope = rememberCoroutineScope() - - val exportBugreportLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.CreateDocument("application/gzip") - ) { uri: Uri? -> - if (uri == null) return@rememberLauncherForActivityResult - scope.launch(Dispatchers.IO) { - loadingDialog.show() - context.contentResolver.openOutputStream(uri)?.use { output -> - getBugreportFile(context).inputStream().use { - it.copyTo(output) - } - } - loadingDialog.hide() - snackBarHost.showSnackbar(context.getString(R.string.log_saved)) - } - } - - val profileTemplate = stringResource(id = R.string.settings_profile_template) - KsuIsValid() { - ListItem( - leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) }, - headlineContent = { Text(profileTemplate) }, - supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) }, - modifier = Modifier.clickable { - navigator.navigate(AppProfileTemplateScreenDestination) - } - ) - } - - var umountChecked by rememberSaveable { - mutableStateOf(Natives.isDefaultUmountModules()) - } - - KsuIsValid() { - SwitchItem( - icon = Icons.Filled.FolderDelete, - title = stringResource(id = R.string.settings_umount_modules_default), - summary = stringResource(id = R.string.settings_umount_modules_default_summary), - checked = umountChecked + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), ) { - if (Natives.setDefaultUmountModules(it)) { - umountChecked = it - } - } - } - - KsuIsValid() { - if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) { - var isSuDisabled by rememberSaveable { - mutableStateOf(!Natives.isSuEnabled()) + var checkUpdate by rememberSaveable { + mutableStateOf( + prefs.getBoolean("check_update", true) + ) } - SwitchItem( - icon = Icons.Filled.RemoveModerator, - title = stringResource(id = R.string.settings_disable_su), - summary = stringResource(id = R.string.settings_disable_su_summary), - checked = isSuDisabled, - ) { checked -> - val shouldEnable = !checked - if (Natives.setSuEnabled(shouldEnable)) { - isSuDisabled = !shouldEnable + SuperSwitch( + title = stringResource(id = R.string.settings_check_update), + summary = stringResource(id = R.string.settings_check_update_summary), + leftAction = { + Icon( + Icons.Rounded.Update, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_check_update), + tint = colorScheme.onBackground + ) + }, + checked = checkUpdate, + onCheckedChange = { it -> + prefs.edit { putBoolean("check_update", it) } + checkUpdate = it } - } + ) } - } - - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - var checkUpdate by rememberSaveable { - mutableStateOf( - prefs.getBoolean("check_update", true) - ) - } - SwitchItem( - icon = Icons.Filled.Update, - title = stringResource(id = R.string.settings_check_update), - summary = stringResource(id = R.string.settings_check_update_summary), - checked = checkUpdate - ) { - prefs.edit().putBoolean("check_update", it).apply() - checkUpdate = it - } - var enableWebDebugging by rememberSaveable { - mutableStateOf( - prefs.getBoolean("enable_web_debugging", false) - ) - } - - KsuIsValid() { - SwitchItem( - icon = Icons.Filled.DeveloperMode, - title = stringResource(id = R.string.enable_web_debugging), - summary = stringResource(id = R.string.enable_web_debugging_summary), - checked = enableWebDebugging - ) { - prefs.edit().putBoolean("enable_web_debugging", it).apply() - enableWebDebugging = it + KsuIsValid { + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + ) { + val profileTemplate = stringResource(id = R.string.settings_profile_template) + SuperArrow( + title = profileTemplate, + summary = stringResource(id = R.string.settings_profile_template_summary), + leftAction = { + Icon( + Icons.Rounded.Fence, + modifier = Modifier.padding(end = 16.dp), + contentDescription = profileTemplate, + tint = colorScheme.onBackground + ) + }, + onClick = { + navigator.navigate(AppProfileTemplateScreenDestination) { + launchSingleTop = true + } + } + ) + } } - } - - var showBottomsheet by remember { mutableStateOf(false) } - ListItem( - leadingContent = { - Icon( - Icons.Filled.BugReport, - stringResource(id = R.string.send_log) - ) - }, - headlineContent = { Text(stringResource(id = R.string.send_log)) }, - modifier = Modifier.clickable { - showBottomsheet = true - } - ) - if (showBottomsheet) { - ModalBottomSheet( - onDismissRequest = { showBottomsheet = false }, - content = { - Row( - modifier = Modifier - .padding(10.dp) - .align(Alignment.CenterHorizontally) + var umountChecked by rememberSaveable { mutableStateOf(Natives.isDefaultUmountModules()) } - ) { - Box { - Column( - modifier = Modifier - .padding(16.dp) - .clickable { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") - val current = LocalDateTime.now().format(formatter) - exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") - showBottomsheet = false - } - ) { + KsuIsValid { + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + ) { + SuperSwitch( + title = stringResource(id = R.string.settings_umount_modules_default), + summary = stringResource(id = R.string.settings_umount_modules_default_summary), + leftAction = { + Icon( + Icons.Rounded.FolderDelete, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_umount_modules_default), + tint = colorScheme.onBackground + ) + }, + checked = umountChecked, + onCheckedChange = { it -> + if (Natives.setDefaultUmountModules(it)) { + umountChecked = it + } + } + ) + if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) { + var isSuDisabled by rememberSaveable { + mutableStateOf(!Natives.isSuEnabled()) + } + SuperSwitch( + title = stringResource(id = R.string.settings_disable_su), + summary = stringResource(id = R.string.settings_disable_su_summary), + leftAction = { Icon( - Icons.Filled.Save, - contentDescription = null, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - Text( - text = stringResource(id = R.string.save_log), - modifier = Modifier.padding(top = 16.dp), - textAlign = TextAlign.Center.also { - LineHeightStyle( - alignment = LineHeightStyle.Alignment.Center, - trim = LineHeightStyle.Trim.None - ) - } - + Icons.Rounded.RemoveModerator, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_su), + tint = colorScheme.onBackground ) + }, + checked = isSuDisabled, + onCheckedChange = { checked: Boolean -> + val shouldEnable = !checked + if (Natives.setSuEnabled(shouldEnable)) { + isSuDisabled = !shouldEnable + } } - } - Box { - Column( - modifier = Modifier - .padding(16.dp) - .clickable { - scope.launch { - val bugreport = loadingDialog.withLoading { - withContext(Dispatchers.IO) { - getBugreportFile(context) - } - } + ) + } - val uri: Uri = - FileProvider.getUriForFile( - context, - "${BuildConfig.APPLICATION_ID}.fileprovider", - bugreport - ) - val shareIntent = Intent(Intent.ACTION_SEND).apply { - putExtra(Intent.EXTRA_STREAM, uri) - setDataAndType(uri, "application/gzip") - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } + var enableWebDebugging by rememberSaveable { + mutableStateOf( + prefs.getBoolean("enable_web_debugging", false) + ) + } - context.startActivity( - Intent.createChooser( - shareIntent, - context.getString(R.string.send_log) - ) - ) - } - } - ) { - Icon( - Icons.Filled.Share, - contentDescription = null, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - Text( - text = stringResource(id = R.string.send_log), - modifier = Modifier.padding(top = 16.dp), - textAlign = TextAlign.Center.also { - LineHeightStyle( - alignment = LineHeightStyle.Alignment.Center, - trim = LineHeightStyle.Trim.None - ) - } - ) - } + SuperSwitch( + title = stringResource(id = R.string.enable_web_debugging), + summary = stringResource(id = R.string.enable_web_debugging_summary), + leftAction = { + Icon( + Icons.Rounded.DeveloperMode, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.enable_web_debugging), + tint = colorScheme.onBackground + ) + }, + checked = enableWebDebugging, + onCheckedChange = { it -> + prefs.edit { putBoolean("enable_web_debugging", it) } + enableWebDebugging = it } - } + ) } - ) - } + } - val shrink = stringResource(id = R.string.shrink_sparse_image) - val shrinkMessage = stringResource(id = R.string.shrink_sparse_image_message) - KsuIsValid() { - ListItem( - leadingContent = { - Icon( - Icons.Filled.Compress, - shrink + val shrink = stringResource(id = R.string.shrink_sparse_image) + KsuIsValid { + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + ) { + SuperArrow( + title = shrink, + leftAction = { + Icon( + Icons.Rounded.Compress, + modifier = Modifier.padding(end = 16.dp), + contentDescription = shrink, + tint = colorScheme.onBackground + ) + }, + onClick = { + scope.launch { + val result = shrinkDialog.awaitConfirm(title = shrink) + if (result == ConfirmResult.Confirmed) { + loadingDialog.withLoading { + shrinkModules() + } + } + } + }, ) - }, - headlineContent = { Text(shrink) }, - modifier = Modifier.clickable { - scope.launch { - val result = shrinkDialog.awaitConfirm(title = shrink, content = shrinkMessage) - if (result == ConfirmResult.Confirmed) { - loadingDialog.withLoading { - shrinkModules() + + val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode + if (lkmMode) { + val uninstall = stringResource(id = R.string.settings_uninstall) + SuperArrow( + title = uninstall, + leftAction = { + Icon( + Icons.Rounded.Delete, + modifier = Modifier.padding(end = 16.dp), + contentDescription = uninstall, + tint = colorScheme.onBackground, + ) + }, + onClick = { + showUninstallDialog.value = true + uninstallDialog } - } + ) } } - ) - } - - val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode - if (lkmMode) { - UninstallItem(navigator) { - loadingDialog.withLoading(it) } - } - val about = stringResource(id = R.string.about) - ListItem( - leadingContent = { - Icon( - Icons.Filled.ContactPage, - about + Card( + modifier = Modifier + .padding(vertical = 12.dp) + .fillMaxWidth(), + ) { + SuperArrow( + title = stringResource(id = R.string.send_log), + leftAction = { + Icon( + Icons.Rounded.BugReport, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.send_log), + tint = colorScheme.onBackground + ) + }, + onClick = { + showSendLogDialog.value = true + sendLogDialog + }, + ) + val about = stringResource(id = R.string.about) + SuperArrow( + title = about, + leftAction = { + Icon( + Icons.Rounded.ContactPage, + modifier = Modifier.padding(end = 16.dp), + contentDescription = about, + tint = colorScheme.onBackground + ) + }, + onClick = { + navigator.navigate(AboutScreenDestination) { + launchSingleTop = true + } + } ) - }, - headlineContent = { Text(about) }, - modifier = Modifier.clickable { - aboutDialog.show() - } - ) - } - } -} - -@Composable -fun UninstallItem( - navigator: DestinationsNavigator, - withLoading: suspend (suspend () -> Unit) -> Unit -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val uninstallConfirmDialog = rememberConfirmDialog() - val showTodo = { - Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() - } - val uninstallDialog = rememberUninstallDialog { uninstallType -> - scope.launch { - val result = uninstallConfirmDialog.awaitConfirm( - title = context.getString(uninstallType.title), - content = context.getString(uninstallType.message) - ) - if (result == ConfirmResult.Confirmed) { - withLoading { - when (uninstallType) { - UninstallType.TEMPORARY -> showTodo() - UninstallType.PERMANENT -> navigator.navigate( - FlashScreenDestination(FlashIt.FlashUninstall) - ) - UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate( - FlashScreenDestination(FlashIt.FlashRestore) - ) - UninstallType.NONE -> Unit - } } + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) } } } - val uninstall = stringResource(id = R.string.settings_uninstall) - ListItem( - leadingContent = { - Icon( - Icons.Filled.Delete, - uninstall - ) - }, - headlineContent = { Text(uninstall) }, - modifier = Modifier.clickable { - uninstallDialog.show() - } - ) } -enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) { +enum class UninstallType(val icon: ImageVector, val title: Int, val message: Int) { TEMPORARY( + Icons.Rounded.RemoveModerator, R.string.settings_uninstall_temporary, - R.string.settings_uninstall_temporary_message, - Icons.Filled.Delete + R.string.settings_uninstall_temporary_message ), PERMANENT( + Icons.Rounded.DeleteForever, R.string.settings_uninstall_permanent, - R.string.settings_uninstall_permanent_message, - Icons.Filled.DeleteForever + R.string.settings_uninstall_permanent_message ), RESTORE_STOCK_IMAGE( + Icons.Rounded.RestartAlt, R.string.settings_restore_stock_image, - R.string.settings_restore_stock_image_message, - Icons.AutoMirrored.Filled.Undo + R.string.settings_restore_stock_image_message ), - NONE(0, 0, Icons.Filled.Delete) + NONE(Icons.Rounded.Adb, 0, 0) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle { - return rememberCustomDialog { dismiss -> - val options = listOf( - // UninstallType.TEMPORARY, - UninstallType.PERMANENT, - UninstallType.RESTORE_STOCK_IMAGE - ) - val listOptions = options.map { - ListOption( - titleText = stringResource(it.title), - subtitleText = if (it.message != 0) stringResource(it.message) else null, - icon = IconSource(it.icon) - ) - } - - var selection = UninstallType.NONE - ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = { - if (selection != UninstallType.NONE) { - onSelected(selection) - } - }, onCloseRequest = { - dismiss() - }), header = Header.Default( - title = stringResource(R.string.settings_uninstall), - ), selection = ListSelection.Single( - showRadioButtons = false, - options = listOptions, - ) { index, _ -> - selection = options[index] - }) - } -} - -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( onBack: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null + scrollBehavior: ScrollBehavior, ) { TopAppBar( - title = { Text(stringResource(R.string.settings)) }, + title = stringResource(R.string.settings), navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack ) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + Icon( + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } - -@Preview -@Composable -private fun SettingsPreview() { - SettingScreen(EmptyDestinationsNavigator) -} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt index a2510a7fff94..6bbe8db533b1 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt @@ -1,126 +1,280 @@ package me.weishu.kernelsu.ui.screen +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.FlowColumn +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.* -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.AsyncImage -import coil.request.ImageRequest -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.SearchAppBar +import me.weishu.kernelsu.ui.component.AppIconImage +import me.weishu.kernelsu.ui.component.DropdownItem +import me.weishu.kernelsu.ui.component.SearchBox +import me.weishu.kernelsu.ui.component.SearchPager import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel +import top.yukonga.miuix.kmp.basic.BasicComponent +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.PullToRefresh +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.basic.ArrowRight +import top.yukonga.miuix.kmp.icon.icons.useful.ImmersionMore +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.PressFeedbackType +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) -@Destination @Composable -fun SuperUserScreen(navigator: DestinationsNavigator) { +fun SuperUserPager( + navigator: DestinationsNavigator, + bottomInnerPadding: Dp +) { val viewModel = viewModel() val scope = rememberCoroutineScope() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() val listState = rememberLazyListState() - + val searchStatus by viewModel.searchStatus LaunchedEffect(key1 = navigator) { - viewModel.search = "" - if (viewModel.appList.isEmpty()) { + if (viewModel.appList.value.isEmpty() || viewModel.searchResults.value.isEmpty()) { viewModel.fetchAppList() } } - LaunchedEffect(viewModel.search) { - if (viewModel.search.isEmpty()) { - listState.scrollToItem(0) - } + LaunchedEffect(searchStatus.searchText) { + viewModel.updateSearchText(searchStatus.searchText) } + val dynamicTopPadding by animateDpAsState( + targetValue = 12.dp * (1f - scrollBehavior.state.collapsedFraction) + ) + Scaffold( topBar = { - SearchAppBar( - title = { Text(stringResource(R.string.superuser)) }, - searchText = viewModel.search, - onSearchTextChange = { viewModel.search = it }, - onClearClick = { viewModel.search = "" }, - dropdownContent = { - var showDropdown by remember { mutableStateOf(false) } - - IconButton( - onClick = { showDropdown = true }, - ) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(id = R.string.settings) - ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false - }) { - DropdownMenuItem(text = { - Text(stringResource(R.string.refresh)) - }, onClick = { - scope.launch { - viewModel.fetchAppList() - } - showDropdown = false - }) - DropdownMenuItem(text = { - Text( - if (viewModel.showSystemApps) { + searchStatus.TopAppBarAnim { + TopAppBar( + title = stringResource(R.string.superuser), + actions = { + val showTopPopup = remember { mutableStateOf(false) } + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + showTopPopup.value = false + } + ) { + ListPopupColumn { + DropdownItem( + text = stringResource(R.string.refresh), + optionSize = 2, + onSelectedIndexChange = { + scope.launch { + viewModel.fetchAppList() + } + showTopPopup.value = false + }, + index = 0 + ) + DropdownItem( + text = if (viewModel.showSystemApps) { stringResource(R.string.hide_system_apps) } else { stringResource(R.string.show_system_apps) - } + }, + optionSize = 2, + onSelectedIndexChange = { + scope.launch { + viewModel.showSystemApps = !viewModel.showSystemApps + viewModel.fetchAppList() + } + showTopPopup.value = false + }, + index = 1 ) - }, onClick = { - viewModel.showSystemApps = !viewModel.showSystemApps - showDropdown = false - }) + } + } + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { + showTopPopup.value = true + }, + holdDownState = showTopPopup.value + ) { + Icon( + imageVector = MiuixIcons.Useful.ImmersionMore, + tint = colorScheme.onSurface, + contentDescription = stringResource(id = R.string.settings) + ) + } + }, + scrollBehavior = scrollBehavior + ) + } + }, + popupHost = { + searchStatus.SearchPager( + defaultResult = {}, + searchBarTopPadding = dynamicTopPadding, + ) { + items(viewModel.searchResults.value, key = { it.packageName + it.uid }) { app -> + AnimatedVisibility( + visible = viewModel.searchResults.value.isNotEmpty(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + AppItem(app) { + navigator.navigate(AppProfileScreenDestination(app)) { + launchSingleTop = true + } } } - }, - scrollBehavior = scrollBehavior - ) + } + item { + Spacer(Modifier.height(bottomInnerPadding)) + } + } }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - PullToRefreshBox( - modifier = Modifier.padding(innerPadding), - onRefresh = { - scope.launch { viewModel.fetchAppList() } - }, - isRefreshing = viewModel.isRefreshing - ) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + val layoutDirection = LocalLayoutDirection.current + searchStatus.SearchBox( + searchBarTopPadding = dynamicTopPadding, + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding(), + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), + ) { boxHeight -> + var isRefreshing by rememberSaveable { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(350) + viewModel.fetchAppList() + isRefreshing = false + } + } + val refreshTexts = listOf( + stringResource(R.string.refresh_pulling), + stringResource(R.string.refresh_release), + stringResource(R.string.refresh_refresh), + stringResource(R.string.refresh_complete), + ) + PullToRefresh( + isRefreshing = isRefreshing, + pullToRefreshState = pullToRefreshState, + onRefresh = { isRefreshing = true }, + refreshTexts = refreshTexts, + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), ) { - items(viewModel.appList, key = { it.packageName + it.uid }) { app -> - AppItem(app) { - navigator.navigate(AppProfileScreenDestination(app)) + if (viewModel.appList.value.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection), + bottom = bottomInnerPadding + ), + contentAlignment = Alignment.Center + ) { + Text( + text = if (viewModel.isRefreshing) "Loading..." else "Empty", + textAlign = TextAlign.Center, + color = Color.Gray, + ) + } + } else { + LazyColumn( + state = listState, + modifier = Modifier + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), + overscrollEffect = null, + ) { + items(viewModel.appList.value, key = { it.packageName + it.uid }) { app -> + AppItem(app) { + navigator.navigate(AppProfileScreenDestination(app)) { + launchSingleTop = true + } + } + } + item { + Spacer(Modifier.height(bottomInnerPadding)) + } } } } @@ -128,65 +282,93 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { } } -@OptIn(ExperimentalLayoutApi::class) @Composable private fun AppItem( app: SuperUserViewModel.AppInfo, onClickListener: () -> Unit, ) { - ListItem( - modifier = Modifier.clickable(onClick = onClickListener), - headlineContent = { Text(app.label) }, - supportingContent = { - Column { - Text(app.packageName) - FlowRow { - if (app.allowSu) { - LabelText(label = "ROOT") - } else { - if (Natives.uidShouldUmount(app.uid)) { - LabelText(label = "UMOUNT") + Card( + modifier = Modifier + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + onClick = { + onClickListener() + }, + pressFeedbackType = PressFeedbackType.Sink, + showIndication = true, + ) { + BasicComponent( + title = app.label, + summary = app.packageName, + leftAction = { + AppIconImage( + packageInfo = app.packageInfo, + label = app.label, + modifier = Modifier + .padding(end = 12.dp) + .size(48.dp) + ) + }, + rightActions = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + FlowColumn( + verticalArrangement = Arrangement.spacedBy(6.dp), + itemHorizontalAlignment = Alignment.End + ) { + if (app.allowSu) { + StatusTag( + label = "ROOT", + backgroundColor = colorScheme.tertiaryContainer, + contentColor = colorScheme.onTertiaryContainer + ) + } else { + if (Natives.uidShouldUmount(app.uid)) { + StatusTag( + label = "UMOUNT", + backgroundColor = colorScheme.secondaryContainer, + contentColor = colorScheme.onSecondaryContainer + ) + } + } + if (app.hasCustomProfile) { + StatusTag( + label = "CUSTOM", + backgroundColor = colorScheme.primaryContainer, + contentColor = colorScheme.onPrimaryContainer + ) } } - if (app.hasCustomProfile) { - LabelText(label = "CUSTOM") - } + Image( + modifier = Modifier + .padding(start = 8.dp) + .size(10.dp, 16.dp), + imageVector = MiuixIcons.Basic.ArrowRight, + contentDescription = null, + colorFilter = ColorFilter.tint(colorScheme.onSurfaceVariantActions), + ) } } - }, - leadingContent = { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(app.packageInfo) - .crossfade(true) - .build(), - contentDescription = app.label, - modifier = Modifier - .padding(4.dp) - .width(48.dp) - .height(48.dp) - ) - }, - ) + ) + } } @Composable -fun LabelText(label: String) { +private fun StatusTag(label: String, backgroundColor: Color, contentColor: Color) { Box( modifier = Modifier - .padding(top = 4.dp, end = 4.dp) .background( - Color.Black, - shape = RoundedCornerShape(4.dp) + color = backgroundColor.copy(alpha = 0.8f), + shape = RoundedCornerShape(6.dp) ) ) { Text( + modifier = Modifier.padding(horizontal = 6.dp, vertical = 3.dp), text = label, - modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp), - style = TextStyle( - fontSize = 8.sp, - color = Color.White, - ) + color = contentColor, + fontSize = 10.sp, + fontWeight = FontWeight.Bold ) } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Template.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Template.kt index 4904f160bd74..d012d083e938 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Template.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Template.kt @@ -1,54 +1,66 @@ package me.weishu.kernelsu.ui.screen import android.widget.Toast -import androidx.compose.foundation.clickable +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.border +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.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ImportExport -import androidx.compose.material.icons.filled.Sync -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material.icons.outlined.Fingerprint +import androidx.compose.material.icons.outlined.Group +import androidx.compose.material.icons.outlined.Shield +import androidx.compose.material.icons.rounded.Add import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel import com.ramcosta.composedestinations.annotation.Destination @@ -58,25 +70,51 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.result.getOr import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.component.DropdownItem import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.FloatingActionButton +import top.yukonga.miuix.kmp.basic.HorizontalDivider +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.PullToRefresh +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Copy +import top.yukonga.miuix.kmp.icon.icons.useful.Refresh +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.PressFeedbackType +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/10/20. */ - -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) -@Destination @Composable +@Destination fun AppProfileTemplateScreen( navigator: DestinationsNavigator, resultRecipient: ResultRecipient ) { val viewModel = viewModel() val scope = rememberCoroutineScope() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() LaunchedEffect(Unit) { if (viewModel.templateList.isEmpty()) { @@ -91,6 +129,36 @@ fun AppProfileTemplateScreen( } } + val listState = rememberLazyListState() + var fabVisible by remember { mutableStateOf(true) } + var scrollDistance by remember { mutableFloatStateOf(0f) } + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val isScrolledToEnd = + (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1 + && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.size + ?: 0) < listState.layoutInfo.viewportEndOffset) + val delta = available.y + if (!isScrolledToEnd) { + scrollDistance += delta + if (scrollDistance < -50f) { + if (fabVisible) fabVisible = false + scrollDistance = 0f + } else if (scrollDistance > 50f) { + if (!fabVisible) fabVisible = true + scrollDistance = 0f + } + } + return Offset.Zero + } + } + } + val offsetHeight by animateDpAsState( + targetValue = if (fabVisible) 0.dp else 100.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(), + animationSpec = tween(durationMillis = 350) + ) + Scaffold( topBar = { val clipboardManager = LocalClipboardManager.current @@ -133,137 +201,285 @@ fun AppProfileTemplateScreen( } } }, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, ) }, floatingActionButton = { - ExtendedFloatingActionButton( + FloatingActionButton( + containerColor = colorScheme.primary, + shadowElevation = 0.dp, onClick = { - navigator.navigate( - TemplateEditorScreenDestination( - TemplateViewModel.TemplateInfo(), - false - ) + navigator.navigate(TemplateEditorScreenDestination(TemplateViewModel.TemplateInfo(), false)) { + launchSingleTop = true + } + }, + modifier = Modifier + .offset(y = offsetHeight) + .padding( + bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 20.dp, + end = 20.dp + ) + .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), + content = { + Icon( + Icons.Rounded.Add, + null, + Modifier.size(40.dp), + tint = Color.White ) }, - icon = { Icon(Icons.Filled.Add, null) }, - text = { Text(stringResource(id = R.string.app_profile_template_create)) }, ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - PullToRefreshBox( - modifier = Modifier.padding(innerPadding), - isRefreshing = viewModel.isRefreshing, - onRefresh = { - scope.launch { viewModel.fetchTemplates() } + var isRefreshing by rememberSaveable { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(350) + viewModel.fetchTemplates() + isRefreshing = false } + } + val refreshTexts = listOf( + stringResource(R.string.refresh_pulling), + stringResource(R.string.refresh_release), + stringResource(R.string.refresh_refresh), + stringResource(R.string.refresh_complete), + ) + val layoutDirection = LocalLayoutDirection.current + PullToRefresh( + isRefreshing = isRefreshing, + pullToRefreshState = pullToRefreshState, + onRefresh = { isRefreshing = true }, + refreshTexts = refreshTexts, + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + 12.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), ) { LazyColumn( modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), - contentPadding = remember { - PaddingValues(bottom = 16.dp + 56.dp + 16.dp /* Scaffold Fab Spacing + Fab container height */) - } + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(nestedScrollConnection) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .padding(horizontal = 12.dp), + contentPadding = innerPadding, + overscrollEffect = null ) { + item { + Spacer(Modifier.height(12.dp)) + } items(viewModel.templateList, key = { it.id }) { app -> TemplateItem(navigator, app) } + item { + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) + } } } } } -@OptIn(ExperimentalLayoutApi::class) @Composable private fun TemplateItem( navigator: DestinationsNavigator, template: TemplateViewModel.TemplateInfo ) { - ListItem( - modifier = Modifier - .clickable { - navigator.navigate(TemplateEditorScreenDestination(template, !template.local)) - }, - headlineContent = { Text(template.name) }, - supportingContent = { - Column { + Card( + modifier = Modifier.padding(bottom = 12.dp), + onClick = { + navigator.navigate(TemplateEditorScreenDestination(template, !template.local)) { + popUpTo(TemplateEditorScreenDestination) { + inclusive = true + } + launchSingleTop = true + } + }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Sink + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { Text( - text = "${template.id}${if (template.author.isEmpty()) "" else "@${template.author}"}", - style = MaterialTheme.typography.bodySmall, - fontSize = MaterialTheme.typography.bodySmall.fontSize, + text = template.name, + fontSize = 17.sp, + fontWeight = FontWeight(550), + color = colorScheme.onSurface, ) - Text(template.description) - FlowRow { - LabelText(label = "UID: ${template.uid}") - LabelText(label = "GID: ${template.gid}") - LabelText(label = template.context) - if (template.local) { - LabelText(label = "local") - } else { - LabelText(label = "remote") - } + Spacer(modifier = Modifier.weight(1f)) + if (template.local) { + Text( + text = "LOCAL", + color = colorScheme.onTertiaryContainer, + fontWeight = FontWeight.Bold, + style = MiuixTheme.textStyles.footnote1 + ) + } else { + Text( + text = "REMOTE", + color = colorScheme.onSurfaceSecondary, + fontWeight = FontWeight.Bold, + style = MiuixTheme.textStyles.footnote1 + ) } } - }, - ) + + Text( + text = "${template.id}${if (template.author.isEmpty()) "" else " by @${template.author}"}", + modifier = Modifier.padding(top = 1.dp), + fontSize = 14.sp, + color = colorScheme.onSurfaceVariantSummary, + fontWeight = FontWeight.Medium, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = template.description, + fontSize = 14.5.sp, + color = colorScheme.onSurfaceVariantSummary, + ) + + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + thickness = 0.5.dp, + color = colorScheme.outline.copy(alpha = 0.5f) + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + InfoChip( + icon = Icons.Outlined.Fingerprint, + text = "UID: ${template.uid}" + ) + InfoChip( + icon = Icons.Outlined.Group, + text = "GID: ${template.gid}" + ) + InfoChip( + icon = Icons.Outlined.Shield, + text = template.context + ) + } + } + } +} + +@Composable +private fun InfoChip(icon: ImageVector, text: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = colorScheme.onSurfaceSecondary.copy(alpha = 0.8f) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = text, + style = MiuixTheme.textStyles.body2, + color = colorScheme.onSurfaceSecondary + ) + } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( onBack: () -> Unit, onSync: () -> Unit = {}, onImport: () -> Unit = {}, onExport: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null + scrollBehavior: ScrollBehavior, ) { TopAppBar( - title = { - Text(stringResource(R.string.settings_profile_template)) - }, + title = stringResource(R.string.settings_profile_template), navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + ) { + Icon( + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } }, actions = { - IconButton(onClick = onSync) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onSync + ) { Icon( - Icons.Filled.Sync, - contentDescription = stringResource(id = R.string.app_profile_template_sync) + imageVector = MiuixIcons.Useful.Refresh, + contentDescription = stringResource(id = R.string.app_profile_template_sync), + tint = colorScheme.onBackground ) } - var showDropdown by remember { mutableStateOf(false) } - IconButton(onClick = { - showDropdown = true - }) { + val showTopPopup = remember { mutableStateOf(false) } + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + showTopPopup.value = false + } + ) { + ListPopupColumn { + val items = listOf( + stringResource(id = R.string.app_profile_import_from_clipboard), + stringResource(id = R.string.app_profile_export_to_clipboard) + ) + items.forEachIndexed { index, text -> + DropdownItem( + text = text, + optionSize = items.size, + index = index, + onSelectedIndexChange = { selectedIndex -> + if (selectedIndex == 0) { + onImport() + } else { + onExport() + } + showTopPopup.value = false + } + ) + } + } + } + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { showTopPopup.value = true }, + holdDownState = showTopPopup.value + ) { Icon( - imageVector = Icons.Filled.ImportExport, - contentDescription = stringResource(id = R.string.app_profile_import_export) + imageVector = MiuixIcons.Useful.Copy, + contentDescription = stringResource(id = R.string.app_profile_import_export), + tint = colorScheme.onBackground ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false - }) { - DropdownMenuItem(text = { - Text(stringResource(id = R.string.app_profile_import_from_clipboard)) - }, onClick = { - onImport() - showDropdown = false - }) - DropdownMenuItem(text = { - Text(stringResource(id = R.string.app_profile_export_to_clipboard)) - }, onClick = { - onExport() - showDropdown = false - }) - } } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/TemplateEditor.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/TemplateEditor.kt index 535b5c30613f..5e9b4e0734a2 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/TemplateEditor.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/TemplateEditor.kt @@ -2,68 +2,69 @@ package me.weishu.kernelsu.ui.screen import android.widget.Toast import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.DeleteForever -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.result.ResultBackNavigator import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.component.EditText import me.weishu.kernelsu.ui.component.profile.RootProfileConfig import me.weishu.kernelsu.ui.util.deleteAppProfileTemplate import me.weishu.kernelsu.ui.util.getAppProfileTemplate import me.weishu.kernelsu.ui.util.setAppProfileTemplate import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel import me.weishu.kernelsu.ui.viewmodel.toJSON +import top.yukonga.miuix.kmp.basic.Card +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.ScrollBehavior +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Confirm +import top.yukonga.miuix.kmp.icon.icons.useful.Delete +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/10/20. */ -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) -@Destination @Composable +@Destination fun TemplateEditorScreen( navigator: ResultBackNavigator, initialTemplate: TemplateViewModel.TemplateInfo, @@ -77,7 +78,7 @@ fun TemplateEditorScreen( mutableStateOf(initialTemplate) } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() BackHandler { navigator.navigateBack(result = !readOnly) @@ -85,15 +86,9 @@ fun TemplateEditorScreen( Scaffold( topBar = { - val author = - if (initialTemplate.author.isNotEmpty()) "@${initialTemplate.author}" else "" - val readOnlyHint = if (readOnly) { - " - ${stringResource(id = R.string.app_profile_template_readonly)}" - } else { - "" - } - val titleSummary = "${initialTemplate.id}$author$readOnlyHint" val saveTemplateFailed = stringResource(id = R.string.app_profile_template_save_failed) + val idConflictError = stringResource(id = R.string.app_profile_template_id_exist) + val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid) val context = LocalContext.current TopBar( @@ -105,7 +100,6 @@ fun TemplateEditorScreen( stringResource(R.string.app_profile_template_edit) }, readOnly = readOnly, - summary = titleSummary, onBack = dropUnlessResumed { navigator.navigateBack(result = !readOnly) }, onDelete = { if (deleteAppProfileTemplate(template.id)) { @@ -113,106 +107,153 @@ fun TemplateEditorScreen( } }, onSave = { + when (idCheck(template.id)) { + 0 -> Unit + + 1 -> { + Toast.makeText(context, idConflictError, Toast.LENGTH_SHORT).show() + return@TopBar + } + + 2 -> { + Toast.makeText(context, idInvalidError, Toast.LENGTH_SHORT).show() + return@TopBar + } + } if (saveTemplate(template, isCreation)) { navigator.navigateBack(result = true) } else { Toast.makeText(context, saveTemplateFailed, Toast.LENGTH_SHORT).show() } }, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - Column( + LazyColumn( modifier = Modifier - .padding(innerPadding) + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) .pointerInteropFilter { // disable click and ripple if readOnly readOnly - } + }, + contentPadding = innerPadding, + overscrollEffect = null ) { - if (isCreation) { - var errorHint by remember { - mutableStateOf("") - } - val idConflictError = stringResource(id = R.string.app_profile_template_id_exist) - val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid) - TextEdit( - label = stringResource(id = R.string.app_profile_template_id), - text = template.id, - errorHint = errorHint, - isError = errorHint.isNotEmpty() - ) { value -> - errorHint = if (isTemplateExist(value)) { - idConflictError - } else if (!isValidTemplateId(value)) { - idInvalidError - } else { - "" + item { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + ) { + var errorHint by remember { + mutableStateOf(false) } - template = template.copy(id = value) - } - } - TextEdit( - label = stringResource(id = R.string.app_profile_template_name), - text = template.name - ) { value -> - template.copy(name = value).run { - if (autoSave) { - if (!saveTemplate(this)) { - // failed - return@run + TextEdit( + label = stringResource(id = R.string.app_profile_template_name), + text = template.name + ) { value -> + template.copy(name = value).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this } } - template = this - } - } - TextEdit( - label = stringResource(id = R.string.app_profile_template_description), - text = template.description - ) { value -> - template.copy(description = value).run { - if (autoSave) { - if (!saveTemplate(this)) { - // failed - return@run + + TextEdit( + label = stringResource(id = R.string.app_profile_template_id), + text = template.id, + isError = errorHint + ) { value -> + errorHint = if (value.isEmpty()) { + false + } else if (isTemplateExist(value)) { + true + } else if (!isValidTemplateId(value)) { + true + } else { + false + } + template = template.copy(id = value) + } + TextEdit( + label = stringResource(R.string.module_author), + text = template.author + ) { value -> + template.copy(author = value).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this } } - template = this - } - } - RootProfileConfig(fixedName = true, - profile = toNativeProfile(template), - onProfileChange = { - template.copy( - uid = it.uid, - gid = it.gid, - groups = it.groups, - capabilities = it.capabilities, - context = it.context, - namespace = it.namespace, - rules = it.rules.split("\n") - ).run { - if (autoSave) { - if (!saveTemplate(this)) { - // failed - return@run + TextEdit( + label = stringResource(id = R.string.app_profile_template_description), + text = template.description + ) { value -> + template.copy(description = value).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } } + template = this } - template = this } - }) + + RootProfileConfig( + fixedName = true, + profile = toNativeProfile(template), + onProfileChange = { + template.copy( + uid = it.uid, + gid = it.gid, + groups = it.groups, + capabilities = it.capabilities, + context = it.context, + namespace = it.namespace, + rules = it.rules.split("\n") + ).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this + } + } + ) + } + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) + } } } } fun toNativeProfile(templateInfo: TemplateViewModel.TemplateInfo): Natives.Profile { - return Natives.Profile().copy(rootTemplate = templateInfo.id, + return Natives.Profile().copy( + rootTemplate = templateInfo.id, uid = templateInfo.uid, gid = templateInfo.gid, groups = templateInfo.groups, @@ -234,6 +275,10 @@ fun isTemplateValid(template: TemplateViewModel.TemplateInfo): Boolean { return true } +fun idCheck(value: String): Int { + return if (value.isEmpty()) 0 else if (isTemplateExist(value)) 1 else if (!isValidTemplateId(value)) 2 else 0 +} + fun saveTemplate(template: TemplateViewModel.TemplateInfo, isCreation: Boolean = false): Boolean { if (!isTemplateValid(template)) { return false @@ -248,50 +293,54 @@ fun saveTemplate(template: TemplateViewModel.TemplateInfo, isCreation: Boolean = return setAppProfileTemplate(template.id, json.toString()) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( title: String, readOnly: Boolean, - summary: String = "", onBack: () -> Unit, onDelete: () -> Unit = {}, onSave: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null + scrollBehavior: ScrollBehavior, ) { TopAppBar( - title = { - Column { - Text(title) - if (summary.isNotBlank()) { - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - ) - } - } - }, navigationIcon = { + title = title, + navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } - }, actions = { + ) { + Icon( + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } + }, + actions = { if (readOnly) { return@TopAppBar } - IconButton(onClick = onDelete) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onDelete + ) { Icon( - Icons.Filled.DeleteForever, - contentDescription = stringResource(id = R.string.app_profile_template_delete) + imageVector = MiuixIcons.Useful.Delete, + contentDescription = stringResource(id = R.string.app_profile_template_delete), + tint = colorScheme.onBackground ) } - IconButton(onClick = onSave) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onSave + ) { Icon( - imageVector = Icons.Filled.Save, - contentDescription = stringResource(id = R.string.app_profile_template_save) + imageVector = MiuixIcons.Useful.Confirm, + contentDescription = stringResource(id = R.string.app_profile_template_save), + tint = colorScheme.onBackground ) } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } @@ -300,35 +349,22 @@ private fun TopBar( private fun TextEdit( label: String, text: String, - errorHint: String = "", isError: Boolean = false, onValueChange: (String) -> Unit = {} ) { - ListItem(headlineContent = { - val keyboardController = LocalSoftwareKeyboardController.current - OutlinedTextField( - value = text, - modifier = Modifier.fillMaxWidth(), - label = { Text(label) }, - suffix = { - if (errorHint.isNotBlank()) { - Text( - text = if (isError) errorHint else "", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - }, - isError = isError, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Next - ), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - }), - onValueChange = onValueChange - ) - }) + val editText = remember { mutableStateOf(text) } + EditText( + title = label.uppercase(), + textValue = editText, + onTextValueChange = { newText -> + editText.value = newText + onValueChange(newText) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, + ), + isError = isError, + ) } private fun isValidTemplateId(id: String): Boolean { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Color.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Color.kt deleted file mode 100644 index 155cf4e248bc..000000000000 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Color.kt +++ /dev/null @@ -1,10 +0,0 @@ -package me.weishu.kernelsu.ui.theme - -import androidx.compose.ui.graphics.Color - -val YELLOW = Color(0xFFeed502) -val YELLOW_LIGHT = Color(0xFFffff52) -val SECONDARY_LIGHT = Color(0xffa9817f) - -val YELLOW_DARK = Color(0xFFb7a400) -val SECONDARY_DARK = Color(0xFF4c2b2b) \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt index 903ee94e0b54..42d41730fbd9 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt @@ -1,46 +1,22 @@ package me.weishu.kernelsu.ui.theme -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val DarkColorScheme = darkColorScheme( - primary = YELLOW, - secondary = YELLOW_DARK, - tertiary = SECONDARY_DARK -) - -private val LightColorScheme = lightColorScheme( - primary = YELLOW, - secondary = YELLOW_LIGHT, - tertiary = SECONDARY_LIGHT -) +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.darkColorScheme +import top.yukonga.miuix.kmp.theme.lightColorScheme @Composable fun KernelSUTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme - else -> LightColorScheme + darkTheme -> darkColorScheme() + else -> lightColorScheme() } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, + MiuixTheme( + colors = colorScheme, content = content ) } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Type.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Type.kt deleted file mode 100644 index e8d73313a330..000000000000 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Type.kt +++ /dev/null @@ -1,33 +0,0 @@ -package me.weishu.kernelsu.ui.theme - -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = androidx.compose.material3.Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt deleted file mode 100644 index c1b57483a02f..000000000000 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt +++ /dev/null @@ -1,8 +0,0 @@ -package me.weishu.kernelsu.ui.util - -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.compositionLocalOf - -val LocalSnackbarHost = compositionLocalOf { - error("CompositionLocal LocalSnackbarController not present") -} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/HyperlinkText.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/HyperlinkText.kt deleted file mode 100644 index 4473b828aa1a..000000000000 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/HyperlinkText.kt +++ /dev/null @@ -1,87 +0,0 @@ -package me.weishu.kernelsu.ui.util - -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextDecoration -import java.util.regex.Pattern - -@Composable -fun LinkifyText( - text: String, - modifier: Modifier = Modifier -) { - val uriHandler = LocalUriHandler.current - val layoutResult = remember { - mutableStateOf(null) - } - val linksList = extractUrls(text) - val annotatedString = buildAnnotatedString { - append(text) - linksList.forEach { - addStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ), - start = it.start, - end = it.end - ) - addStringAnnotation( - tag = "URL", - annotation = it.url, - start = it.start, - end = it.end - ) - } - } - Text( - text = annotatedString, - modifier = modifier.pointerInput(Unit) { - detectTapGestures { offsetPosition -> - layoutResult.value?.let { - val position = it.getOffsetForPosition(offsetPosition) - annotatedString.getStringAnnotations(position, position).firstOrNull() - ?.let { result -> - if (result.tag == "URL") { - uriHandler.openUri(result.item) - } - } - } - } - }, - onTextLayout = { layoutResult.value = it } - ) -} - -private val urlPattern: Pattern = Pattern.compile( - "(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" - + "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*" - + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)", - Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL -) - -private data class LinkInfo( - val url: String, - val start: Int, - val end: Int -) - -private fun extractUrls(text: String): List = buildList { - val matcher = urlPattern.matcher(text) - while (matcher.find()) { - val matchStart = matcher.start(1) - val matchEnd = matcher.end() - val url = text.substring(matchStart, matchEnd).replaceFirst("http://", "https://") - add(LinkInfo(url, matchStart, matchEnd)) - } -} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt index 374b3853ab1a..6b8ca488f2bd 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt @@ -1,7 +1,7 @@ package me.weishu.kernelsu.ui.util.module data class LatestVersionInfo( - val versionCode : Int = 0, - val downloadUrl : String = "", - val changelog : String = "" + val versionCode: Int = 0, + val downloadUrl: String = "", + val changelog: String = "" ) diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt index 55f863ce1835..b151ea34d293 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt @@ -41,13 +41,6 @@ class ModuleViewModel : ViewModel() { val hasActionScript: Boolean, ) - data class ModuleUpdateInfo( - val version: String, - val versionCode: Int, - val zipUrl: String, - val changelog: String, - ) - var isRefreshing by mutableStateOf(false) private set var search by mutableStateOf("") @@ -143,8 +136,8 @@ class ModuleViewModel : ViewModel() { val url = m.updateJson Log.i(TAG, "checkUpdate url: $url") val response = ksuApp.okhttpClient.newCall( - okhttp3.Request.Builder().url(url).build() - ).execute() + okhttp3.Request.Builder().url(url).build() + ).execute() Log.d(TAG, "checkUpdate code: ${response.code}") if (response.isSuccessful) { response.body?.string() ?: "" diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt index ecb5024aaed0..c33fadccaa36 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt @@ -9,7 +9,7 @@ import android.os.IBinder import android.os.Parcelable import android.os.SystemClock import android.util.Log -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -22,10 +22,11 @@ import me.weishu.kernelsu.IKsuInterface import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.KsuService +import me.weishu.kernelsu.ui.component.SearchStatus import me.weishu.kernelsu.ui.util.HanziToPinyin import me.weishu.kernelsu.ui.util.KsuCli import java.text.Collator -import java.util.* +import java.util.Locale import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -36,6 +37,12 @@ class SuperUserViewModel : ViewModel() { private var apps by mutableStateOf>(emptyList()) } + + private var _appList = mutableStateOf>(emptyList()) + val appList: State> = _appList + private val _searchStatus = mutableStateOf(SearchStatus("")) + val searchStatus: State = _searchStatus + @Parcelize data class AppInfo( val label: String, @@ -63,35 +70,46 @@ class SuperUserViewModel : ViewModel() { } } - var search by mutableStateOf("") var showSystemApps by mutableStateOf(false) var isRefreshing by mutableStateOf(false) private set - private val sortedList by derivedStateOf { - val comparator = compareBy { - when { - it.allowSu -> 0 - it.hasCustomProfile -> 1 - else -> 2 + private val _searchResults = mutableStateOf>(emptyList()) + val searchResults: State> = _searchResults + + suspend fun updateSearchText(text: String) { + _searchStatus.value.searchText = text + + if (text.isEmpty()) { + _searchStatus.value.resultStatus = SearchStatus.ResultStatus.DEFAULT + _searchResults.value = emptyList() + return + } + + val result = withContext(Dispatchers.Default) { + _searchStatus.value.resultStatus = SearchStatus.ResultStatus.LOAD + _appList.value.filter { + it.label.contains(_searchStatus.value.searchText, true) || it.packageName.contains( + _searchStatus.value.searchText, + true + ) || HanziToPinyin.getInstance().toPinyinString(it.label) + .contains(_searchStatus.value.searchText, true) } - }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) - apps.sortedWith(comparator).also { - isRefreshing = false } - } - val appList by derivedStateOf { - sortedList.filter { - it.label.contains(search, true) || it.packageName.contains( - search, - true - ) || HanziToPinyin.getInstance() - .toPinyinString(it.label).contains(search, true) - }.filter { - it.uid == 2000 // Always show shell - || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 + if (_searchResults.value == result) { + fetchAppList() + updateSearchText(text) + } else { + _searchResults.value = result + + } + _searchStatus.value.resultStatus = if (result.isEmpty()) { + SearchStatus.ResultStatus.EMPTY + } else { + SearchStatus.ResultStatus.SHOW } + } private suspend inline fun connectKsuService( @@ -154,6 +172,21 @@ class SuperUserViewModel : ViewModel() { profile = profile, ) }.filter { it.packageName != ksuApp.packageName } + + + val comparator = compareBy { + when { + it.allowSu -> 0 + it.hasCustomProfile -> 1 + else -> 2 + } + }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) + _appList.value = apps.sortedWith(comparator).also { + isRefreshing = false + }.filter { + it.uid == 2000 // Always show shell + || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 + } Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}") } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt index e825d5e9fbaf..4c7caed81b6c 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt @@ -212,11 +212,11 @@ private fun getLocaleString(json: JSONObject, key: String): String { val localeKey = "${locale.language}_${locale.country}" json.optJSONObject("locales")?.let { // check locale first - it.optJSONObject(localeKey)?.let { json-> + it.optJSONObject(localeKey)?.let { json -> return json.optString(key, fallback) } // fallback to language - it.optJSONObject(locale.language)?.let { json-> + it.optJSONObject(locale.language)?.let { json -> return json.optString(key, fallback) } } @@ -274,8 +274,9 @@ fun TemplateViewModel.TemplateInfo.toJSON(): JSONObject { put("gid", template.gid) if (template.groups.isNotEmpty()) { - put("groups", JSONArray( - Groups.entries.filter { + put( + "groups", JSONArray( + Groups.entries.filter { template.groups.contains(it.gid) }.map { it.name @@ -284,8 +285,9 @@ fun TemplateViewModel.TemplateInfo.toJSON(): JSONObject { } if (template.capabilities.isNotEmpty()) { - put("capabilities", JSONArray( - Capabilities.entries.filter { + put( + "capabilities", JSONArray( + Capabilities.entries.filter { template.capabilities.contains(it.cap) }.map { it.name diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 81621391f936..04b5e87340e1 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -53,7 +53,7 @@ 了解如何安装 KernelSU 以及如何开发模块 支持开发 KernelSU 将保持免费开源,向开发者捐赠以表示支持。 - 加入我们的 %2$s 频道
加入我们的 QQ 频道]]>
+ 加入我们的 %2$s 频道
加入我们的 QQ 频道]]>
默认 模版 自定义 @@ -138,4 +138,9 @@ 临时禁止任何应用通过 su 命令获取 root 权限(已运行的 root 进程不受影响) 将安装以下模块:%1$s 确认 + 处理中… + 下拉刷新 + 松开刷新 + 正在刷新… + 刷新成功 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index b924c0b5ced7..fd7ca63a5cbf 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -121,4 +121,9 @@ 將模組所在的稀疏影像調整為實際大小。 請注意,這可能會導致模組工作異常,因此請僅在必要時使用(例如備份) 解除安裝 保存日志 + 處理中… + 下拉刷新 + 鬆開刷新 + 正在刷新… + 刷新成功 diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index e2461e17ccec..03ae16c50502 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -131,4 +131,9 @@ 儲存運作日誌 執行 已儲存運作日誌 + 處理中… + 下拉刷新 + 鬆開刷新 + 正在刷新… + 刷新成功 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 995738f8b85e..0e300c708bfb 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -140,4 +140,9 @@ Logs saved Disable su compatibility Temporarily disable the ability of any app to gain root privileges via the ⁠su command (existing root processes won\'t be affected). - + Processing… + Pull down to refresh + Release to refresh + Refreshing… + Refreshed successfully + \ No newline at end of file diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 5e8065241672..9fb68b1bd5b6 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -1,7 +1,6 @@ import com.android.build.api.dsl.ApplicationDefaultConfig import com.android.build.api.dsl.CommonExtension import com.android.build.gradle.api.AndroidBasePlugin -import java.io.ByteArrayOutputStream plugins { alias(libs.plugins.agp.app) apply false @@ -28,8 +27,8 @@ cmaker { } val androidMinSdkVersion = 26 -val androidTargetSdkVersion = 35 -val androidCompileSdkVersion = 35 +val androidTargetSdkVersion = 36 +val androidCompileSdkVersion = 36 val androidCompileNdkVersion = "28.0.13004108" val androidSourceCompatibility = JavaVersion.VERSION_21 val androidTargetCompatibility = JavaVersion.VERSION_21 @@ -37,21 +36,13 @@ val managerVersionCode by extra(getVersionCode()) val managerVersionName by extra(getVersionName()) fun getGitCommitCount(): Int { - val out = ByteArrayOutputStream() - exec { - commandLine("git", "rev-list", "--count", "HEAD") - standardOutput = out - } - return out.toString().trim().toInt() + val process = Runtime.getRuntime().exec(arrayOf("git", "rev-list", "--count", "HEAD")) + return process.inputStream.bufferedReader().use { it.readText().trim().toInt() } } fun getGitDescribe(): String { - val out = ByteArrayOutputStream() - exec { - commandLine("git", "describe", "--tags", "--always") - standardOutput = out - } - return out.toString().trim() + val process = Runtime.getRuntime().exec(arrayOf("git", "describe", "--tags", "--always")) + return process.inputStream.bufferedReader().use { it.readText().trim() } } fun getVersionCode(): Int { diff --git a/manager/gradle.properties b/manager/gradle.properties index 387da396166f..980cafa3f4a9 100644 --- a/manager/gradle.properties +++ b/manager/gradle.properties @@ -1,3 +1,8 @@ android.experimental.enableNewResourceShrinker.preciseShrinking=true android.enableAppCompileTimeRClass=true android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048m +org.gradle.parallel=true +org.gradle.vfs.watch=true +android.r8.maxWorkers=4 +android.native.buildOutput=verbose diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index 12a0c142fb25..27b7dfc6fb5c 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,22 +1,22 @@ [versions] -agp = "8.10.1" -kotlin = "2.1.21" -ksp = "2.1.21-2.0.2" -compose-bom = "2025.06.00" -lifecycle = "2.9.1" -navigation = "2.9.0" +agp = "8.12.1" +kotlin = "2.2.10" +ksp = "2.2.10-2.0.2" +compose-bom = "2025.08.00" +lifecycle = "2.9.2" +navigation = "2.9.3" activity-compose = "1.10.1" kotlinx-coroutines = "1.10.2" coil-compose = "2.7.0" compose-destination = "2.2.0" -sheets-compose-dialogs = "1.3.0" markdown = "4.6.2" webkit = "1.14.0" -appiconloader-coil = "1.5.0" parcelablelist = "2.0.1" libsu = "6.0.0" apksign = "1.4" cmaker = "1.2" +miuix = "0.5.1" +haze = "1.6.10" [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } @@ -37,8 +37,6 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } -androidx-compose-material = { group = "androidx.compose.material", name = "material" } -androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } @@ -60,15 +58,13 @@ io-coil-kt-coil-compose = { group = "io.coil-kt", name = "coil-compose", version kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } -me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version.ref = "appiconloader-coil" } - compose-destinations-core = { group = "io.github.raamcosta.compose-destinations", name = "core", version.ref = "compose-destination" } compose-destinations-ksp = { group = "io.github.raamcosta.compose-destinations", name = "ksp", version.ref = "compose-destination" } -sheet-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets-compose-dialogs" } -sheet-compose-dialogs-list = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "list", version.ref = "sheets-compose-dialogs" } -sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "input", version.ref = "sheets-compose-dialogs" } - markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } -lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "28.1.13356709" } \ No newline at end of file +lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "28.1.13356709" } + +miuix = { module = "top.yukonga.miuix.kmp:miuix", version.ref = "miuix" } + +haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } \ No newline at end of file diff --git a/manager/gradle/wrapper/gradle-wrapper.jar b/manager/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d6..8bdaf60c75ab 100644 Binary files a/manager/gradle/wrapper/gradle-wrapper.jar and b/manager/gradle/wrapper/gradle-wrapper.jar differ diff --git a/manager/gradle/wrapper/gradle-wrapper.properties b/manager/gradle/wrapper/gradle-wrapper.properties index c1048ec017ce..2a84e188b85a 100644 --- a/manager/gradle/wrapper/gradle-wrapper.properties +++ b/manager/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/manager/gradlew b/manager/gradlew index f3b75f3b0d4f..ef07e0162b18 100755 --- a/manager/gradlew +++ b/manager/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/manager/gradlew.bat b/manager/gradlew.bat index 9d21a21834d5..db3a6ac207e5 100644 --- a/manager/gradlew.bat +++ b/manager/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/manager/sign.example.properties b/manager/sign.example.properties index bc70a60cae89..7e73c02db92d 100644 --- a/manager/sign.example.properties +++ b/manager/sign.example.properties @@ -1,4 +1,4 @@ KEYSTORE_FILE= KEYSTORE_PASSWORD= KEY_ALIAS= -KEY_PASSWORD= +KEY_PASSWORD= \ No newline at end of file