mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-11 18:34:25 +02:00
Merge branch 'compose-dev' into compose/fix/patcher-screen-process-death
This commit is contained in:
commit
625abd72b0
@ -191,6 +191,10 @@ dependencies {
|
|||||||
// Scrollbars
|
// Scrollbars
|
||||||
implementation(libs.scrollbars)
|
implementation(libs.scrollbars)
|
||||||
|
|
||||||
|
// EnumUtil
|
||||||
|
implementation(libs.enumutil)
|
||||||
|
ksp(libs.enumutil.ksp)
|
||||||
|
|
||||||
// Reorderable lists
|
// Reorderable lists
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
|
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import com.github.materiiapps.enumutil.FromValue
|
||||||
|
|
||||||
|
private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit)
|
||||||
|
private typealias InstallerStatusDialogButton = @Composable (model: InstallerStatusDialogModel) -> Unit
|
||||||
|
|
||||||
|
interface InstallerModel {
|
||||||
|
fun reinstall()
|
||||||
|
fun install()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstallerStatusDialogModel : InstallerModel {
|
||||||
|
var packageInstallerStatus: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
|
||||||
|
val dialogKind = remember {
|
||||||
|
DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
model.packageInstallerStatus = null
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
dialogKind.confirmButton(model)
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
dialogKind.dismissButton?.invoke(model)
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(dialogKind.icon, null)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(dialogKind.title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(dialogKind.contentStringResId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installerStatusDialogButton(
|
||||||
|
@StringRes buttonStringResId: Int,
|
||||||
|
buttonHandler: InstallerStatusDialogButtonHandler = { },
|
||||||
|
): InstallerStatusDialogButton = { model ->
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
model.packageInstallerStatus = null
|
||||||
|
buttonHandler(model)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(buttonStringResId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FromValue("flag")
|
||||||
|
enum class DialogKind(
|
||||||
|
val flag: Int,
|
||||||
|
val title: Int,
|
||||||
|
@StringRes val contentStringResId: Int,
|
||||||
|
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||||
|
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||||
|
val dismissButton: InstallerStatusDialogButton? = null,
|
||||||
|
) {
|
||||||
|
FAILURE(
|
||||||
|
flag = PackageInstaller.STATUS_FAILURE,
|
||||||
|
title = R.string.installation_failed_dialog_title,
|
||||||
|
contentStringResId = R.string.installation_failed_description,
|
||||||
|
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||||
|
model.install()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
FAILURE_ABORTED(
|
||||||
|
flag = PackageInstaller.STATUS_FAILURE_ABORTED,
|
||||||
|
title = R.string.installation_cancelled_dialog_title,
|
||||||
|
contentStringResId = R.string.installation_aborted_description,
|
||||||
|
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||||
|
model.install()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
FAILURE_BLOCKED(
|
||||||
|
flag = PackageInstaller.STATUS_FAILURE_BLOCKED,
|
||||||
|
title = R.string.installation_blocked_dialog_title,
|
||||||
|
contentStringResId = R.string.installation_blocked_description,
|
||||||
|
),
|
||||||
|
FAILURE_CONFLICT(
|
||||||
|
flag = PackageInstaller.STATUS_FAILURE_CONFLICT,
|
||||||
|
title = R.string.installation_conflict_dialog_title,
|
||||||
|
contentStringResId = R.string.installation_conflict_description,
|
||||||
|
confirmButton = installerStatusDialogButton(R.string.reinstall) { model ->
|
||||||
|
model.reinstall()
|
||||||
|
},
|
||||||
|
dismissButton = installerStatusDialogButton(R.string.cancel),
|
||||||
|
),
|
||||||
|
FAILURE_INCOMPATIBLE(
|
||||||
|
flag = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
|
||||||
|
title = R.string.installation_incompatible_dialog_title,
|
||||||
|
contentStringResId = R.string.installation_incompatible_description,
|
||||||
|
),
|
||||||
|
FAILURE_INVALID(
|
||||||
|
flag = PackageInstaller.STATUS_FAILURE_INVALID,
|
||||||
|
title = R.string.installation_invalid_dialog_title,
|
||||||
|
contentStringResId = R.string.installation_invalid_description,
|
||||||
|
confirmButton = installerStatusDialogButton(R.string.reinstall) { model ->
|
||||||
|
model.reinstall()
|
||||||
|
},
|
||||||
|
dismissButton = installerStatusDialogButton(R.string.cancel),
|
||||||
|
),
|
||||||
|
FAILURE_STORAGE(
|
||||||
|
flag = PackageInstaller.STATUS_FAILURE_STORAGE,
|
||||||
|
title = R.string.installation_storage_issue_dialog_title,
|
||||||
|
contentStringResId = R.string.installation_storage_issue_description,
|
||||||
|
),
|
||||||
|
|
||||||
|
@RequiresApi(34)
|
||||||
|
FAILURE_TIMEOUT(
|
||||||
|
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||||
|
title = R.string.installation_timeout_dialog_title,
|
||||||
|
contentStringResId = R.string.installation_timeout_description,
|
||||||
|
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||||
|
model.install()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Needed due to the @FromValue annotation.
|
||||||
|
companion object
|
||||||
|
}
|
@ -2,37 +2,33 @@ package app.revanced.manager.ui.component.bundle
|
|||||||
|
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material.icons.outlined.Extension
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material.icons.outlined.Inventory2
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material.icons.outlined.Sell
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.TextInputDialog
|
import app.revanced.manager.ui.component.TextInputDialog
|
||||||
import app.revanced.manager.util.isDebuggable
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseBundleDialog(
|
fun BaseBundleDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isDefault: Boolean,
|
isDefault: Boolean,
|
||||||
name: String?,
|
name: String?,
|
||||||
onNameChange: ((String) -> Unit)? = null,
|
|
||||||
remoteUrl: String?,
|
remoteUrl: String?,
|
||||||
onRemoteUrlChange: ((String) -> Unit)? = null,
|
onRemoteUrlChange: ((String) -> Unit)? = null,
|
||||||
patchCount: Int,
|
patchCount: Int,
|
||||||
@ -40,39 +36,66 @@ fun BaseBundleDialog(
|
|||||||
autoUpdate: Boolean,
|
autoUpdate: Boolean,
|
||||||
onAutoUpdateChange: (Boolean) -> Unit,
|
onAutoUpdateChange: (Boolean) -> Unit,
|
||||||
onPatchesClick: () -> Unit,
|
onPatchesClick: () -> Unit,
|
||||||
onBundleTypeClick: () -> Unit = {},
|
|
||||||
extraFields: @Composable ColumnScope.() -> Unit = {}
|
extraFields: @Composable ColumnScope.() -> Unit = {}
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollbar(
|
ColumnWithScrollbar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(modifier)
|
.then(modifier),
|
||||||
) {
|
) {
|
||||||
if (name != null) {
|
Column(
|
||||||
var showNameInputDialog by rememberSaveable {
|
modifier = Modifier.padding(16.dp),
|
||||||
mutableStateOf(false)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
}
|
) {
|
||||||
if (showNameInputDialog) {
|
Row(
|
||||||
TextInputDialog(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
initial = name,
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
|
||||||
title = stringResource(R.string.bundle_input_name),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
onDismissRequest = {
|
) {
|
||||||
showNameInputDialog = false
|
Icon(
|
||||||
},
|
imageVector = Icons.Outlined.Inventory2,
|
||||||
onConfirm = {
|
contentDescription = null,
|
||||||
showNameInputDialog = false
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
onNameChange?.invoke(it)
|
modifier = Modifier.size(32.dp)
|
||||||
},
|
|
||||||
validator = {
|
|
||||||
it.length in 1..19
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
name?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 2.dp)
|
||||||
|
) {
|
||||||
|
version?.let {
|
||||||
|
Tag(Icons.Outlined.Sell, it)
|
||||||
|
}
|
||||||
|
Tag(Icons.Outlined.Extension, patchCount.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
if (remoteUrl != null) {
|
||||||
BundleListItem(
|
BundleListItem(
|
||||||
headlineText = stringResource(R.string.bundle_input_name),
|
headlineText = stringResource(R.string.bundle_auto_update),
|
||||||
supportingText = name.ifEmpty { stringResource(R.string.field_not_set) },
|
supportingText = stringResource(R.string.bundle_auto_update_description),
|
||||||
modifier = Modifier.clickable(enabled = onNameChange != null) {
|
trailingContent = {
|
||||||
showNameInputDialog = true
|
Switch(
|
||||||
|
checked = autoUpdate,
|
||||||
|
onCheckedChange = onAutoUpdateChange
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onAutoUpdateChange(!autoUpdate)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -99,81 +122,59 @@ fun BaseBundleDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
BundleListItem(
|
BundleListItem(
|
||||||
modifier = Modifier.clickable(enabled = onRemoteUrlChange != null) {
|
modifier = Modifier.clickable(
|
||||||
showUrlInputDialog = true
|
enabled = onRemoteUrlChange != null,
|
||||||
},
|
onClick = {
|
||||||
headlineText = stringResource(R.string.bundle_input_source_url),
|
showUrlInputDialog = true
|
||||||
supportingText = url.ifEmpty { stringResource(R.string.field_not_set) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
extraFields()
|
|
||||||
|
|
||||||
if (remoteUrl != null) {
|
|
||||||
BundleListItem(
|
|
||||||
headlineText = stringResource(R.string.bundle_auto_update),
|
|
||||||
supportingText = stringResource(R.string.bundle_auto_update_description),
|
|
||||||
trailingContent = {
|
|
||||||
Switch(
|
|
||||||
checked = autoUpdate,
|
|
||||||
onCheckedChange = onAutoUpdateChange
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
onAutoUpdateChange(!autoUpdate)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BundleListItem(
|
|
||||||
headlineText = stringResource(R.string.bundle_type),
|
|
||||||
supportingText = stringResource(R.string.bundle_type_description),
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
onBundleTypeClick()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = onBundleTypeClick,
|
|
||||||
content = {
|
|
||||||
if (remoteUrl == null) {
|
|
||||||
Text(stringResource(R.string.local))
|
|
||||||
} else {
|
|
||||||
Text(stringResource(R.string.remote))
|
|
||||||
}
|
}
|
||||||
|
),
|
||||||
|
headlineText = stringResource(R.string.bundle_input_source_url),
|
||||||
|
supportingText = url.ifEmpty {
|
||||||
|
stringResource(R.string.field_not_set)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version != null || patchCount > 0) {
|
val patchesClickable = patchCount > 0
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.information),
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
horizontal = 16.dp,
|
|
||||||
vertical = 12.dp
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0
|
|
||||||
BundleListItem(
|
BundleListItem(
|
||||||
headlineText = stringResource(R.string.patches),
|
headlineText = stringResource(R.string.patches),
|
||||||
supportingText = pluralStringResource(R.plurals.bundle_patches_available, patchCount, patchCount),
|
supportingText = stringResource(R.string.bundle_view_patches),
|
||||||
modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick)
|
modifier = Modifier.clickable(
|
||||||
|
enabled = patchesClickable,
|
||||||
|
onClick = onPatchesClick
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (patchesClickable)
|
if (patchesClickable) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||||
stringResource(R.string.patches)
|
stringResource(R.string.patches)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
version?.let {
|
extraFields()
|
||||||
BundleListItem(
|
}
|
||||||
headlineText = stringResource(R.string.version),
|
}
|
||||||
supportingText = it,
|
|
||||||
)
|
@Composable
|
||||||
}
|
private fun Tag(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,18 +11,9 @@ import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
|||||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||||
import androidx.compose.material.icons.outlined.Share
|
import androidx.compose.material.icons.outlined.Share
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
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.compose.ui.res.stringResource
|
||||||
@ -78,7 +69,7 @@ fun BundleInformationDialog(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = bundleName,
|
title = stringResource(R.string.patch_bundle_field),
|
||||||
onBackClick = onDismissRequest,
|
onBackClick = onDismissRequest,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@ -111,7 +102,6 @@ fun BundleInformationDialog(
|
|||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
isDefault = bundle.isDefault,
|
isDefault = bundle.isDefault,
|
||||||
name = bundleName,
|
name = bundleName,
|
||||||
onNameChange = { composableScope.launch { bundle.setName(it) } },
|
|
||||||
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
||||||
patchCount = patchCount,
|
patchCount = patchCount,
|
||||||
version = props?.versionInfo?.patches,
|
version = props?.versionInfo?.patches,
|
||||||
|
@ -1,33 +1,35 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
package app.revanced.manager.ui.component.bundle
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.Lightbulb
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
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.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
|
import app.revanced.manager.ui.component.ArrowButton
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.NotificationCard
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -35,7 +37,8 @@ fun BundlePatchesDialog(
|
|||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
bundle: PatchBundleSource,
|
bundle: PatchBundleSource,
|
||||||
) {
|
) {
|
||||||
var informationCardVisible by remember { mutableStateOf(true) }
|
var showAllVersions by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showOptions by rememberSaveable { mutableStateOf(false) }
|
||||||
val state by bundle.state.collectAsStateWithLifecycle()
|
val state by bundle.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
@ -62,44 +65,212 @@ fun BundlePatchesDialog(
|
|||||||
LazyColumnWithScrollbar(
|
LazyColumnWithScrollbar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues),
|
||||||
.padding(16.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
contentPadding = PaddingValues(16.dp)
|
||||||
) {
|
) {
|
||||||
item {
|
|
||||||
AnimatedVisibility(visible = informationCardVisible) {
|
|
||||||
NotificationCard(
|
|
||||||
icon = Icons.Outlined.Lightbulb,
|
|
||||||
text = stringResource(R.string.tap_on_patches),
|
|
||||||
onDismiss = { informationCardVisible = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.patchBundleOrNull()?.let { bundle ->
|
state.patchBundleOrNull()?.let { bundle ->
|
||||||
items(bundle.patches.size) { bundleIndex ->
|
items(bundle.patches) { patch ->
|
||||||
val patch = bundle.patches[bundleIndex]
|
PatchItem(
|
||||||
ListItem(
|
patch,
|
||||||
headlineContent = {
|
showAllVersions,
|
||||||
Text(
|
onExpandVersions = { showAllVersions = !showAllVersions },
|
||||||
text = patch.name,
|
showOptions,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
onExpandOptions = { showOptions = !showOptions }
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
},
|
|
||||||
supportingContent = {
|
|
||||||
patch.description?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun PatchItem(
|
||||||
|
patch: PatchInfo,
|
||||||
|
expandVersions: Boolean,
|
||||||
|
onExpandVersions: () -> Unit,
|
||||||
|
expandOptions: Boolean,
|
||||||
|
onExpandOptions: () -> Unit
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(
|
||||||
|
if (patch.options.isNullOrEmpty()) Modifier else Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable(onClick = onExpandOptions),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Absolute.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = patch.name,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!patch.options.isNullOrEmpty()) {
|
||||||
|
ArrowButton(expanded = expandOptions, onClick = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patch.description?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
if (patch.compatiblePackages.isNullOrEmpty()) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
PatchInfoChip(
|
||||||
|
text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}"
|
||||||
|
)
|
||||||
|
PatchInfoChip(
|
||||||
|
text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
patch.compatiblePackages.forEach { compatiblePackage ->
|
||||||
|
val packageName = compatiblePackage.packageName
|
||||||
|
val versions = compatiblePackage.versions.orEmpty().reversed()
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
PatchInfoChip(
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
|
text = "$PACKAGE_ICON $packageName"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (versions.isNotEmpty()) {
|
||||||
|
if (expandVersions) {
|
||||||
|
versions.forEach { version ->
|
||||||
|
PatchInfoChip(
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
|
text = "$VERSION_ICON $version"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PatchInfoChip(
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
|
text = "$VERSION_ICON ${versions.first()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (versions.size > 1) {
|
||||||
|
PatchInfoChip(
|
||||||
|
onClick = onExpandVersions,
|
||||||
|
text = if (expandVersions) stringResource(R.string.less) else "+${versions.size - 1}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!patch.options.isNullOrEmpty()) {
|
||||||
|
AnimatedVisibility(visible = expandOptions) {
|
||||||
|
val options = patch.options
|
||||||
|
|
||||||
|
Column {
|
||||||
|
options.forEachIndexed { i, option ->
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
), shape = when {
|
||||||
|
options.size == 1 -> RoundedCornerShape(8.dp)
|
||||||
|
i == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)
|
||||||
|
i == options.lastIndex -> RoundedCornerShape(
|
||||||
|
bottomStart = 8.dp,
|
||||||
|
bottomEnd = 8.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> RoundedCornerShape(0.dp)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = option.title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = option.description,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PatchInfoChip(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
text: String
|
||||||
|
) {
|
||||||
|
val shape = RoundedCornerShape(8.0.dp)
|
||||||
|
val cardModifier = if (onClick != null) {
|
||||||
|
Modifier
|
||||||
|
.clip(shape)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = modifier.then(cardModifier),
|
||||||
|
colors = CardColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
shape = shape,
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
softWrap = false,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val PACKAGE_ICON = "\uD83D\uDCE6"
|
||||||
|
const val VERSION_ICON = "\uD83C\uDFAF"
|
@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppScaffold
|
import app.revanced.manager.ui.component.AppScaffold
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.InstallerStatusDialog
|
||||||
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
||||||
import app.revanced.manager.ui.component.patcher.Steps
|
import app.revanced.manager.ui.component.patcher.Steps
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
@ -73,6 +74,9 @@ fun PatcherScreen(
|
|||||||
onConfirm = vm::install
|
onConfirm = vm::install
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (vm.installerStatusDialogModel.packageInstallerStatus != null)
|
||||||
|
InstallerStatusDialog(vm.installerStatusDialogModel)
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
@ -85,7 +89,7 @@ fun PatcherScreen(
|
|||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
||||||
enabled = canInstall
|
enabled = patcherSucceeded == true
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||||
}
|
}
|
||||||
@ -155,4 +159,4 @@ fun PatcherScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ fun SettingsScreen(
|
|||||||
) to SettingsDestination.Advanced,
|
) to SettingsDestination.Advanced,
|
||||||
Triple(
|
Triple(
|
||||||
R.string.about,
|
R.string.about,
|
||||||
R.string.about_description,
|
R.string.app_name,
|
||||||
Icons.Outlined.Info
|
Icons.Outlined.Info
|
||||||
) to SettingsDestination.About,
|
) to SettingsDestination.About,
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Api
|
import androidx.compose.material.icons.outlined.Api
|
||||||
@ -13,7 +17,9 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -28,9 +34,10 @@ import app.revanced.manager.ui.component.settings.BooleanItem
|
|||||||
import app.revanced.manager.ui.component.settings.IntegerItem
|
import app.revanced.manager.ui.component.settings.IntegerItem
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AdvancedSettingsScreen(
|
fun AdvancedSettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
@ -45,6 +52,7 @@ fun AdvancedSettingsScreen(
|
|||||||
activityManager.largeMemoryClass
|
activityManager.largeMemoryClass
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -82,15 +90,6 @@ fun AdvancedSettingsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val exportDebugLogsLauncher =
|
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
|
|
||||||
it?.let(vm::exportDebugLogs)
|
|
||||||
}
|
|
||||||
SettingsListItem(
|
|
||||||
headlineContent = stringResource(R.string.debug_logs_export),
|
|
||||||
modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) }
|
|
||||||
)
|
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.patcher))
|
GroupHeader(stringResource(R.string.patcher))
|
||||||
BooleanItem(
|
BooleanItem(
|
||||||
preference = vm.prefs.useProcessRuntime,
|
preference = vm.prefs.useProcessRuntime,
|
||||||
@ -138,16 +137,38 @@ fun AdvancedSettingsScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.debugging))
|
GroupHeader(stringResource(R.string.debugging))
|
||||||
|
val exportDebugLogsLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
|
||||||
|
it?.let(vm::exportDebugLogs)
|
||||||
|
}
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.about_device),
|
headlineContent = stringResource(R.string.debug_logs_export),
|
||||||
supportingContent = """
|
modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) }
|
||||||
**Version**: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
|
)
|
||||||
**Build type**: ${BuildConfig.BUILD_TYPE}
|
val clipboard = remember { context.getSystemService<ClipboardManager>()!! }
|
||||||
**Model**: ${Build.MODEL}
|
val deviceContent = """
|
||||||
**Android version**: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT})
|
Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
|
||||||
**Supported Archs**: ${Build.SUPPORTED_ABIS.joinToString(", ")}
|
Build type: ${BuildConfig.BUILD_TYPE}
|
||||||
**Memory limit**: $memoryLimit
|
Model: ${Build.MODEL}
|
||||||
|
Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT})
|
||||||
|
Supported Archs: ${Build.SUPPORTED_ABIS.joinToString(", ")}
|
||||||
|
Memory limit: $memoryLimit
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
SettingsListItem(
|
||||||
|
modifier = Modifier.combinedClickable(
|
||||||
|
onClick = { },
|
||||||
|
onLongClickLabel = stringResource(R.string.copy_to_clipboard),
|
||||||
|
onLongClick = {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
clipboard.setPrimaryClip(
|
||||||
|
ClipData.newPlainText("Device Information", deviceContent)
|
||||||
|
)
|
||||||
|
|
||||||
|
context.toast(context.getString(R.string.toast_copied_to_clipboard))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
headlineContent = stringResource(R.string.about_device),
|
||||||
|
supportingContent = deviceContent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,20 @@ package app.revanced.manager.ui.screen.settings
|
|||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
@ -32,7 +32,7 @@ fun DeveloperOptionsScreen(
|
|||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
GroupHeader(stringResource(R.string.patch_bundles_section))
|
GroupHeader(stringResource(R.string.patch_bundles_section))
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.patch_bundles_redownload),
|
headlineContent = stringResource(R.string.patch_bundles_force_download),
|
||||||
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||||
)
|
)
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
|
@ -2,8 +2,18 @@ package app.revanced.manager.ui.screen.settings
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@ -96,7 +106,7 @@ private fun ThemePicker(
|
|||||||
title = { Text(stringResource(R.string.theme)) },
|
title = { Text(stringResource(R.string.theme)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Theme.values().forEach {
|
Theme.entries.forEach {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -250,12 +250,17 @@ private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes description: Int) =
|
private fun GroupItem(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
@StringRes headline: Int,
|
||||||
|
@StringRes description: Int? = null
|
||||||
|
) {
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { onClick() },
|
modifier = Modifier.clickable { onClick() },
|
||||||
headlineContent = stringResource(headline),
|
headlineContent = stringResource(headline),
|
||||||
supportingContent = stringResource(description)
|
supportingContent = description?.let { stringResource(it) }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun KeystoreCredentialsDialog(
|
fun KeystoreCredentialsDialog(
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
@ -35,6 +35,8 @@ import app.revanced.manager.patcher.logger.LogLevel
|
|||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||||
import app.revanced.manager.service.InstallService
|
import app.revanced.manager.service.InstallService
|
||||||
|
import app.revanced.manager.service.UninstallService
|
||||||
|
import app.revanced.manager.ui.component.InstallerStatusDialogModel
|
||||||
import app.revanced.manager.ui.destination.Destination
|
import app.revanced.manager.ui.destination.Destination
|
||||||
import app.revanced.manager.ui.model.ProgressKey
|
import app.revanced.manager.ui.model.ProgressKey
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
@ -77,6 +79,20 @@ class PatcherViewModel(
|
|||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
private val savedStateHandle: SavedStateHandle by inject()
|
private val savedStateHandle: SavedStateHandle by inject()
|
||||||
|
|
||||||
|
val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel {
|
||||||
|
override var packageInstallerStatus: Int? by mutableStateOf(null)
|
||||||
|
|
||||||
|
override fun reinstall() {
|
||||||
|
this@PatcherViewModel.reinstall()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun install() {
|
||||||
|
// Since this is a package installer status dialog,
|
||||||
|
// InstallType.ROOT is never used here.
|
||||||
|
install(InstallType.DEFAULT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var installedApp: InstalledApp? = null
|
private var installedApp: InstalledApp? = null
|
||||||
val packageName = input.selectedApp.packageName
|
val packageName = input.selectedApp.packageName
|
||||||
|
|
||||||
@ -192,15 +208,19 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
InstallService.APP_INSTALL_ACTION -> {
|
InstallService.APP_INSTALL_ACTION -> {
|
||||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
val pmStatus = intent.getIntExtra(
|
||||||
val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
InstallService.EXTRA_INSTALL_STATUS,
|
||||||
|
PackageInstaller.STATUS_FAILURE
|
||||||
|
)
|
||||||
|
|
||||||
|
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||||
|
?.let(logger::trace)
|
||||||
|
|
||||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
|
||||||
installedPackageName =
|
installedPackageName =
|
||||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -212,8 +232,24 @@ class PatcherViewModel(
|
|||||||
input.selectedPatches
|
input.selectedPatches
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
|
||||||
|
installerStatusDialogModel.packageInstallerStatus = pmStatus
|
||||||
|
|
||||||
|
isInstalling = false
|
||||||
|
}
|
||||||
|
|
||||||
|
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||||
|
val pmStatus = intent.getIntExtra(
|
||||||
|
UninstallService.EXTRA_UNINSTALL_STATUS,
|
||||||
|
PackageInstaller.STATUS_FAILURE
|
||||||
|
)
|
||||||
|
|
||||||
|
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||||
|
?.let(logger::trace)
|
||||||
|
|
||||||
|
if (pmStatus != PackageInstaller.STATUS_SUCCESS) {
|
||||||
|
installerStatusDialogModel.packageInstallerStatus = pmStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,9 +258,15 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
// TODO: detect system-initiated process death during the patching process.
|
// TODO: detect system-initiated process death during the patching process.
|
||||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
ContextCompat.registerReceiver(
|
||||||
addAction(InstallService.APP_INSTALL_ACTION)
|
app,
|
||||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
installerBroadcastReceiver,
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(InstallService.APP_INSTALL_ACTION)
|
||||||
|
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||||
|
},
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
|
)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
installedApp = installedAppRepository.get(packageName)
|
installedApp = installedAppRepository.get(packageName)
|
||||||
@ -234,7 +276,7 @@ class PatcherViewModel(
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installBroadcastReceiver)
|
app.unregisterReceiver(installerBroadcastReceiver)
|
||||||
workManager.cancelWorkById(patcherWorkerId)
|
workManager.cancelWorkById(patcherWorkerId)
|
||||||
|
|
||||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) {
|
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) {
|
||||||
@ -277,20 +319,56 @@ class PatcherViewModel(
|
|||||||
fun open() = installedPackageName?.let(pm::launch)
|
fun open() = installedPackageName?.let(pm::launch)
|
||||||
|
|
||||||
fun install(installType: InstallType) = viewModelScope.launch {
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
|
var pmInstallStarted = false
|
||||||
try {
|
try {
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
|
|
||||||
|
val currentPackageInfo = pm.getPackageInfo(outputFile)
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
|
||||||
|
// If the app is currently installed
|
||||||
|
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
|
||||||
|
if (existingPackageInfo != null) {
|
||||||
|
// Check if the app version is less than the installed version
|
||||||
|
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
|
||||||
|
// Exit if the selected app version is less than the installed version
|
||||||
|
installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when (installType) {
|
when (installType) {
|
||||||
InstallType.DEFAULT -> {
|
InstallType.DEFAULT -> {
|
||||||
|
// Check if the app is mounted as root
|
||||||
|
// If it is, unmount it first, silently
|
||||||
|
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||||
|
rootInstaller.unmount(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install regularly
|
||||||
pm.installApp(listOf(outputFile))
|
pm.installApp(listOf(outputFile))
|
||||||
|
pmInstallStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallType.ROOT -> {
|
InstallType.ROOT -> {
|
||||||
try {
|
try {
|
||||||
val label = with(pm) {
|
// Check for base APK, first check if the app is already installed
|
||||||
getPackageInfo(outputFile)?.label()
|
if (existingPackageInfo == null) {
|
||||||
?: throw Exception("Failed to load application info")
|
// If the app is not installed, check if the output file is a base apk
|
||||||
|
if (currentPackageInfo.splitNames != null) {
|
||||||
|
// Exit if there is no base APK package
|
||||||
|
installerStatusDialogModel.packageInstallerStatus =
|
||||||
|
PackageInstaller.STATUS_FAILURE_INVALID
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get label
|
||||||
|
val label = with(pm) {
|
||||||
|
currentPackageInfo.label()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install as root
|
||||||
rootInstaller.install(
|
rootInstaller.install(
|
||||||
outputFile,
|
outputFile,
|
||||||
inputFile,
|
inputFile,
|
||||||
@ -322,8 +400,22 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e(tag, "Failed to install", e)
|
||||||
|
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||||
} finally {
|
} finally {
|
||||||
isInstalling = false
|
if (!pmInstallStarted)
|
||||||
|
isInstalling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reinstall() = viewModelScope.launch {
|
||||||
|
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||||
|
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
|
||||||
|
pm.installApp(listOf(outputFile))
|
||||||
|
isInstalling = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import android.content.pm.PackageInstaller
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
@ -115,6 +116,8 @@ class PM(
|
|||||||
|
|
||||||
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
||||||
|
|
||||||
|
fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo)
|
||||||
|
|
||||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
val packageInstaller = app.packageManager.packageInstaller
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||||
@ -170,4 +173,4 @@ class PM(
|
|||||||
Intent(this, UninstallService::class.java),
|
Intent(this, UninstallService::class.java),
|
||||||
intentFlags
|
intentFlags
|
||||||
).intentSender
|
).intentSender
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package app.revanced.manager.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@ -10,7 +11,7 @@ import androidx.annotation.RequiresApi
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract<String, Boolean>() {
|
class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract<String, Boolean>() {
|
||||||
override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.fromParts("package", context.packageName, null))
|
||||||
|
|
||||||
override fun getSynchronousResult(context: Context, input: String): SynchronousResult<Boolean>? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null
|
override fun getSynchronousResult(context: Context, input: String): SynchronousResult<Boolean>? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null
|
||||||
|
|
||||||
|
@ -11,8 +11,4 @@
|
|||||||
<plurals name="selected_count">
|
<plurals name="selected_count">
|
||||||
<item quantity="other">%d selected</item>
|
<item quantity="other">%d selected</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="bundle_patches_available">
|
|
||||||
<item quantity="one">%d patch available</item>
|
|
||||||
<item quantity="other">%d patches available</item>
|
|
||||||
</plurals>
|
|
||||||
</resources>
|
</resources>
|
@ -6,6 +6,9 @@
|
|||||||
<string name="cli">CLI</string>
|
<string name="cli">CLI</string>
|
||||||
<string name="manager">Manager</string>
|
<string name="manager">Manager</string>
|
||||||
|
|
||||||
|
<string name="toast_copied_to_clipboard">Copied!</string>
|
||||||
|
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||||
|
|
||||||
<string name="dashboard">Dashboard</string>
|
<string name="dashboard">Dashboard</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="select_app">Select an app</string>
|
<string name="select_app">Select an app</string>
|
||||||
@ -49,19 +52,18 @@
|
|||||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||||
|
|
||||||
<string name="general">General</string>
|
<string name="general">General</string>
|
||||||
<string name="general_description">General settings</string>
|
<string name="general_description">Theme, dynamic color</string>
|
||||||
<string name="advanced">Advanced</string>
|
|
||||||
<string name="advanced_description">Advanced settings</string>
|
|
||||||
<string name="updates">Updates</string>
|
<string name="updates">Updates</string>
|
||||||
<string name="updates_description">Updates for ReVanced Manager</string>
|
<string name="updates_description">Check for updates and view changelogs</string>
|
||||||
|
<string name="downloads">Downloads</string>
|
||||||
|
<string name="downloads_description">Downloader plugins and downloaded apps</string>
|
||||||
|
<string name="import_export">Import & export</string>
|
||||||
|
<string name="import_export_description">Keystore, patch options and selection</string>
|
||||||
|
<string name="advanced">Advanced</string>
|
||||||
|
<string name="advanced_description">API URL, memory limit, debugging</string>
|
||||||
|
<string name="about">About</string>
|
||||||
<string name="opensource_licenses">Open source licenses</string>
|
<string name="opensource_licenses">Open source licenses</string>
|
||||||
<string name="opensource_licenses_description">View all the libraries used to make this application</string>
|
<string name="opensource_licenses_description">View all the libraries used to make this application</string>
|
||||||
<string name="downloads">Downloads</string>
|
|
||||||
<string name="downloads_description">Manage downloaded content</string>
|
|
||||||
<string name="import_export">Import & export</string>
|
|
||||||
<string name="import_export_description">Import and export settings</string>
|
|
||||||
<string name="about">About</string>
|
|
||||||
<string name="about_description">About ReVanced</string>
|
|
||||||
|
|
||||||
<string name="contributors">Contributors</string>
|
<string name="contributors">Contributors</string>
|
||||||
<string name="contributors_description">View the contributors of ReVanced</string>
|
<string name="contributors_description">View the contributors of ReVanced</string>
|
||||||
@ -143,7 +145,6 @@
|
|||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
<string name="system">System</string>
|
<string name="system">System</string>
|
||||||
<string name="light">Light</string>
|
<string name="light">Light</string>
|
||||||
<string name="information">Information</string>
|
|
||||||
<string name="dark">Dark</string>
|
<string name="dark">Dark</string>
|
||||||
<string name="appearance">Appearance</string>
|
<string name="appearance">Appearance</string>
|
||||||
<string name="downloaded_apps">Downloaded apps</string>
|
<string name="downloaded_apps">Downloaded apps</string>
|
||||||
@ -169,7 +170,7 @@
|
|||||||
<string name="device_memory_limit">Memory limits</string>
|
<string name="device_memory_limit">Memory limits</string>
|
||||||
<string name="device_memory_limit_format">%1$dMB (Normal) - %2$dMB (Large)</string>
|
<string name="device_memory_limit_format">%1$dMB (Normal) - %2$dMB (Large)</string>
|
||||||
<string name="patch_bundles_section">Patch bundles</string>
|
<string name="patch_bundles_section">Patch bundles</string>
|
||||||
<string name="patch_bundles_redownload">Redownload all patch bundles</string>
|
<string name="patch_bundles_force_download">Force download all patch bundles</string>
|
||||||
<string name="patch_bundles_reset">Reset patch bundles</string>
|
<string name="patch_bundles_reset">Reset patch bundles</string>
|
||||||
<string name="patching">Patching</string>
|
<string name="patching">Patching</string>
|
||||||
<string name="signing">Signing</string>
|
<string name="signing">Signing</string>
|
||||||
@ -179,8 +180,6 @@
|
|||||||
<string name="tab_bundles">Patch bundles</string>
|
<string name="tab_bundles">Patch bundles</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="refresh">Refresh</string>
|
<string name="refresh">Refresh</string>
|
||||||
<string name="remote">Remote</string>
|
|
||||||
<string name="local">Local</string>
|
|
||||||
<string name="continue_anyways">Continue anyways</string>
|
<string name="continue_anyways">Continue anyways</string>
|
||||||
<string name="download_another_version">Download another version</string>
|
<string name="download_another_version">Download another version</string>
|
||||||
<string name="download_app">Download app</string>
|
<string name="download_app">Download app</string>
|
||||||
@ -258,6 +257,7 @@
|
|||||||
<string name="install_app">Install</string>
|
<string name="install_app">Install</string>
|
||||||
<string name="install_app_success">App installed</string>
|
<string name="install_app_success">App installed</string>
|
||||||
<string name="install_app_fail">Failed to install app: %s</string>
|
<string name="install_app_fail">Failed to install app: %s</string>
|
||||||
|
<string name="reinstall_app_fail">Failed to reinstall app: %s</string>
|
||||||
<string name="uninstall_app_fail">Failed to uninstall app: %s</string>
|
<string name="uninstall_app_fail">Failed to uninstall app: %s</string>
|
||||||
<string name="open_app">Open</string>
|
<string name="open_app">Open</string>
|
||||||
<string name="save_apk">Save APK</string>
|
<string name="save_apk">Save APK</string>
|
||||||
@ -288,6 +288,7 @@
|
|||||||
<string name="drag_handle">reorder</string>
|
<string name="drag_handle">reorder</string>
|
||||||
|
|
||||||
<string name="more">More</string>
|
<string name="more">More</string>
|
||||||
|
<string name="less">Less</string>
|
||||||
<string name="continue_">Continue</string>
|
<string name="continue_">Continue</string>
|
||||||
<string name="dismiss">Dismiss</string>
|
<string name="dismiss">Dismiss</string>
|
||||||
<string name="permanent_dismiss">Do not show this again</string>
|
<string name="permanent_dismiss">Do not show this again</string>
|
||||||
@ -300,14 +301,15 @@
|
|||||||
<string name="submit_feedback_description">Help us improve this application</string>
|
<string name="submit_feedback_description">Help us improve this application</string>
|
||||||
<string name="developer_options">Developer options</string>
|
<string name="developer_options">Developer options</string>
|
||||||
<string name="developer_options_description">Options for debugging issues</string>
|
<string name="developer_options_description">Options for debugging issues</string>
|
||||||
<string name="bundle_input_name">Name</string>
|
|
||||||
<string name="bundle_input_source_url">Source URL</string>
|
<string name="bundle_input_source_url">Source URL</string>
|
||||||
<string name="bundle_update_success">Successfully updated %s</string>
|
<string name="bundle_update_success">Successfully updated %s</string>
|
||||||
<string name="bundle_update_unavailable">No update available for %s</string>
|
<string name="bundle_update_unavailable">No update available for %s</string>
|
||||||
<string name="bundle_auto_update">Auto update</string>
|
<string name="bundle_auto_update">Auto update</string>
|
||||||
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string>
|
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string>
|
||||||
<string name="bundle_type">Bundle type</string>
|
<string name="bundle_view_patches">View patches</string>
|
||||||
<string name="bundle_type_description">Choose the type of bundle you want</string>
|
<string name="bundle_view_patches_any_version">Any version</string>
|
||||||
|
<string name="bundle_view_patches_any_package">Any package</string>
|
||||||
|
|
||||||
<string name="about_revanced_manager">About ReVanced Manager</string>
|
<string name="about_revanced_manager">About ReVanced Manager</string>
|
||||||
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
|
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
|
||||||
<string name="update_available">An update is available</string>
|
<string name="update_available">An update is available</string>
|
||||||
@ -359,6 +361,24 @@
|
|||||||
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
|
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
|
||||||
<string name="remote_bundle_description">Import remote files from a URL, can automatically update</string>
|
<string name="remote_bundle_description">Import remote files from a URL, can automatically update</string>
|
||||||
<string name="recommended">Recommended</string>
|
<string name="recommended">Recommended</string>
|
||||||
|
|
||||||
|
<string name="installation_failed_dialog_title">Installation failed</string>
|
||||||
|
<string name="installation_cancelled_dialog_title">Installation cancelled</string>
|
||||||
|
<string name="installation_blocked_dialog_title">Installation blocked</string>
|
||||||
|
<string name="installation_conflict_dialog_title">Installation conflict</string>
|
||||||
|
<string name="installation_incompatible_dialog_title">Installation incompatible</string>
|
||||||
|
<string name="installation_invalid_dialog_title">Installation invalid</string>
|
||||||
|
<string name="installation_storage_issue_dialog_title">Not enough storage</string>
|
||||||
|
<string name="installation_timeout_dialog_title">Installation timed out</string>
|
||||||
|
<string name="installation_failed_description">The installation failed due to an unknown reason. Try again?</string>
|
||||||
|
<string name="installation_aborted_description">The installation was cancelled manually. Try again?</string>
|
||||||
|
<string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string>
|
||||||
|
<string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string>
|
||||||
|
<string name="installation_incompatible_description">The app is incompatible with this device. Use the correct APK for your device and try again.</string>
|
||||||
|
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string>
|
||||||
|
<string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string>
|
||||||
|
<string name="installation_timeout_description">The installation took too long. Try again?</string>
|
||||||
|
<string name="reinstall">Reinstall</string>
|
||||||
<string name="show">Show</string>
|
<string name="show">Show</string>
|
||||||
<string name="debugging">Debugging</string>
|
<string name="debugging">Debugging</string>
|
||||||
<string name="about_device">About device</string>
|
<string name="about_device">About device</string>
|
||||||
|
@ -32,6 +32,7 @@ app-icon-loader-coil = "1.5.0"
|
|||||||
skrapeit = "1.2.2"
|
skrapeit = "1.2.2"
|
||||||
libsu = "5.2.2"
|
libsu = "5.2.2"
|
||||||
scrollbars = "1.0.4"
|
scrollbars = "1.0.4"
|
||||||
|
enumutil = "1.1.0"
|
||||||
compose-icons = "1.2.4"
|
compose-icons = "1.2.4"
|
||||||
kotlin-process = "1.4.1"
|
kotlin-process = "1.4.1"
|
||||||
hidden-api-stub = "4.3.3"
|
hidden-api-stub = "4.3.3"
|
||||||
@ -121,6 +122,10 @@ libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref =
|
|||||||
# Scrollbars
|
# Scrollbars
|
||||||
scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" }
|
scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" }
|
||||||
|
|
||||||
|
# EnumUtil
|
||||||
|
enumutil = { group = "io.github.materiiapps", name = "enumutil", version.ref = "enumutil" }
|
||||||
|
enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil" }
|
||||||
|
|
||||||
# Reorderable lists
|
# Reorderable lists
|
||||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user