Minimum availability
This commit is contained in:
parent
f7be0567eb
commit
d593107e04
|
|
@ -13,6 +13,7 @@ val composeVersion = "1.2.0-alpha03"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
id("kotlin-parcelize")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ private fun MainNavHost(navController: NavHostController, modifier: Modifier) {
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainNavigationBar(page: PageList, onClick: (PageList) -> Unit) {
|
private fun MainNavigationBar(page: PageList, onClick: (PageList) -> Unit) {
|
||||||
NavigationBar(tonalElevation = 8.dp) {
|
NavigationBar(tonalElevation = 8.dp) {
|
||||||
arrayOf(PageList.Home, PageList.Patches, PageList.Repo, PageList.Settings).forEach {
|
arrayOf(PageList.Home, PageList.Manage, PageList.Repo, PageList.Settings).forEach {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = page == it,
|
selected = page == it,
|
||||||
onClick = { onClick(it) },
|
onClick = { onClick(it) },
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ import android.graphics.drawable.GradientDrawable
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -18,8 +16,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
import org.lsposed.lspatch.ui.theme.LSPTheme
|
import org.lsposed.lspatch.ui.theme.LSPTheme
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppItem(
|
fun AppItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -28,7 +25,8 @@ fun AppItem(
|
||||||
packageName: String,
|
packageName: String,
|
||||||
additionalInfo: (@Composable () -> Unit)? = null,
|
additionalInfo: (@Composable () -> Unit)? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: (() -> Unit)? = null
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
checked: Boolean? = null
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -39,19 +37,28 @@ fun AppItem(
|
||||||
)
|
)
|
||||||
.padding(20.dp)
|
.padding(20.dp)
|
||||||
) {
|
) {
|
||||||
|
Row(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = rememberDrawablePainter(icon),
|
painter = rememberDrawablePainter(icon),
|
||||||
contentDescription = label,
|
contentDescription = label,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(32.dp),
|
||||||
tint = Color.Unspecified
|
tint = Color.Unspecified
|
||||||
)
|
)
|
||||||
Column(Modifier.padding(start = 20.dp)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(text = label, style = MaterialTheme.typography.bodyMedium)
|
Text(text = label, style = MaterialTheme.typography.bodyMedium)
|
||||||
Text(text = packageName, style = MaterialTheme.typography.bodySmall)
|
Text(text = packageName, style = MaterialTheme.typography.bodySmall)
|
||||||
additionalInfo?.invoke()
|
additionalInfo?.invoke()
|
||||||
}
|
}
|
||||||
|
if (checked != null) {
|
||||||
|
Checkbox(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = { onClick() },
|
||||||
|
modifier = Modifier.padding(start = 20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,15 @@ fun SettingsSlot(
|
||||||
extraContent: (@Composable ColumnScope.() -> Unit)? = null,
|
extraContent: (@Composable ColumnScope.() -> Unit)? = null,
|
||||||
action: (@Composable RowScope.() -> Unit)?,
|
action: (@Composable RowScope.() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
|
val enabledModifier =
|
||||||
|
if (enabled) Modifier
|
||||||
|
.alpha(1f)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
else Modifier.alpha(0.5f)
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth() then (
|
.fillMaxWidth()
|
||||||
if (enabled) Modifier
|
.then(enabledModifier)
|
||||||
.alpha(1f)
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
else Modifier.alpha(0.5f)
|
|
||||||
)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|
@ -54,7 +55,9 @@ fun SettingsSlot(
|
||||||
Text(
|
Text(
|
||||||
text = desc,
|
text = desc,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.alpha(0.75f).padding(top = 4.dp)
|
modifier = Modifier
|
||||||
|
.alpha(0.75f)
|
||||||
|
.padding(top = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
extraContent?.invoke(this)
|
extraContent?.invoke(this)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package org.lsposed.lspatch.ui.page
|
package org.lsposed.lspatch.ui.page
|
||||||
|
|
||||||
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import org.lsposed.lspatch.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage() {
|
fun HomePage() {
|
||||||
|
|
@ -9,5 +13,7 @@ fun HomePage() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar() {
|
fun HomeTopBar() {
|
||||||
|
SmallTopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.app_name)) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,21 +12,21 @@ import org.lsposed.lspatch.R
|
||||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesTopBar() {
|
fun ManageTopBar() {
|
||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
title = { Text(PageList.Patches.title) }
|
title = { Text(PageList.Manage.title) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesFab() {
|
fun ManageFab() {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
FloatingActionButton(onClick = { navController.navigate(PageList.NewPatch.name) }) {
|
FloatingActionButton(onClick = { navController.navigate(PageList.NewPatch.name) }) {
|
||||||
Icon(Icons.Filled.Add, stringResource(R.string.patches_add))
|
Icon(Icons.Filled.Add, stringResource(R.string.add))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesPage() {
|
fun ManagePage() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -20,8 +20,8 @@ import androidx.compose.material.icons.outlined.BugReport
|
||||||
import androidx.compose.material.icons.outlined.WorkOutline
|
import androidx.compose.material.icons.outlined.WorkOutline
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -40,6 +40,7 @@ import org.lsposed.lspatch.ui.component.settings.SettingsItem
|
||||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||||
import org.lsposed.lspatch.ui.util.isScrolledToEnd
|
import org.lsposed.lspatch.ui.util.isScrolledToEnd
|
||||||
import org.lsposed.lspatch.ui.util.lastItemIndex
|
import org.lsposed.lspatch.ui.util.lastItemIndex
|
||||||
|
import org.lsposed.lspatch.ui.util.observeState
|
||||||
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
||||||
import org.lsposed.patch.util.Logger
|
import org.lsposed.patch.util.Logger
|
||||||
|
|
||||||
|
|
@ -68,8 +69,7 @@ fun NewPatchFab() {
|
||||||
fun NewPatchPage() {
|
fun NewPatchPage() {
|
||||||
val viewModel = viewModel<NewPatchPageViewModel>()
|
val viewModel = viewModel<NewPatchPageViewModel>()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val patchApp by navController.currentBackStackEntry!!.savedStateHandle
|
val patchApp by navController.currentBackStackEntry!!.observeState<AppInfo>("appInfo")
|
||||||
.getLiveData<AppInfo>("appInfo").observeAsState()
|
|
||||||
if (viewModel.patchState == PatchState.SELECTING && patchApp != null) viewModel.patchState = PatchState.CONFIGURING
|
if (viewModel.patchState == PatchState.SELECTING && patchApp != null) viewModel.patchState = PatchState.CONFIGURING
|
||||||
|
|
||||||
Log.d(TAG, "NewPatchPage: ${viewModel.patchState}")
|
Log.d(TAG, "NewPatchPage: ${viewModel.patchState}")
|
||||||
|
|
@ -83,6 +83,7 @@ fun NewPatchPage() {
|
||||||
@Composable
|
@Composable
|
||||||
private fun PatchOptionsPage(patchApp: AppInfo) {
|
private fun PatchOptionsPage(patchApp: AppInfo) {
|
||||||
val viewModel = viewModel<NewPatchPageViewModel>()
|
val viewModel = viewModel<NewPatchPageViewModel>()
|
||||||
|
val navController = LocalNavController.current
|
||||||
var useManager by rememberSaveable { mutableStateOf(true) }
|
var useManager by rememberSaveable { mutableStateOf(true) }
|
||||||
var debuggable by rememberSaveable { mutableStateOf(false) }
|
var debuggable by rememberSaveable { mutableStateOf(false) }
|
||||||
var v1 by rememberSaveable { mutableStateOf(false) }
|
var v1 by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -90,9 +91,12 @@ private fun PatchOptionsPage(patchApp: AppInfo) {
|
||||||
var v3 by rememberSaveable { mutableStateOf(true) }
|
var v3 by rememberSaveable { mutableStateOf(true) }
|
||||||
val sigBypassLevel by rememberSaveable { mutableStateOf(2) }
|
val sigBypassLevel by rememberSaveable { mutableStateOf(2) }
|
||||||
var overrideVersionCode by rememberSaveable { mutableStateOf(false) }
|
var overrideVersionCode by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val embeddedModules = navController.currentBackStackEntry!!
|
||||||
|
.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("selected", SnapshotStateList())
|
||||||
|
|
||||||
if (viewModel.patchState == PatchState.SUBMITTING) LaunchedEffect(patchApp) {
|
if (viewModel.patchState == PatchState.SUBMITTING) LaunchedEffect(patchApp) {
|
||||||
val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
|
val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
|
||||||
|
if (useManager) embeddedModules.value?.clear()
|
||||||
viewModel.patchOptions = Patcher.Options(
|
viewModel.patchOptions = Patcher.Options(
|
||||||
apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk
|
apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk
|
||||||
outputPath = downloadDir,
|
outputPath = downloadDir,
|
||||||
|
|
@ -102,7 +106,7 @@ private fun PatchOptionsPage(patchApp: AppInfo) {
|
||||||
useManager = useManager,
|
useManager = useManager,
|
||||||
overrideVersionCode = overrideVersionCode,
|
overrideVersionCode = overrideVersionCode,
|
||||||
verbose = true,
|
verbose = true,
|
||||||
embeddedModules = emptyList() // TODO: Embed modules
|
embeddedModules = embeddedModules.value?.map { it.app.sourceDir } ?: emptyList() // TODO: Split Apk
|
||||||
)
|
)
|
||||||
viewModel.patchState = PatchState.PATCHING
|
viewModel.patchState = PatchState.PATCHING
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +149,7 @@ private fun PatchOptionsPage(patchApp: AppInfo) {
|
||||||
desc = stringResource(R.string.patch_portable_desc),
|
desc = stringResource(R.string.patch_portable_desc),
|
||||||
extraContent = {
|
extraContent = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { /* TODO */ }
|
onClick = { navController.navigate(PageList.SelectApps.name + "/true") }
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge)
|
Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge)
|
||||||
}
|
}
|
||||||
|
|
@ -229,14 +233,14 @@ private fun DoPatchPage(patcherOptions: Patcher.Options) {
|
||||||
val patching by remember { derivedStateOf { viewModel.patchState == PatchState.PATCHING } }
|
val patching by remember { derivedStateOf { viewModel.patchState == PatchState.PATCHING } }
|
||||||
ShimmerAnimation(enabled = patching) {
|
ShimmerAnimation(enabled = patching) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace)
|
LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberLazyListState()
|
val scrollState = rememberLazyListState()
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = scrollState,
|
state = scrollState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 300.dp)
|
.heightIn(max = 320.dp)
|
||||||
.clip(RoundedCornerShape(32.dp))
|
.clip(RoundedCornerShape(32.dp))
|
||||||
.background(brush)
|
.background(brush)
|
||||||
.padding(horizontal = 24.dp, vertical = 18.dp)
|
.padding(horizontal = 24.dp, vertical = 18.dp)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
package org.lsposed.lspatch.ui.page
|
package org.lsposed.lspatch.ui.page
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.GetApp
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Healing
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material.icons.filled.Home
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material.icons.outlined.GetApp
|
|
||||||
import androidx.compose.material.icons.outlined.Healing
|
|
||||||
import androidx.compose.material.icons.outlined.Home
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -32,11 +26,11 @@ enum class PageList(
|
||||||
topBar = { HomeTopBar() },
|
topBar = { HomeTopBar() },
|
||||||
body = { HomePage() }
|
body = { HomePage() }
|
||||||
),
|
),
|
||||||
Patches(
|
Manage(
|
||||||
iconSelected = Icons.Filled.Healing,
|
iconSelected = Icons.Filled.Dashboard,
|
||||||
iconNotSelected = Icons.Outlined.Healing,
|
iconNotSelected = Icons.Outlined.Dashboard,
|
||||||
topBar = { PatchesTopBar() },
|
topBar = { ManageTopBar() },
|
||||||
fab = { PatchesFab() },
|
fab = { ManageFab() },
|
||||||
body = {}
|
body = {}
|
||||||
),
|
),
|
||||||
Repo(
|
Repo(
|
||||||
|
|
@ -60,17 +54,18 @@ enum class PageList(
|
||||||
arguments = listOf(
|
arguments = listOf(
|
||||||
navArgument("multiSelect") { type = NavType.BoolType }
|
navArgument("multiSelect") { type = NavType.BoolType }
|
||||||
),
|
),
|
||||||
topBar = {},
|
topBar = { SelectAppsTopBar() },
|
||||||
|
fab = { SelectAppsFab() },
|
||||||
body = { SelectAppsPage(this) }
|
body = { SelectAppsPage(this) }
|
||||||
);
|
);
|
||||||
|
|
||||||
val title: String
|
val title: String
|
||||||
@Composable get() = when (this) {
|
@Composable get() = when (this) {
|
||||||
Home -> stringResource(R.string.app_name)
|
Home -> stringResource(R.string.app_name)
|
||||||
Patches -> stringResource(R.string.page_patches)
|
Manage -> stringResource(R.string.page_manage)
|
||||||
Repo -> stringResource(R.string.page_repo)
|
Repo -> stringResource(R.string.page_repo)
|
||||||
Settings -> stringResource(R.string.page_settings)
|
Settings -> stringResource(R.string.page_settings)
|
||||||
NewPatch -> stringResource(R.string.page_new_patch)
|
NewPatch -> stringResource(R.string.page_new_patch)
|
||||||
SelectApps -> stringResource(R.string.page_select_app)
|
SelectApps -> stringResource(R.string.page_select_apps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,90 @@
|
||||||
package org.lsposed.lspatch.ui.page
|
package org.lsposed.lspatch.ui.page
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Done
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||||
|
import org.lsposed.lspatch.R
|
||||||
import org.lsposed.lspatch.ui.component.AppItem
|
import org.lsposed.lspatch.ui.component.AppItem
|
||||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||||
|
import org.lsposed.lspatch.ui.util.observeState
|
||||||
|
import org.lsposed.lspatch.ui.util.setState
|
||||||
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
||||||
import org.lsposed.lspatch.ui.viewmodel.SelectAppViewModel
|
import org.lsposed.lspatch.ui.viewmodel.SelectAppsViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectAppsTopBar() {
|
||||||
|
SmallTopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.page_select_apps)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectAppsFab() {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val viewModel = viewModel<SelectAppsViewModel>()
|
||||||
|
val multiSelect = navController.currentBackStackEntry?.arguments?.get("multiSelect") as? Boolean
|
||||||
|
?: throw IllegalArgumentException("multiSelect is null")
|
||||||
|
|
||||||
|
if (multiSelect) {
|
||||||
|
FloatingActionButton(onClick = { viewModel.done = true }) {
|
||||||
|
Icon(Icons.Outlined.Done, stringResource(R.string.add))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectAppsPage(entry: NavBackStackEntry) {
|
fun SelectAppsPage(entry: NavBackStackEntry) {
|
||||||
val multiSelect = entry.arguments?.get("multiSelect") as? Boolean
|
val multiSelect = entry.arguments?.get("multiSelect") as? Boolean
|
||||||
?: throw IllegalArgumentException("multiSelect is null")
|
?: throw IllegalArgumentException("multiSelect is null")
|
||||||
if (multiSelect) {
|
if (multiSelect) {
|
||||||
TODO("MultiSelect")
|
MultiSelect(filter = { it.app.metaData?.get("xposedminversion") != null })
|
||||||
} else {
|
} else {
|
||||||
SelectSingle()
|
SingleSelect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectSingle(
|
private fun SingleSelect(
|
||||||
filter: (AppInfo) -> Boolean = { true }
|
filter: (AppInfo) -> Boolean = { it.app.flags and ApplicationInfo.FLAG_SYSTEM == 0 }
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val viewModel = viewModel<SelectAppViewModel>()
|
val viewModel = viewModel<SelectAppsViewModel>()
|
||||||
val isRefreshing by viewModel.isRefreshing.collectAsState()
|
LaunchedEffect(viewModel) {
|
||||||
if (SelectAppViewModel.appList.isEmpty())
|
viewModel.filterAppList(context, false, filter)
|
||||||
LaunchedEffect(viewModel) {
|
}
|
||||||
viewModel.loadAppList(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
SwipeRefresh(
|
SwipeRefresh(
|
||||||
state = rememberSwipeRefreshState(isRefreshing),
|
state = rememberSwipeRefreshState(viewModel.isRefreshing),
|
||||||
onRefresh = { viewModel.loadAppList(context) },
|
onRefresh = { viewModel.filterAppList(context, true, filter) },
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(SelectAppViewModel.appList) {
|
items(viewModel.filteredList) {
|
||||||
AppItem(
|
AppItem(
|
||||||
icon = it.icon,
|
icon = viewModel.getIcon(it),
|
||||||
label = it.label,
|
label = it.label,
|
||||||
packageName = it.app.packageName,
|
packageName = it.app.packageName,
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.previousBackStackEntry!!.savedStateHandle.getLiveData<AppInfo>("appInfo").value = it
|
navController.previousBackStackEntry!!.setState("appInfo", it)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -62,3 +92,43 @@ private fun SelectSingle(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MultiSelect(
|
||||||
|
filter: (AppInfo) -> Boolean = { true }
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val viewModel = viewModel<SelectAppsViewModel>()
|
||||||
|
val selected by navController.previousBackStackEntry!!.observeState<SnapshotStateList<AppInfo>>("selected")
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel) {
|
||||||
|
viewModel.filterAppList(context, false, filter)
|
||||||
|
}
|
||||||
|
if (viewModel.done) {
|
||||||
|
LaunchedEffect(viewModel) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeRefresh(
|
||||||
|
state = rememberSwipeRefreshState(viewModel.isRefreshing),
|
||||||
|
onRefresh = { viewModel.filterAppList(context, true, filter) },
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
items(viewModel.filteredList) {
|
||||||
|
val checked = selected!!.contains(it)
|
||||||
|
AppItem(
|
||||||
|
icon = viewModel.getIcon(it),
|
||||||
|
label = it.label,
|
||||||
|
packageName = it.app.packageName,
|
||||||
|
onClick = {
|
||||||
|
if (checked) selected!!.remove(it) else selected!!.add(it)
|
||||||
|
},
|
||||||
|
checked = checked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package org.lsposed.lspatch.ui.util
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
|
@ -17,6 +19,13 @@ val NavController.currentRoute: String?
|
||||||
val NavController.startRoute: String?
|
val NavController.startRoute: String?
|
||||||
get() = graph.findStartDestination().route
|
get() = graph.findStartDestination().route
|
||||||
|
|
||||||
|
fun <T> NavBackStackEntry.setState(key: String, value: T?) {
|
||||||
|
savedStateHandle.getLiveData<T>(key).value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> NavBackStackEntry.observeState(key: String, initial: T? = null) = savedStateHandle.getLiveData(key, initial).observeAsState()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavController.isAtStartRoute(): Boolean = currentRoute == startRoute
|
fun NavController.isAtStartRoute(): Boolean = currentRoute == startRoute
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -11,46 +12,54 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.lsposed.lspatch.TAG
|
import org.lsposed.lspatch.TAG
|
||||||
|
|
||||||
class AppInfo(val app: ApplicationInfo, val icon: Drawable, val label: String)
|
@Parcelize
|
||||||
|
class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable
|
||||||
|
|
||||||
class SelectAppViewModel : ViewModel() {
|
private var appList = listOf<AppInfo>()
|
||||||
|
private val appIcon = mutableMapOf<String, Drawable>()
|
||||||
|
|
||||||
|
class SelectAppsViewModel : ViewModel() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(TAG, "SelectAppViewModel ${toString().substringAfterLast('@')} construct")
|
Log.d(TAG, "SelectAppsViewModel ${toString().substringAfterLast('@')} construct")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
var done by mutableStateOf(false)
|
||||||
var appList by mutableStateOf(listOf<AppInfo>())
|
|
||||||
private set
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _isRefreshing = MutableStateFlow(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
val isRefreshing: StateFlow<Boolean>
|
private set
|
||||||
get() = _isRefreshing.asStateFlow()
|
|
||||||
|
|
||||||
fun loadAppList(context: Context) {
|
var filteredList by mutableStateOf(listOf<AppInfo>())
|
||||||
viewModelScope.launch {
|
private set
|
||||||
Log.d(TAG, "Start refresh apps")
|
|
||||||
_isRefreshing.emit(true)
|
private suspend fun refreshAppList(context: Context) {
|
||||||
val pm = context.packageManager
|
Log.d(TAG, "Start refresh apps")
|
||||||
val collection = mutableListOf<AppInfo>()
|
isRefreshing = true
|
||||||
withContext(Dispatchers.IO) {
|
val pm = context.packageManager
|
||||||
pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach {
|
val collection = mutableListOf<AppInfo>()
|
||||||
val icon = pm.getApplicationIcon(it)
|
withContext(Dispatchers.IO) {
|
||||||
val label = pm.getApplicationLabel(it)
|
pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach {
|
||||||
collection.add(AppInfo(it, icon, label.toString()))
|
val label = pm.getApplicationLabel(it)
|
||||||
}
|
appIcon[it.packageName] = pm.getApplicationIcon(it)
|
||||||
|
collection.add(AppInfo(it, label.toString()))
|
||||||
}
|
}
|
||||||
appList = collection
|
}
|
||||||
_isRefreshing.emit(false)
|
appList = collection
|
||||||
Log.d(TAG, "Refreshed ${appList.size} apps")
|
isRefreshing = false
|
||||||
|
Log.d(TAG, "Refreshed ${appList.size} apps")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filterAppList(context: Context, refresh: Boolean, filter: (AppInfo) -> Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (appList.isEmpty() || refresh) refreshAppList(context)
|
||||||
|
filteredList = appList.filter(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getIcon(appInfo: AppInfo) = appIcon[appInfo.app.packageName]!!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">LSPatch</string>
|
<string name="app_name">LSPatch</string>
|
||||||
|
<string name="add">Add</string>
|
||||||
|
|
||||||
<string name="page_repo">Repo</string>
|
<string name="page_repo">Repo</string>
|
||||||
<string name="page_settings">Settings</string>
|
<string name="page_settings">Settings</string>
|
||||||
<string name="page_select_app">Select App</string>
|
|
||||||
|
|
||||||
<!-- Patches Page -->
|
<!-- Manage Page -->
|
||||||
<string name="page_patches">Patches</string>
|
<string name="page_manage">Manage</string>
|
||||||
<string name="patches_add">Add</string>
|
|
||||||
|
|
||||||
<!-- New Patch Page -->
|
<!-- New Patch Page -->
|
||||||
<string name="page_new_patch">New Patch</string>
|
<string name="page_new_patch">New Patch</string>
|
||||||
|
|
@ -28,4 +27,7 @@
|
||||||
<string name="patch_start">Start Patch</string>
|
<string name="patch_start">Start Patch</string>
|
||||||
<string name="patch_return">Return</string>
|
<string name="patch_return">Return</string>
|
||||||
<string name="patch_install">Install</string>
|
<string name="patch_install">Install</string>
|
||||||
|
|
||||||
|
<!-- Select Apps Page -->
|
||||||
|
<string name="page_select_apps">Select Apps</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue