Skip to content

Commit

Permalink
feat: patch options UI (ReVanced#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 authored Aug 12, 2023
1 parent 3f059d7 commit 7aea947
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ fun AppScaffold(
fun AppTopBar(
title: String,
onBackClick: (() -> Unit)? = null,
backIcon: @Composable (() -> Unit) = @Composable {
Icon(
imageVector = Icons.Default.ArrowBack, contentDescription = stringResource(
R.string.back
)
)
},
actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null
) {
Expand All @@ -47,10 +54,7 @@ fun AppTopBar(
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.back)
)
backIcon()
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,84 +1,205 @@
package app.revanced.manager.ui.component.patches

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.Button
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.ListItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
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.data.platform.FileSystem
import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.util.toast
import app.revanced.patcher.patch.PatchOption
import org.koin.compose.rememberKoinInject

/**
* [Composable] functions do not support function references, so we have to use composable lambdas instead.
*/
private typealias OptionField = @Composable (Any?, (Any?) -> Unit) -> Unit
// Composable functions do not support function references, so we have to use composable lambdas instead.
private typealias OptionImpl = @Composable (Option, Any?, (Any?) -> Unit) -> Unit

private val StringField: OptionField = { value, setValue ->
val fs: FileSystem = rememberKoinInject()
@Composable
private fun OptionListItem(
option: Option,
onClick: () -> Unit,
trailingContent: @Composable () -> Unit
) {
ListItem(
modifier = Modifier.clickable(onClick = onClick),
headlineContent = { Text(option.title) },
supportingContent = { Text(option.description) },
trailingContent = trailingContent
)
}

@Composable
private fun StringOptionDialog(
name: String,
value: String?,
onSubmit: (String) -> Unit,
onDismissRequest: () -> Unit
) {
var showFileDialog by rememberSaveable { mutableStateOf(false) }
var fieldValue by rememberSaveable(value) {
mutableStateOf(value.orEmpty())
}

val fs: FileSystem = rememberKoinInject()
val (contract, permissionName) = fs.permissionContract()
val permissionLauncher = rememberLauncherForActivityResult(contract = contract) {
showFileDialog = it
}
val current = value as? String

if (showFileDialog) {
PathSelectorDialog(
root = fs.externalFilesDir()
) {
showFileDialog = false
it?.let { path ->
setValue(path.toString())
fieldValue = path.toString()
}
}
}

Column {
TextField(value = current ?: "", onValueChange = setValue)
Button(onClick = {
if (fs.hasStoragePermission()) {
showFileDialog = true
} else {
permissionLauncher.launch(permissionName)
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(name) },
text = {
OutlinedTextField(
value = fieldValue,
onValueChange = { fieldValue = it },
placeholder = {
Text(stringResource(R.string.string_option_placeholder))
},
trailingIcon = {
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
IconButton(
onClick = { showDropdownMenu = true }
) {
Icon(
Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.string_option_menu_description)
)
}

DropdownMenu(
expanded = showDropdownMenu,
onDismissRequest = { showDropdownMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Outlined.Folder, null)
},
text = {
Text(stringResource(R.string.path_selector))
},
onClick = {
showDropdownMenu = false
if (fs.hasStoragePermission()) {
showFileDialog = true
} else {
permissionLauncher.launch(permissionName)
}
}
)
}
}
)
},
confirmButton = {
TextButton(onClick = { onSubmit(fieldValue) }) {
Text(stringResource(R.string.save))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
}) {
Icon(Icons.Filled.FileOpen, null)
Text("Select file or folder")
},
)
}

private val StringOption: OptionImpl = { option, value, setValue ->
var showInputDialog by rememberSaveable { mutableStateOf(false) }
fun showInputDialog() {
showInputDialog = true
}

fun dismissInputDialog() {
showInputDialog = false
}

if (showInputDialog) {
StringOptionDialog(
name = option.title,
value = value as? String,
onSubmit = {
dismissInputDialog()
setValue(it)
},
onDismissRequest = ::dismissInputDialog
)
}

OptionListItem(
option = option,
onClick = ::showInputDialog
) {
IconButton(onClick = ::showInputDialog) {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.string_option_icon_description)
)
}
}
}

private val BooleanField: OptionField = { value, setValue ->
val current = value as? Boolean
Switch(checked = current ?: false, onCheckedChange = setValue)
private val BooleanOption: OptionImpl = { option, value, setValue ->
val current = (value as? Boolean) ?: false

OptionListItem(
option = option,
onClick = { setValue(!current) }
) {
Switch(checked = current, onCheckedChange = setValue)
}
}

private val UnknownField: OptionField = { _, _ ->
Text("This type has not been implemented")
private val UnknownOption: OptionImpl = { option, _, _ ->
val context = LocalContext.current
OptionListItem(
option = option,
onClick = { context.toast("Unknown type: ${option.type.name}") },
trailingContent = {})
}

@Composable
fun OptionField(option: Option, value: Any?, setValue: (Any?) -> Unit) {
fun OptionItem(option: Option, value: Any?, setValue: (Any?) -> Unit) {
val implementation = remember(option.type) {
when (option.type) {
// These are the only two types that are currently used by the official patches.
PatchOption.StringOption::class.java -> StringField
PatchOption.BooleanOption::class.java -> BooleanField
else -> UnknownField
PatchOption.StringOption::class.java -> StringOption
PatchOption.BooleanOption::class.java -> BooleanOption
else -> UnknownOption
}
}

implementation(value, setValue)
implementation(option, value, setValue)
}
Loading

0 comments on commit 7aea947

Please sign in to comment.