feat: improved ui code

This commit is contained in:
Canny 2022-11-12 18:25:12 +03:00 committed by CnC-Robert
parent 98300cfb57
commit 0a5dfa906f
12 changed files with 221 additions and 188 deletions

View File

@ -11,4 +11,6 @@ val viewModelModule = module {
viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::PatchingScreenViewModel)
viewModelOf(::ContributorsViewModel)
viewModelOf(::PatcherScreenViewModel)
viewModelOf(::SourceSelectorViewModel)
}

View File

@ -38,7 +38,7 @@ class ManagerAPI(
return out
}
suspend fun downloadPatches() = withContext(Dispatchers.Main) {
suspend fun downloadPatches() = withContext(Dispatchers.Default) {
try {
val asset =
if (prefs.srcPatches!! == ghPatches) reVancedAPI.findAsset(ghPatches, ".jar")

View File

@ -8,6 +8,7 @@ import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.viewmodel.PatchClass
import app.revanced.manager.util.tag
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.patchName
@ -19,12 +20,13 @@ import java.util.*
class PatcherUtils(val app: Application) {
val patches = mutableStateOf<Resource<List<Class<out Patch<Context>>>>>(Resource.Loading)
val filteredPatches = mutableStateListOf<PatchClass>()
val selectedAppPackage = mutableStateOf(Optional.empty<ApplicationInfo>())
val selectedPatches = mutableStateListOf<String>()
lateinit var patchBundleFile: String
fun cleanup() {
patches.value = Resource.Success(emptyList())
patches.value = Resource.Loading
selectedAppPackage.value = Optional.empty()
selectedPatches.clear()
}

View File

@ -1,6 +1,12 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material3.*
@ -11,14 +17,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.FloatingActionButton
import app.revanced.manager.ui.component.SplitAPKDialog
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import org.koin.androidx.compose.get
import app.revanced.manager.ui.viewmodel.PatcherScreenViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@ -28,27 +31,17 @@ fun PatcherScreen(
onClickPatchSelector: () -> Unit,
onClickPatch: () -> Unit,
onClickSourceSelector: () -> Unit,
patcherUtils: PatcherUtils = get(),
psvm: PatchesSelectorViewModel = getViewModel(),
managerAPI: ManagerAPI = get()
vm: PatcherScreenViewModel = getViewModel(),
) {
val selectedAmount = patcherUtils.selectedPatches.size
val selectedAppPackage by patcherUtils.selectedAppPackage
val hasAppSelected = selectedAppPackage.isPresent
val patchesLoaded = patcherUtils.patches.value is Resource.Success
var showDialog by remember { mutableStateOf(false) }
LaunchedEffect(patchesLoaded) {
if (!patchesLoaded) {
managerAPI.downloadPatches()
}
}
val hasAppSelected by mutableStateOf(vm.selectedAppPackage.isPresent)
val patchesLoaded by mutableStateOf(vm.patchesLoaded is Resource.Success)
Scaffold(
floatingActionButton = {
FloatingActionButton(
enabled = hasAppSelected && psvm.anyPatchSelected(),
onClick = { onClickPatch(); patcherUtils.loadPatchBundle() }, // TODO: replace this with something better
icon = { Icon(Icons.Default.Build, contentDescription = stringResource(R.string.patch)) },
enabled = hasAppSelected && vm.selectedPatches.isNotEmpty(),
onClick = onClickPatch,
icon = { Icon(Icons.Default.Build, contentDescription = "Patch") },
text = { Text(stringResource(R.string.patch)) }
)
}) { paddingValues ->
@ -89,18 +82,18 @@ fun PatcherScreen(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
if (selectedAppPackage.isPresent) {
if (vm.selectedAppPackage.isPresent) {
AppIcon(
LocalContext.current.packageManager.getApplicationIcon(
selectedAppPackage.get().packageName
vm.selectedAppPackage.get().packageName
), contentDescription = null, size = 18
)
Spacer(Modifier.width(5.dp))
}
Text(
text = if (patchesLoaded) {
if (selectedAppPackage.isPresent) {
selectedAppPackage.get().packageName
if (vm.selectedAppPackage.isPresent) {
vm.selectedAppPackage.get().packageName
} else {
stringResource(R.string.card_application_not_selected)
}
@ -128,8 +121,8 @@ fun PatcherScreen(
Text(
text = if (!hasAppSelected) {
stringResource(R.string.select_an_application_first)
} else if (psvm.anyPatchSelected()) {
"$selectedAmount patches selected."
} else if (vm.selectedPatches.isNotEmpty()) {
"${vm.selectedPatches.size} patches selected."
} else {
stringResource(R.string.card_patches_body_patches)
},

View File

@ -25,11 +25,11 @@ import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = getViewModel(),
vm: SettingsViewModel = getViewModel(),
onClickContributors: () -> Unit,
onClickLicenses: () -> Unit,
) {
val prefs = viewModel.prefs
val prefs = vm.prefs
Column(
modifier = Modifier
@ -38,19 +38,19 @@ fun SettingsScreen(
.verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (viewModel.showThemePicker) {
if (vm.showThemePicker) {
ThemePicker(
onDismissRequest = viewModel::dismissThemePicker,
onConfirm = viewModel::setTheme
onDismissRequest = vm::dismissThemePicker,
onConfirm = vm::setTheme
)
}
GroupHeader(stringResource(R.string.appearance))
ListItem(
modifier = Modifier.clickable { viewModel.showThemePicker() },
modifier = Modifier.clickable { vm.showThemePicker() },
headlineText = { Text(stringResource(R.string.theme)) },
leadingContent = { Icon(Icons.Default.Style, contentDescription = null) },
trailingContent = {
FilledTonalButton(onClick = { viewModel.showThemePicker() }) {
FilledTonalButton(onClick = { vm.showThemePicker() }) {
Text(text = prefs.theme.displayName)
}
}
@ -85,7 +85,7 @@ fun SettingsScreen(
}
)
Divider()
SocialItem(R.string.github, R.drawable.ic_github, viewModel::openGitHub)
SocialItem(R.string.github, R.drawable.ic_github, vm::openGitHub)
SocialItem(R.string.opensource_licenses, Icons.Default.LibraryBooks, onClickLicenses)
SocialItem(R.string.screen_contributors_title, Icons.Default.Group, onClickContributors)
}

View File

@ -1,7 +1,6 @@
package app.revanced.manager.ui.screen.subscreens
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
@ -12,9 +11,7 @@ import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.SdStorage
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppIcon
@ -31,17 +28,12 @@ fun AppSelectorSubscreen(
navigator: BackstackNavigator<AppDestination>,
vm: AppSelectorViewModel = getViewModel(),
) {
val context = LocalContext.current
val filtered = mutableStateOf(vm.filteredApps.isNotEmpty())
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
it?.let { uri ->
vm.setSelectedAppPackageFromFile(uri)
navigator.pop()
return@rememberLauncherForActivityResult
}
Toast.makeText(context, "Couldn't load APK file.", Toast.LENGTH_SHORT).show()
}
Scaffold(
@ -63,7 +55,7 @@ fun AppSelectorSubscreen(
)
},
) { paddingValues ->
if (filtered.value) {
if (vm.filteredApps.isNotEmpty()) {
LazyColumn(modifier = Modifier.padding(paddingValues)) {
items(count = vm.filteredApps.size) { int ->
val app = vm.filteredApps[int]

View File

@ -13,132 +13,122 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.Resource
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.PatchCard
import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.patcher.extensions.PatchExtensions.patchName
import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.get
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel
import app.revanced.manager.ui.component.PatchCard
@SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PatchesSelectorSubscreen(
navigator: BackstackNavigator<AppDestination>,
psvm: PatchesSelectorViewModel = getViewModel(),
patcherUtils: PatcherUtils = get()
vm: PatchesSelectorViewModel = getViewModel(),
) {
val patchesState by patcherUtils.patches
val patches = psvm.getFilteredPatches()
val patches = vm.filteredPatches
var query by mutableStateOf("")
Scaffold(
topBar = {
MediumTopAppBar(
title = {
Text(
text = stringResource(R.string.card_patches_header),
style = MaterialTheme.typography.headlineLarge
)
},
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null
)
}
},
actions = {
IconButton(onClick = {
psvm.selectAllPatches(patches, !psvm.anyPatchSelected())
}) {
if (!psvm.anyPatchSelected()) Icon(
Icons.Default.SelectAll,
contentDescription = null
) else Icon(Icons.Default.Deselect, contentDescription = null)
}
}
)
LaunchedEffect(null) {
launch(Dispatchers.Default) {
vm.filterPatches()
}
) { paddingValues ->
}
Scaffold(topBar = {
MediumTopAppBar(title = {
Text(
text = stringResource(R.string.card_patches_header),
style = MaterialTheme.typography.headlineLarge
)
}, navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(
imageVector = Icons.Default.ArrowBack, contentDescription = null
)
}
}, actions = {
IconButton(onClick = {
vm.selectAllPatches(patches, vm.selectedPatches.isEmpty())
}) {
if (vm.selectedPatches.isEmpty()) Icon(
Icons.Default.SelectAll, contentDescription = null
) else Icon(Icons.Default.Deselect, contentDescription = null)
}
})
}) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
modifier = Modifier.padding(paddingValues)
) {
when (patchesState) {
is Resource.Success -> {
if (patches.isNotEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp, 4.dp),
if (!vm.loading) {
if (patches.isNotEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp, 4.dp),
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
shape = RoundedCornerShape(12.dp),
value = query,
onValueChange = { newValue ->
query = newValue
},
leadingIcon = {
Icon(Icons.Default.Search, stringResource(R.string.search))
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = {
query = ""
}) {
Icon(Icons.Default.Clear, stringResource(R.string.clear))
}
}
},
)
}
}
LazyColumn(Modifier.padding(0.dp, 2.dp)) {
if (query.isEmpty() || query.isBlank()) {
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
PatchCard(patch, psvm.isPatchSelected(name)) {
psvm.selectPatch(name, !psvm.isPatchSelected(name))
}
}
} else {
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
if (name.contains(query.lowercase())) {
PatchCard(patch, psvm.isPatchSelected(name)) {
psvm.selectPatch(name, !psvm.isPatchSelected(name))
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
shape = RoundedCornerShape(12.dp),
value = query,
onValueChange = { newValue ->
query = newValue
},
leadingIcon = {
Icon(Icons.Default.Search, "Search")
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = {
query = ""
}) {
Icon(Icons.Default.Clear, "Clear")
}
}
}
}
}
} else {
Column(
Modifier.fillMaxSize(),
Arrangement.Center,
Alignment.CenterHorizontally
) {
Text(stringResource(R.string.no_compatible_patches))
},
)
}
}
LazyColumn(Modifier.padding(0.dp, 2.dp)) {
if (query.isEmpty() || query.isBlank()) {
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
PatchCard(patch, vm.isPatchSelected(name)) {
vm.selectPatch(name, !vm.isPatchSelected(name))
}
}
} else {
items(count = patches.size) {
val patch = patches[it]
val name = patch.patch.patchName
if (name.contains(query.lowercase())) {
PatchCard(patch, vm.isPatchSelected(name)) {
vm.selectPatch(name, !vm.isPatchSelected(name))
}
}
}
}
}
} else {
Column(
Modifier.fillMaxSize(),
Arrangement.Center,
Alignment.CenterHorizontally
) {
Text(stringResource(R.string.no_compatible_patches))
}
}
else -> LoadingIndicator(null)
}
} else LoadingIndicator(null)
}
}
}

View File

@ -20,19 +20,17 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.component.SourceItem
import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.viewmodel.SourceSelectorViewModel
import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.get
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SourceSelectorSubscreen(
navigator: BackstackNavigator<AppDestination>,
patcherUtils: PatcherUtils = get()
viewModel: SourceSelectorViewModel = getViewModel()
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
state = rememberTopAppBarState(),
@ -42,17 +40,7 @@ fun SourceSelectorSubscreen(
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
it?.let { uri ->
val patchesFile = context.cacheDir.resolve("patches.jar")
Files.copy(
context.contentResolver.openInputStream(uri),
patchesFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
patchesFile.absolutePath.also {
patcherUtils.patchBundleFile = it
patcherUtils.loadPatchBundle(it)
}
viewModel.loadBundle(uri)
navigator.pop()
return@rememberLauncherForActivityResult
}

View File

@ -6,6 +6,7 @@ import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.patcher.PatcherUtils
@ -23,8 +24,9 @@ class AppSelectorViewModel(
val app: Application, patcherUtils: PatcherUtils
) : ViewModel() {
val filteredApps = mutableListOf<ApplicationInfo>()
val filteredApps = mutableStateListOf<ApplicationInfo>()
val patches = patcherUtils.patches
private val filteredPatches = patcherUtils.filteredPatches
private val selectedAppPackage = patcherUtils.selectedAppPackage
private val selectedPatches = patcherUtils.selectedPatches
@ -32,7 +34,7 @@ class AppSelectorViewModel(
viewModelScope.launch { filterApps() }
}
private suspend fun filterApps() = withContext(Dispatchers.Main) {
private suspend fun filterApps() = withContext(Dispatchers.Default) {
try {
val (patches) = patches.value as Resource.Success
patches.forEach patch@{ patch ->
@ -59,13 +61,16 @@ class AppSelectorViewModel(
return app.packageManager.getApplicationLabel(info).toString()
}
fun loadIcon(info: ApplicationInfo): Drawable? {
return info.loadIcon(app.packageManager)
fun loadIcon(info: ApplicationInfo): Drawable {
return app.packageManager.getApplicationIcon(info)
}
fun setSelectedAppPackage(appId: ApplicationInfo) {
selectedAppPackage.value.ifPresent { s ->
if (s != appId) selectedPatches.clear()
if (s != appId) {
selectedPatches.clear()
filteredPatches.clear()
}
}
selectedAppPackage.value = Optional.of(appId)
}

View File

@ -0,0 +1,25 @@
package app.revanced.manager.ui.viewmodel
import androidx.compose.runtime.getValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.patcher.PatcherUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class PatcherScreenViewModel(
patcherUtils: PatcherUtils,
api: ManagerAPI
) : ViewModel() {
init {
viewModelScope.launch(Dispatchers.IO) {
api.downloadPatches()
}
}
val selectedPatches = patcherUtils.selectedPatches
val selectedAppPackage by patcherUtils.selectedAppPackage
val patchesLoaded by patcherUtils.patches
}

View File

@ -1,6 +1,9 @@
package app.revanced.manager.ui.viewmodel
import android.os.Parcelable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.ui.Resource
@ -13,16 +16,14 @@ import kotlinx.parcelize.Parcelize
class PatchesSelectorViewModel(
private val patcherUtils: PatcherUtils
) : ViewModel() {
private val selectedPatches = patcherUtils.selectedPatches
val filteredPatches = patcherUtils.filteredPatches
val selectedPatches = patcherUtils.selectedPatches
var loading by mutableStateOf(true)
fun isPatchSelected(patchId: String): Boolean {
return selectedPatches.contains(patchId)
}
fun anyPatchSelected(): Boolean {
return !selectedPatches.isEmpty()
}
fun selectPatch(patchId: String, state: Boolean) {
if (state) selectedPatches.add(patchId)
else selectedPatches.remove(patchId)
@ -36,23 +37,26 @@ class PatchesSelectorViewModel(
}
}
fun getFilteredPatches(): List<PatchClass> {
return buildList {
val selected = patcherUtils.getSelectedPackageInfo() ?: return@buildList
val (patches) = patcherUtils.patches.value as? Resource.Success ?: return@buildList
patches.forEach patch@{ patch ->
var unsupported = false
patch.compatiblePackages?.forEach { pkg ->
// if we detect unsupported once, don't overwrite it
if (pkg.name == selected.packageName) {
if (!unsupported)
unsupported =
pkg.versions.isNotEmpty() && !pkg.versions.any { it == selected.versionName }
add(PatchClass(patch, unsupported))
}
fun filterPatches() {
loading = true
val selected = patcherUtils.getSelectedPackageInfo() ?: return
val (patches) = patcherUtils.patches.value as? Resource.Success ?: return
if (filteredPatches.isNotEmpty()) {
loading = false; return
}
patches.forEach patch@{ patch ->
var unsupported = false
patch.compatiblePackages?.forEach { pkg ->
// if we detect unsupported once, don't overwrite it
if (pkg.name == selected.packageName) {
if (!unsupported)
unsupported =
pkg.versions.isNotEmpty() && !pkg.versions.any { it == selected.versionName }
filteredPatches.add(PatchClass(patch, unsupported))
}
}
}
loading = false
}
}

View File

@ -0,0 +1,32 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.util.tag
import io.sentry.Sentry
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
class SourceSelectorViewModel(val app: Application, val patcherUtils: PatcherUtils) : ViewModel() {
fun loadBundle(uri: Uri) {
try {
val patchesFile = app.cacheDir.resolve(File(uri.path!!).name)
Files.copy(
app.contentResolver.openInputStream(uri),
patchesFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
patchesFile.absolutePath.also {
patcherUtils.patchBundleFile = it
patcherUtils.loadPatchBundle(it)
}
} catch (e: Exception) {
Log.e(tag, "Failed to load bundle", e)
Sentry.captureException(e)
}
}
}