Minimum availability

This commit is contained in:
Nullptr 2022-02-13 23:43:52 +08:00
parent f7be0567eb
commit d593107e04
12 changed files with 201 additions and 95 deletions

View File

@ -13,6 +13,7 @@ val composeVersion = "1.2.0-alpha03"
plugins {
id("com.android.application")
id("kotlin-parcelize")
kotlin("android")
}

View File

@ -81,7 +81,7 @@ private fun MainNavHost(navController: NavHostController, modifier: Modifier) {
@Composable
private fun MainNavigationBar(page: PageList, onClick: (PageList) -> Unit) {
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(
selected = page == it,
onClick = { onClick(it) },

View File

@ -5,9 +5,7 @@ import android.graphics.drawable.GradientDrawable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -18,8 +16,7 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.lsposed.lspatch.ui.theme.LSPTheme
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun AppItem(
modifier: Modifier = Modifier,
@ -28,7 +25,8 @@ fun AppItem(
packageName: String,
additionalInfo: (@Composable () -> Unit)? = null,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null
onLongClick: (() -> Unit)? = null,
checked: Boolean? = null
) {
Column(
modifier = modifier
@ -39,19 +37,28 @@ fun AppItem(
)
.padding(20.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = rememberDrawablePainter(icon),
contentDescription = label,
modifier = Modifier.size(32.dp),
tint = Color.Unspecified
)
Column(Modifier.padding(start = 20.dp)) {
Column(Modifier.weight(1f)) {
Text(text = label, style = MaterialTheme.typography.bodyMedium)
Text(text = packageName, style = MaterialTheme.typography.bodySmall)
additionalInfo?.invoke()
}
if (checked != null) {
Checkbox(
checked = checked,
onCheckedChange = { onClick() },
modifier = Modifier.padding(start = 20.dp)
)
}
}
}
}

View File

@ -23,14 +23,15 @@ fun SettingsSlot(
extraContent: (@Composable ColumnScope.() -> Unit)? = null,
action: (@Composable RowScope.() -> Unit)?,
) {
Row(
modifier = modifier
.fillMaxWidth() then (
val enabledModifier =
if (enabled) Modifier
.alpha(1f)
.clickable(onClick = onClick)
else Modifier.alpha(0.5f)
)
Row(
modifier = modifier
.fillMaxWidth()
.then(enabledModifier)
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
@ -54,7 +55,9 @@ fun SettingsSlot(
Text(
text = desc,
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)

View File

@ -1,6 +1,10 @@
package org.lsposed.lspatch.ui.page
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import org.lsposed.lspatch.R
@Composable
fun HomePage() {
@ -9,5 +13,7 @@ fun HomePage() {
@Composable
fun HomeTopBar() {
SmallTopAppBar(
title = { Text(stringResource(R.string.app_name)) }
)
}

View File

@ -12,21 +12,21 @@ import org.lsposed.lspatch.R
import org.lsposed.lspatch.ui.util.LocalNavController
@Composable
fun PatchesTopBar() {
fun ManageTopBar() {
SmallTopAppBar(
title = { Text(PageList.Patches.title) }
title = { Text(PageList.Manage.title) }
)
}
@Composable
fun PatchesFab() {
fun ManageFab() {
val navController = LocalNavController.current
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
fun PatchesPage() {
fun ManagePage() {
}

View File

@ -20,8 +20,8 @@ import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material.icons.outlined.WorkOutline
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.isScrolledToEnd
import org.lsposed.lspatch.ui.util.lastItemIndex
import org.lsposed.lspatch.ui.util.observeState
import org.lsposed.lspatch.ui.viewmodel.AppInfo
import org.lsposed.patch.util.Logger
@ -68,8 +69,7 @@ fun NewPatchFab() {
fun NewPatchPage() {
val viewModel = viewModel<NewPatchPageViewModel>()
val navController = LocalNavController.current
val patchApp by navController.currentBackStackEntry!!.savedStateHandle
.getLiveData<AppInfo>("appInfo").observeAsState()
val patchApp by navController.currentBackStackEntry!!.observeState<AppInfo>("appInfo")
if (viewModel.patchState == PatchState.SELECTING && patchApp != null) viewModel.patchState = PatchState.CONFIGURING
Log.d(TAG, "NewPatchPage: ${viewModel.patchState}")
@ -83,6 +83,7 @@ fun NewPatchPage() {
@Composable
private fun PatchOptionsPage(patchApp: AppInfo) {
val viewModel = viewModel<NewPatchPageViewModel>()
val navController = LocalNavController.current
var useManager by rememberSaveable { mutableStateOf(true) }
var debuggable 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) }
val sigBypassLevel by rememberSaveable { mutableStateOf(2) }
var overrideVersionCode by rememberSaveable { mutableStateOf(false) }
val embeddedModules = navController.currentBackStackEntry!!
.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("selected", SnapshotStateList())
if (viewModel.patchState == PatchState.SUBMITTING) LaunchedEffect(patchApp) {
val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
if (useManager) embeddedModules.value?.clear()
viewModel.patchOptions = Patcher.Options(
apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk
outputPath = downloadDir,
@ -102,7 +106,7 @@ private fun PatchOptionsPage(patchApp: AppInfo) {
useManager = useManager,
overrideVersionCode = overrideVersionCode,
verbose = true,
embeddedModules = emptyList() // TODO: Embed modules
embeddedModules = embeddedModules.value?.map { it.app.sourceDir } ?: emptyList() // TODO: Split Apk
)
viewModel.patchState = PatchState.PATCHING
}
@ -145,7 +149,7 @@ private fun PatchOptionsPage(patchApp: AppInfo) {
desc = stringResource(R.string.patch_portable_desc),
extraContent = {
TextButton(
onClick = { /* TODO */ }
onClick = { navController.navigate(PageList.SelectApps.name + "/true") }
) {
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 } }
ShimmerAnimation(enabled = patching) {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace)
LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)
) {
val scrollState = rememberLazyListState()
LazyColumn(
state = scrollState,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp)
.heightIn(max = 320.dp)
.clip(RoundedCornerShape(32.dp))
.background(brush)
.padding(horizontal = 24.dp, vertical = 18.dp)

View File

@ -1,14 +1,8 @@
package org.lsposed.lspatch.ui.page
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.GetApp
import androidx.compose.material.icons.filled.Healing
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.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
@ -32,11 +26,11 @@ enum class PageList(
topBar = { HomeTopBar() },
body = { HomePage() }
),
Patches(
iconSelected = Icons.Filled.Healing,
iconNotSelected = Icons.Outlined.Healing,
topBar = { PatchesTopBar() },
fab = { PatchesFab() },
Manage(
iconSelected = Icons.Filled.Dashboard,
iconNotSelected = Icons.Outlined.Dashboard,
topBar = { ManageTopBar() },
fab = { ManageFab() },
body = {}
),
Repo(
@ -60,17 +54,18 @@ enum class PageList(
arguments = listOf(
navArgument("multiSelect") { type = NavType.BoolType }
),
topBar = {},
topBar = { SelectAppsTopBar() },
fab = { SelectAppsFab() },
body = { SelectAppsPage(this) }
);
val title: String
@Composable get() = when (this) {
Home -> stringResource(R.string.app_name)
Patches -> stringResource(R.string.page_patches)
Manage -> stringResource(R.string.page_manage)
Repo -> stringResource(R.string.page_repo)
Settings -> stringResource(R.string.page_settings)
NewPatch -> stringResource(R.string.page_new_patch)
SelectApps -> stringResource(R.string.page_select_app)
SelectApps -> stringResource(R.string.page_select_apps)
}
}

View File

@ -1,60 +1,90 @@
package org.lsposed.lspatch.ui.page
import android.content.pm.ApplicationInfo
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import org.lsposed.lspatch.R
import org.lsposed.lspatch.ui.component.AppItem
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.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
fun SelectAppsPage(entry: NavBackStackEntry) {
val multiSelect = entry.arguments?.get("multiSelect") as? Boolean
?: throw IllegalArgumentException("multiSelect is null")
if (multiSelect) {
TODO("MultiSelect")
MultiSelect(filter = { it.app.metaData?.get("xposedminversion") != null })
} else {
SelectSingle()
SingleSelect()
}
}
@Composable
private fun SelectSingle(
filter: (AppInfo) -> Boolean = { true }
private fun SingleSelect(
filter: (AppInfo) -> Boolean = { it.app.flags and ApplicationInfo.FLAG_SYSTEM == 0 }
) {
val context = LocalContext.current
val navController = LocalNavController.current
val viewModel = viewModel<SelectAppViewModel>()
val isRefreshing by viewModel.isRefreshing.collectAsState()
if (SelectAppViewModel.appList.isEmpty())
val viewModel = viewModel<SelectAppsViewModel>()
LaunchedEffect(viewModel) {
viewModel.loadAppList(context)
viewModel.filterAppList(context, false, filter)
}
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.loadAppList(context) },
state = rememberSwipeRefreshState(viewModel.isRefreshing),
onRefresh = { viewModel.filterAppList(context, true, filter) },
modifier = Modifier.fillMaxSize()
) {
LazyColumn {
items(SelectAppViewModel.appList) {
items(viewModel.filteredList) {
AppItem(
icon = it.icon,
icon = viewModel.getIcon(it),
label = it.label,
packageName = it.app.packageName,
onClick = {
navController.previousBackStackEntry!!.savedStateHandle.getLiveData<AppInfo>("appInfo").value = it
navController.previousBackStackEntry!!.setState("appInfo", it)
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
)
}
}
}
}

View File

@ -2,6 +2,8 @@ package org.lsposed.lspatch.ui.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.livedata.observeAsState
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
@ -17,6 +19,13 @@ val NavController.currentRoute: String?
val NavController.startRoute: String?
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
fun NavController.isAtStartRoute(): Boolean = currentRoute == startRoute

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Parcelable
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -11,46 +12,54 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.withContext
import kotlinx.parcelize.Parcelize
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 {
Log.d(TAG, "SelectAppViewModel ${toString().substringAfterLast('@')} construct")
Log.d(TAG, "SelectAppsViewModel ${toString().substringAfterLast('@')} construct")
}
companion object {
var appList by mutableStateOf(listOf<AppInfo>())
var done by mutableStateOf(false)
var isRefreshing by mutableStateOf(false)
private set
}
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean>
get() = _isRefreshing.asStateFlow()
var filteredList by mutableStateOf(listOf<AppInfo>())
private set
fun loadAppList(context: Context) {
viewModelScope.launch {
private suspend fun refreshAppList(context: Context) {
Log.d(TAG, "Start refresh apps")
_isRefreshing.emit(true)
isRefreshing = true
val pm = context.packageManager
val collection = mutableListOf<AppInfo>()
withContext(Dispatchers.IO) {
pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach {
val icon = pm.getApplicationIcon(it)
val label = pm.getApplicationLabel(it)
collection.add(AppInfo(it, icon, label.toString()))
appIcon[it.packageName] = pm.getApplicationIcon(it)
collection.add(AppInfo(it, label.toString()))
}
}
appList = collection
_isRefreshing.emit(false)
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]!!
}

View File

@ -1,13 +1,12 @@
<resources>
<string name="app_name">LSPatch</string>
<string name="add">Add</string>
<string name="page_repo">Repo</string>
<string name="page_settings">Settings</string>
<string name="page_select_app">Select App</string>
<!-- Patches Page -->
<string name="page_patches">Patches</string>
<string name="patches_add">Add</string>
<!-- Manage Page -->
<string name="page_manage">Manage</string>
<!-- New Patch Page -->
<string name="page_new_patch">New Patch</string>
@ -28,4 +27,7 @@
<string name="patch_start">Start Patch</string>
<string name="patch_return">Return</string>
<string name="patch_install">Install</string>
<!-- Select Apps Page -->
<string name="page_select_apps">Select Apps</string>
</resources>