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 { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-parcelize")
kotlin("android") kotlin("android")
} }

View File

@ -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) },

View File

@ -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)
)
}
} }
} }
} }

View File

@ -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)

View File

@ -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)) }
)
} }

View File

@ -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() {
} }

View File

@ -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)

View File

@ -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)
} }
} }

View File

@ -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
)
}
}
}
}

View File

@ -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

View File

@ -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]!!
} }

View File

@ -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>