Compare commits

..

22 Commits

Author SHA1 Message Date
05afc6bd0c Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-11 16:18:05 +07:00
a2caffb089 fix: leftovers 2025-07-11 16:12:58 +07:00
71b0fd48ab Merge branch 'compose-dev' into feat/tooltip
# Conflicts:
#	app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt
2025-07-11 16:07:30 +07:00
438fa485bd feat: Tooltip{component}.kt 2025-07-11 16:04:05 +07:00
7148ee66f8 feat: Rename strings 2025-07-10 22:29:25 +02:00
1510aa58f2 chore: Merge branch 'compose-dev' into feat/tooltip 2025-07-09 21:01:08 +07:00
f7a4ae5791 fix: Add missing header for "Updates" settings (#2642) 2025-07-08 21:57:56 +02:00
cb2dbbee24 feat: Improve bundle info screen design (#2548) 2025-07-08 20:23:03 +02:00
578dcce9b6 chore: Merge branch 'dev' into compose-dev 2025-07-08 22:24:17 +07:00
8c6c0f3c76 fix: Patch selection screen padding (#2533) 2025-07-08 17:20:44 +02:00
979a2dc410 fix: Playback Switch's Haptic Feedback (#2639)
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-07-08 18:11:45 +07:00
baa9122a88 fix: Improve background running notification (#2614) 2025-07-07 22:38:41 +02:00
111c74329f feat: Enable tooltip for unobvious elements 2025-07-06 00:47:40 +07:00
b70fc03bc7 fix: Allow different app version when downloading via plugin if setting is off (#2579)
Co-authored-by: Ax333l <main@axelen.xyz>
2025-07-05 18:44:11 +02:00
81a4ebd327 fix: display version from manifest (#2634) 2025-07-04 18:58:11 +02:00
83fc7f131a chore: Remove obsolete deleteLastPatchedApp call 2025-06-02 17:12:50 +03:00
8e4a9088ea ci: Use install instead of clean install for NPM dependencies
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2025-06-02 16:05:59 +02:00
7ee2b1a026 chore: Sync translations (#2522)
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 15:36:42 +07:00
40c99ab4dc ci: Set missing translation actor/email for commit
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 15:18:37 +07:00
1752fae9d9 chore: Better ignore rules
Based on https://github.com/github/gitignore/blob/main/Flutter.gitignore

Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 15:12:26 +07:00
d264a2a363 build: Support Flutter v3.32
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 15:11:49 +07:00
a5e909cfc8 fix: Obscure Flutter Impeller renderer bugs
This is workaround to entirely disabling Flutter Impeller in favour of Skia.

Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2025-05-31 14:57:21 +07:00
49 changed files with 904 additions and 261 deletions

View File

@ -13,6 +13,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Gradle - name: Cache Gradle
uses: burrunan/gradle-cache-action@v1 uses: burrunan/gradle-cache-action@v1

View File

@ -18,6 +18,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v4 uses: actions/setup-java@v4

View File

@ -12,7 +12,7 @@ interface PatchBundleDao {
fun getPropsById(uid: Int): Flow<BundleProperties?> fun getPropsById(uid: Int): Flow<BundleProperties?>
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid") @Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
suspend fun updateVersion(uid: Int, patches: String?) suspend fun updateVersionHash(uid: Int, patches: String?)
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid") @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
suspend fun setAutoUpdate(uid: Int, value: Boolean) suspend fun setAutoUpdate(uid: Int, value: Boolean)
@ -26,7 +26,7 @@ interface PatchBundleDao {
@Transaction @Transaction
suspend fun reset() { suspend fun reset() {
purgeCustomBundles() purgeCustomBundles()
updateVersion(0, null) // Reset the main source updateVersionHash(0, null) // Reset the main source
} }
@Query("DELETE FROM patch_bundles WHERE uid = :uid") @Query("DELETE FROM patch_bundles WHERE uid = :uid")

View File

@ -33,12 +33,12 @@ sealed class Source {
data class PatchBundleEntity( data class PatchBundleEntity(
@PrimaryKey val uid: Int, @PrimaryKey val uid: Int,
@ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "version") val version: String? = null, @ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "source") val source: Source, @ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean @ColumnInfo(name = "auto_update") val autoUpdate: Boolean
) )
data class BundleProperties( data class BundleProperties(
@ColumnInfo(name = "version") val version: String? = null, @ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean @ColumnInfo(name = "auto_update") val autoUpdate: Boolean
) )

View File

@ -15,7 +15,7 @@ class LocalPatchBundle(name: String, id: Int, directory: File) :
} }
reload()?.also { reload()?.also {
saveVersion(it.readManifestAttribute("Version")) saveVersionHash(it.patchBundleManifestAttributes?.version)
} }
} }
} }

View File

@ -34,10 +34,13 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
private val _nameFlow = MutableStateFlow(initialName) private val _nameFlow = MutableStateFlow(initialName)
val nameFlow = val nameFlow =
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } } _nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.patches_name_default else R.string.patches_name_fallback) } }
suspend fun getName() = nameFlow.first() suspend fun getName() = nameFlow.first()
val versionFlow = state.map { it.patchBundleOrNull()?.patchBundleManifestAttributes?.version }
val patchCountFlow = state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
/** /**
* Returns true if the bundle has been downloaded to local storage. * Returns true if the bundle has been downloaded to local storage.
*/ */
@ -71,7 +74,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
val bundle = newState.patchBundleOrNull() val bundle = newState.patchBundleOrNull()
// Try to read the name from the patch bundle manifest if the bundle does not have a name. // Try to read the name from the patch bundle manifest if the bundle does not have a name.
if (bundle != null && _nameFlow.value.isEmpty()) { if (bundle != null && _nameFlow.value.isEmpty()) {
bundle.readManifestAttribute("Name")?.let { setName(it) } bundle.patchBundleManifestAttributes?.name?.let { setName(it) }
} }
return bundle return bundle
@ -84,9 +87,9 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default) fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
suspend fun getProps() = propsFlow().first()!! suspend fun getProps() = propsFlow().first()!!
suspend fun currentVersion() = getProps().version suspend fun currentVersionHash() = getProps().versionHash
protected suspend fun saveVersion(version: String?) = protected suspend fun saveVersionHash(version: String?) =
configRepository.updateVersion(uid, version) configRepository.updateVersionHash(uid, version)
suspend fun setName(name: String) { suspend fun setName(name: String) {
configRepository.setName(uid, name) configRepository.setName(uid, name)

View File

@ -25,7 +25,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
} }
} }
saveVersion(info.version) saveVersionHash(info.version)
reload() reload()
} }
@ -35,7 +35,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
suspend fun update(): Boolean = withContext(Dispatchers.IO) { suspend fun update(): Boolean = withContext(Dispatchers.IO) {
val info = getLatestInfo() val info = getLatestInfo()
if (hasInstalled() && info.version == currentVersion()) if (hasInstalled() && info.version == currentVersionHash())
return@withContext false return@withContext false
download(info) download(info)
@ -50,7 +50,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value) suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
companion object { companion object {
const val updateFailMsg = "Failed to update patch bundle(s)" const val updateFailMsg = "Failed to update patches"
} }
} }

View File

@ -40,6 +40,8 @@ class DownloadedAppRepository(
data: Parcelable, data: Parcelable,
expectedPackageName: String, expectedPackageName: String,
expectedVersion: String?, expectedVersion: String?,
appCompatibilityCheck: Boolean,
patchesCompatibilityCheck: Boolean,
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit, onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
): File { ): File {
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here. // Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
@ -96,7 +98,12 @@ class DownloadedAppRepository(
val pkgInfo = val pkgInfo =
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid") pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}") if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}") expectedVersion?.let {
if (
pkgInfo.versionName != expectedVersion &&
(appCompatibilityCheck || patchesCompatibilityCheck)
) error("The selected app version ($pkgInfo.versionName) doesn't match the suggested version. Please use the suggested version ($expectedVersion), or adjust your settings by disabling \"Require suggested app version\" and enabling \"Disable version compatibility check\".")
}
// Delete the previous copy (if present). // Delete the previous copy (if present).
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let { dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {

View File

@ -25,7 +25,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
PatchBundleEntity( PatchBundleEntity(
uid = generateUid(), uid = generateUid(),
name = name, name = name,
version = null, versionHash = null,
source = source, source = source,
autoUpdate = autoUpdate autoUpdate = autoUpdate
).also { ).also {
@ -34,8 +34,11 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun delete(uid: Int) = dao.remove(uid) suspend fun delete(uid: Int) = dao.remove(uid)
suspend fun updateVersion(uid: Int, version: String?) = /**
dao.updateVersion(uid, version) * Sets the version hash used for updates.
*/
suspend fun updateVersionHash(uid: Int, versionHash: String?) =
dao.updateVersionHash(uid, versionHash)
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value) suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
@ -47,7 +50,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
val defaultSource = PatchBundleEntity( val defaultSource = PatchBundleEntity(
uid = 0, uid = 0,
name = "", name = "",
version = null, versionHash = null,
source = Source.API, source = Source.API,
autoUpdate = false autoUpdate = false
) )

View File

@ -165,7 +165,7 @@ class PatchBundleRepository(
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() } getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
suspend fun updateCheck() = suspend fun updateCheck() =
uiSafe(app, R.string.source_download_fail, "Failed to update bundles") { uiSafe(app, R.string.patches_download_fail, "Failed to update bundles") {
coroutineScope { coroutineScope {
if (!networkInfo.isSafe()) { if (!networkInfo.isSafe()) {
Log.d(tag, "Skipping update check because the network is down or metered.") Log.d(tag, "Skipping update check because the network is down or metered.")

View File

@ -8,6 +8,17 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.jar.JarFile import java.util.jar.JarFile
class PatchBundleManifestAttributes(
val name: String?,
val version: String?,
val description: String?,
val source: String?,
val author: String?,
val contact: String?,
val website: String?,
val license: String?
)
class PatchBundle(val patchesJar: File) { class PatchBundle(val patchesJar: File) {
private val loader = object : Iterable<Patch<*>> { private val loader = object : Iterable<Patch<*>> {
private fun load(): Iterable<Patch<*>> { private fun load(): Iterable<Patch<*>> {
@ -36,7 +47,20 @@ class PatchBundle(val patchesJar: File) {
null null
} }
fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name) val patchBundleManifestAttributes = if(manifest != null)
PatchBundleManifestAttributes(
name = readManifestAttribute("name"),
version = readManifestAttribute("version"),
description = readManifestAttribute("description"),
source = readManifestAttribute("source"),
author = readManifestAttribute("author"),
contact = readManifestAttribute("contact"),
website = readManifestAttribute("website"),
license = readManifestAttribute("license")
) else
null
private fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)?.takeIf { it.isNotBlank() } // If empty, set it to null instead.
/** /**
* Load all patches compatible with the specified package. * Load all patches compatible with the specified package.

View File

@ -14,9 +14,9 @@ import android.os.Parcelable
import android.os.PowerManager import android.os.PowerManager
import android.util.Log import android.util.Log
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.core.content.ContextCompat
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import app.revanced.manager.MainActivity
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstallType
@ -88,22 +88,25 @@ class PatcherWorker(
) )
private fun createNotification(): Notification { private fun createNotification(): Notification {
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java) val notificationIntent = Intent(applicationContext, MainActivity::class.java).apply {
val pendingIntent: PendingIntent = PendingIntent.getActivity( flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
) )
val channel = NotificationChannel( val channel = NotificationChannel(
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH "revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW
) )
val notificationManager = val notificationManager =
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) applicationContext.getSystemService(NotificationManager::class.java)
notificationManager!!.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
return Notification.Builder(applicationContext, channel.id) return Notification.Builder(applicationContext, channel.id)
.setContentTitle(applicationContext.getText(R.string.app_name)) .setContentTitle(applicationContext.getText(R.string.patcher_notification_title))
.setContentText(applicationContext.getText(R.string.patcher_notification_message)) .setContentText(applicationContext.getText(R.string.patcher_notification_text))
.setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification)) .setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
.setContentIntent(pendingIntent).build() .setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
} }
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
@ -158,6 +161,8 @@ class PatcherWorker(
data, data,
args.packageName, args.packageName,
args.input.version, args.input.version,
prefs.suggestedVersionSafeguard.get(),
!prefs.disablePatchVersionCompatCheck.get(),
onDownload = args.onDownloadProgress onDownload = args.onDownloadProgress
).also { ).also {
args.setInputFile(it) args.setInputFile(it)

View File

@ -6,7 +6,6 @@ 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.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -22,6 +21,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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.tooltip.TooltipIconButton
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -69,7 +69,11 @@ fun AppTopBar(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
navigationIcon = { navigationIcon = {
if (onBackClick != null) { if (onBackClick != null) {
IconButton(onClick = onBackClick) { TooltipIconButton(
modifier = Modifier,
onClick = onBackClick,
tooltip = stringResource(R.string.back),
) {
backIcon() backIcon()
} }
} }
@ -108,7 +112,11 @@ fun AppTopBar(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
navigationIcon = { navigationIcon = {
if (onBackClick != null) { if (onBackClick != null) {
IconButton(onClick = onBackClick) { TooltipIconButton(
modifier = Modifier,
onClick = onBackClick,
tooltip = stringResource(R.string.back),
) {
backIcon() backIcon()
} }
} }

View File

@ -4,13 +4,13 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@Composable @Composable
fun ArrowButton( fun ArrowButton(
@ -27,7 +27,11 @@ fun ArrowButton(
) )
onClick?.let { onClick?.let {
IconButton(onClick = it) { TooltipIconButton(
modifier = Modifier,
onClick = it,
tooltip = stringResource(description),
) {
Icon( Icon(
imageVector = Icons.Filled.KeyboardArrowUp, imageVector = Icons.Filled.KeyboardArrowUp,
contentDescription = stringResource(description), contentDescription = stringResource(description),

View File

@ -9,7 +9,6 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.Share import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -18,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -30,7 +30,7 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
Scaffold( Scaffold(
topBar = { topBar = {
BundleTopBar( BundleTopBar(
title = stringResource(R.string.bundle_error), title = stringResource(R.string.patches_error),
onBackClick = onDismiss, onBackClick = onDismiss,
backIcon = { backIcon = {
Icon( Icon(
@ -39,7 +39,8 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
) )
}, },
actions = { actions = {
IconButton( TooltipIconButton(
modifier = Modifier,
onClick = { onClick = {
val sendIntent: Intent = Intent().apply { val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
@ -52,7 +53,8 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
val shareIntent = Intent.createChooser(sendIntent, null) val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent) context.startActivity(shareIntent)
} },
tooltip = stringResource(R.string.share),
) { ) {
Icon( Icon(
Icons.Outlined.Share, Icons.Outlined.Share,

View File

@ -14,7 +14,6 @@ import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -25,6 +24,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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.tooltip.TooltipIconButton
@Composable @Composable
fun NotificationCard( fun NotificationCard(
@ -138,7 +138,11 @@ fun NotificationCard(
) )
} }
if (onDismiss != null) { if (onDismiss != null) {
IconButton(onClick = onDismiss) { TooltipIconButton(
modifier = modifier,
onClick = onDismiss,
tooltip = stringResource(R.string.close),
) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.close), contentDescription = stringResource(R.string.close),

View File

@ -5,7 +5,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -19,6 +18,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@Composable @Composable
fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null) { fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null) {
@ -33,9 +33,15 @@ fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (
label = label, label = label,
modifier = modifier, modifier = modifier,
trailingIcon = { trailingIcon = {
IconButton(onClick = { TooltipIconButton(
modifier = Modifier,
onClick = {
visible = !visible visible = !visible
}) { },
tooltip = if (visible) stringResource(R.string.show_password_field) else stringResource(
R.string.hide_password_field
),
) {
val (icon, description) = remember(visible) { val (icon, description) = remember(visible) {
if (visible) Icons.Outlined.VisibilityOff to R.string.hide_password_field else Icons.Outlined.Visibility to R.string.show_password_field if (visible) Icons.Outlined.VisibilityOff to R.string.hide_password_field else Icons.Outlined.Visibility to R.string.show_password_field
} }

View File

@ -5,7 +5,6 @@ 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.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarColors import androidx.compose.material3.SearchBarColors
@ -19,6 +18,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -48,7 +48,11 @@ fun SearchView(
onExpandedChange = onActiveChange, onExpandedChange = onActiveChange,
placeholder = placeholder, placeholder = placeholder,
leadingIcon = { leadingIcon = {
IconButton(onClick = { onActiveChange(false) }) { TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.back),
onClick = { onActiveChange(false) }
) {
Icon( Icon(
Icons.AutoMirrored.Filled.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
stringResource(R.string.back) stringResource(R.string.back)

View File

@ -2,13 +2,26 @@ 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.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.material.icons.outlined.Extension import androidx.compose.material.icons.automirrored.outlined.Send
import androidx.compose.material.icons.outlined.Inventory2 import androidx.compose.material.icons.outlined.Commit
import androidx.compose.material.icons.outlined.Description
import androidx.compose.material.icons.outlined.Gavel
import androidx.compose.material.icons.outlined.Language
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.Sell import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material3.* import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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
@ -17,10 +30,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
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.patcher.patch.PatchBundleManifestAttributes
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.ui.component.haptics.HapticSwitch import app.revanced.manager.ui.component.haptics.HapticSwitch
@ -29,12 +43,12 @@ import app.revanced.manager.ui.component.haptics.HapticSwitch
fun BaseBundleDialog( fun BaseBundleDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDefault: Boolean, isDefault: Boolean,
name: String?,
remoteUrl: String?, remoteUrl: String?,
onRemoteUrlChange: ((String) -> Unit)? = null, onRemoteUrlChange: ((String) -> Unit)? = null,
patchCount: Int, patchCount: Int,
version: String?, version: String?,
autoUpdate: Boolean, autoUpdate: Boolean,
bundleManifestAttributes: PatchBundleManifestAttributes?,
onAutoUpdateChange: (Boolean) -> Unit, onAutoUpdateChange: (Boolean) -> Unit,
onPatchesClick: () -> Unit, onPatchesClick: () -> Unit,
extraFields: @Composable ColumnScope.() -> Unit = {} extraFields: @Composable ColumnScope.() -> Unit = {}
@ -47,36 +61,27 @@ fun BaseBundleDialog(
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Inventory2,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
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 { version?.let {
Tag(Icons.Outlined.Sell, it) Tag(Icons.Outlined.Sell, it)
} }
Tag(Icons.Outlined.Extension, patchCount.toString()) bundleManifestAttributes?.description?.let {
Tag(Icons.Outlined.Description, it)
}
bundleManifestAttributes?.source?.let {
Tag(Icons.Outlined.Commit, it)
}
bundleManifestAttributes?.author?.let {
Tag(Icons.Outlined.Person, it)
}
bundleManifestAttributes?.contact?.let {
Tag(Icons.AutoMirrored.Outlined.Send, it)
}
bundleManifestAttributes?.website?.let {
Tag(Icons.Outlined.Language, it, isUrl = true)
}
bundleManifestAttributes?.license?.let {
Tag(Icons.Outlined.Gavel, it)
} }
} }
@ -87,8 +92,8 @@ fun BaseBundleDialog(
if (remoteUrl != null) { if (remoteUrl != null) {
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.bundle_auto_update), headlineText = stringResource(R.string.auto_update),
supportingText = stringResource(R.string.bundle_auto_update_description), supportingText = stringResource(R.string.auto_update_description),
trailingContent = { trailingContent = {
HapticSwitch( HapticSwitch(
checked = autoUpdate, checked = autoUpdate,
@ -108,7 +113,7 @@ fun BaseBundleDialog(
if (showUrlInputDialog) { if (showUrlInputDialog) {
TextInputDialog( TextInputDialog(
initial = url, initial = url,
title = stringResource(R.string.bundle_input_source_url), title = stringResource(R.string.patches_url),
onDismissRequest = { showUrlInputDialog = false }, onDismissRequest = { showUrlInputDialog = false },
onConfirm = { onConfirm = {
showUrlInputDialog = false showUrlInputDialog = false
@ -129,7 +134,7 @@ fun BaseBundleDialog(
showUrlInputDialog = true showUrlInputDialog = true
} }
), ),
headlineText = stringResource(R.string.bundle_input_source_url), headlineText = stringResource(R.string.patches_url),
supportingText = url.ifEmpty { supportingText = url.ifEmpty {
stringResource(R.string.field_not_set) stringResource(R.string.field_not_set)
} }
@ -139,7 +144,7 @@ fun BaseBundleDialog(
val patchesClickable = patchCount > 0 val patchesClickable = patchCount > 0
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.patches), headlineText = stringResource(R.string.patches),
supportingText = stringResource(R.string.bundle_view_patches), supportingText = stringResource(R.string.view_patches),
modifier = Modifier.clickable( modifier = Modifier.clickable(
enabled = patchesClickable, enabled = patchesClickable,
onClick = onPatchesClick onClick = onPatchesClick
@ -160,22 +165,34 @@ fun BaseBundleDialog(
@Composable @Composable
private fun Tag( private fun Tag(
icon: ImageVector, icon: ImageVector,
text: String text: String,
isUrl: Boolean = false
) { ) {
val uriHandler = LocalUriHandler.current
Row( Row(
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
modifier = if (isUrl) {
Modifier
.clickable {
try {
uriHandler.openUri(text)
} catch (_: Exception) {}
}
}
else
Modifier,
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp)
tint = MaterialTheme.colorScheme.outline,
) )
Text( Text(
text, text,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline, color = if(isUrl) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
) )
} }
} }

View File

@ -22,6 +22,7 @@ import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefaul
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import app.revanced.manager.ui.component.ExceptionViewerDialog import app.revanced.manager.ui.component.ExceptionViewerDialog
import app.revanced.manager.ui.component.FullscreenDialog import app.revanced.manager.ui.component.FullscreenDialog
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
@ -42,16 +43,16 @@ fun BundleInformationDialog(
val props by remember(bundle) { val props by remember(bundle) {
bundle.propsFlow() bundle.propsFlow()
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
val patchCount = remember(state) { val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0)
state.patchBundleOrNull()?.patches?.size ?: 0 val version by bundle.versionFlow.collectAsStateWithLifecycle(null)
} val bundleManifestAttributes = state.patchBundleOrNull()?.patchBundleManifestAttributes
if (viewCurrentBundlePatches) { if (viewCurrentBundlePatches) {
BundlePatchesDialog( BundlePatchesDialog(
onDismissRequest = { onDismissRequest = {
viewCurrentBundlePatches = false viewCurrentBundlePatches = false
}, },
bundle = bundle, bundle = bundle
) )
} }
@ -63,7 +64,7 @@ fun BundleInformationDialog(
Scaffold( Scaffold(
topBar = { topBar = {
BundleTopBar( BundleTopBar(
title = stringResource(R.string.patch_bundle_field), title = bundleName,
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
backIcon = { backIcon = {
Icon( Icon(
@ -73,7 +74,11 @@ fun BundleInformationDialog(
}, },
actions = { actions = {
if (!bundle.isDefault) { if (!bundle.isDefault) {
IconButton(onClick = onDeleteRequest) { TooltipIconButton(
modifier = Modifier,
onClick = onDeleteRequest,
tooltip = stringResource(R.string.delete),
) {
Icon( Icon(
Icons.Outlined.DeleteOutline, Icons.Outlined.DeleteOutline,
stringResource(R.string.delete) stringResource(R.string.delete)
@ -81,7 +86,11 @@ fun BundleInformationDialog(
} }
} }
if (!isLocal && hasNetwork) { if (!isLocal && hasNetwork) {
IconButton(onClick = onUpdate) { TooltipIconButton(
modifier = Modifier,
onClick = onUpdate,
tooltip = stringResource(R.string.refresh),
) {
Icon( Icon(
Icons.Outlined.Update, Icons.Outlined.Update,
stringResource(R.string.refresh) stringResource(R.string.refresh)
@ -95,11 +104,11 @@ fun BundleInformationDialog(
BaseBundleDialog( BaseBundleDialog(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
isDefault = bundle.isDefault, isDefault = bundle.isDefault,
name = bundleName,
remoteUrl = bundle.asRemoteOrNull?.endpoint, remoteUrl = bundle.asRemoteOrNull?.endpoint,
patchCount = patchCount, patchCount = patchCount,
version = props?.version, version = version,
autoUpdate = props?.autoUpdate ?: false, autoUpdate = props?.autoUpdate == true,
bundleManifestAttributes = bundleManifestAttributes,
onAutoUpdateChange = { onAutoUpdateChange = {
composableScope.launch { composableScope.launch {
bundle.asRemoteOrNull?.setAutoUpdate(it) bundle.asRemoteOrNull?.setAutoUpdate(it)
@ -119,8 +128,8 @@ fun BundleInformationDialog(
) )
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.bundle_error), headlineText = stringResource(R.string.patches_error),
supportingText = stringResource(R.string.bundle_error_description), supportingText = stringResource(R.string.patches_error_description),
trailingContent = { trailingContent = {
Icon( Icon(
Icons.AutoMirrored.Outlined.ArrowRight, Icons.AutoMirrored.Outlined.ArrowRight,
@ -133,8 +142,8 @@ fun BundleInformationDialog(
if (state is PatchBundleSource.State.Missing && !isLocal) { if (state is PatchBundleSource.State.Missing && !isLocal) {
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.bundle_error), headlineText = stringResource(R.string.patches_error),
supportingText = stringResource(R.string.bundle_not_downloaded), supportingText = stringResource(R.string.patches_not_downloaded),
modifier = Modifier.clickable(onClick = onUpdate) modifier = Modifier.clickable(onClick = onUpdate)
) )
} }

View File

@ -47,9 +47,8 @@ fun BundleItem(
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
val version by remember(bundle) { val version by bundle.versionFlow.collectAsStateWithLifecycle(null)
bundle.propsFlow().map { props -> props?.version } val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0)
}.collectAsStateWithLifecycle(null)
val name by bundle.nameState val name by bundle.nameState
if (viewBundleDialogPage) { if (viewBundleDialogPage) {
@ -68,8 +67,8 @@ fun BundleItem(
onDelete() onDelete()
viewBundleDialogPage = false viewBundleDialogPage = false
}, },
title = stringResource(R.string.bundle_delete_single_dialog_title), title = stringResource(R.string.delete),
description = stringResource(R.string.bundle_delete_single_dialog_description, name), description = stringResource(R.string.patches_delete_single_dialog_description, name),
icon = Icons.Outlined.Delete icon = Icons.Outlined.Delete
) )
} }
@ -93,7 +92,7 @@ fun BundleItem(
headlineContent = { Text(name) }, headlineContent = { Text(name) },
supportingContent = { supportingContent = {
state.patchBundleOrNull()?.patches?.size?.let { patchCount -> if (state is PatchBundleSource.State.Loaded) {
Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount)) Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
} }
}, },
@ -101,8 +100,8 @@ fun BundleItem(
Row { Row {
val icon = remember(state) { val icon = remember(state) {
when (state) { when (state) {
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.patches_error
is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.patches_missing
is PatchBundleSource.State.Loaded -> null is PatchBundleSource.State.Loaded -> null
} }
} }

View File

@ -46,7 +46,7 @@ fun BundlePatchesDialog(
Scaffold( Scaffold(
topBar = { topBar = {
BundleTopBar( BundleTopBar(
title = stringResource(R.string.bundle_patches), title = stringResource(R.string.patches),
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
backIcon = { backIcon = {
Icon( Icon(
@ -133,10 +133,10 @@ fun PatchItem(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
PatchInfoChip( PatchInfoChip(
text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}" text = "$PACKAGE_ICON ${stringResource(R.string.patches_view_any_package)}"
) )
PatchInfoChip( PatchInfoChip(
text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}" text = "$VERSION_ICON ${stringResource(R.string.patches_view_any_version)}"
) )
} }
} else { } else {

View File

@ -13,14 +13,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -50,16 +50,14 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
.fillMaxWidth() .fillMaxWidth()
) { ) {
Text( Text(
text = "Select bundle", text = stringResource(R.string.select),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
} }
bundles.forEach { bundles.forEach {
val name by it.nameState val name by it.nameState
val version by remember(it) { val version by it.versionFlow.collectAsStateWithLifecycle(null)
it.propsFlow().map { props -> props?.version }
}.collectAsStateWithLifecycle(null)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,

View File

@ -2,7 +2,6 @@ package app.revanced.manager.ui.component.bundle
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@ -10,7 +9,11 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -33,7 +36,11 @@ fun BundleTopBar(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
navigationIcon = { navigationIcon = {
if (onBackClick != null) { if (onBackClick != null) {
IconButton(onClick = onBackClick) { TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.back),
onClick = onBackClick
) {
backIcon() backIcon()
} }
} }

View File

@ -77,7 +77,7 @@ fun ImportPatchBundleDialog(
AlertDialogExtended( AlertDialogExtended(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { title = {
Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patch_bundle)) Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patches))
}, },
text = { text = {
steps[currentStep]() steps[currentStep]()
@ -126,7 +126,7 @@ fun SelectBundleTypeStep(
) { ) {
Text( Text(
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.select_bundle_type_dialog_description) text = stringResource(R.string.select_patches_type_dialog_description)
) )
Column { Column {
ListItem( ListItem(
@ -136,7 +136,7 @@ fun SelectBundleTypeStep(
), ),
headlineContent = { Text(stringResource(R.string.enter_url)) }, headlineContent = { Text(stringResource(R.string.enter_url)) },
overlineContent = { Text(stringResource(R.string.recommended)) }, overlineContent = { Text(stringResource(R.string.recommended)) },
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) }, supportingContent = { Text(stringResource(R.string.remote_patches_description)) },
leadingContent = { leadingContent = {
HapticRadioButton( HapticRadioButton(
selected = bundleType == BundleType.Remote, selected = bundleType == BundleType.Remote,
@ -152,7 +152,7 @@ fun SelectBundleTypeStep(
onClick = { onBundleTypeSelected(BundleType.Local) } onClick = { onBundleTypeSelected(BundleType.Local) }
), ),
headlineContent = { Text(stringResource(R.string.select_from_storage)) }, headlineContent = { Text(stringResource(R.string.select_from_storage)) },
supportingContent = { Text(stringResource(R.string.local_bundle_description)) }, supportingContent = { Text(stringResource(R.string.local_patches_description)) },
overlineContent = { }, overlineContent = { },
leadingContent = { leadingContent = {
HapticRadioButton( HapticRadioButton(
@ -185,10 +185,11 @@ fun ImportBundleStep(
) { ) {
ListItem( ListItem(
headlineContent = { headlineContent = {
Text(stringResource(R.string.patch_bundle_field)) Text(stringResource(R.string.patches))
}, },
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) }, supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) },
trailingContent = { trailingContent = {
// TODO: Determine if this button should be [TooltipWrap]'ped
IconButton(onClick = launchPatchActivity) { IconButton(onClick = launchPatchActivity) {
Icon(imageVector = Icons.Default.Topic, contentDescription = null) Icon(imageVector = Icons.Default.Topic, contentDescription = null)
} }
@ -206,11 +207,11 @@ fun ImportBundleStep(
OutlinedTextField( OutlinedTextField(
value = remoteUrl, value = remoteUrl,
onValueChange = onRemoteUrlChange, onValueChange = onRemoteUrlChange,
label = { Text(stringResource(R.string.bundle_url)) } label = { Text(stringResource(R.string.patches_url)) }
) )
} }
Column( Column(
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp, vertical = 5.dp)
) { ) {
ListItem( ListItem(
modifier = Modifier.clickable( modifier = Modifier.clickable(

View File

@ -0,0 +1,38 @@
package app.revanced.manager.ui.component.haptics
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.FloatingActionButtonElevation
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticSmallFloatingActionButton (
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = FloatingActionButtonDefaults.smallShape,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit,
) {
SmallFloatingActionButton(
onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
modifier = modifier,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
interactionSource = interactionSource,
content = content
)
}

View File

@ -9,6 +9,7 @@ import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
@Composable @Composable
fun HapticSwitch( fun HapticSwitch(
@ -20,16 +21,19 @@ fun HapticSwitch(
colors: SwitchColors = SwitchDefaults.colors(), colors: SwitchColors = SwitchDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
val view = LocalView.current
Switch( Switch(
checked = checked, checked = checked,
onCheckedChange = { newChecked -> onCheckedChange = { newChecked ->
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
when { val hapticFeedbackType = when {
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF !newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
!newChecked -> HapticFeedbackConstants.CLOCK_TICK !newChecked -> HapticFeedbackConstants.CLOCK_TICK
else -> {HapticFeedbackConstants.VIRTUAL_KEY}
} }
view.performHapticFeedback(hapticFeedbackType)
onCheckedChange(newChecked) onCheckedChange(newChecked)
}, },
modifier = modifier, modifier = modifier,

View File

@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@ -68,19 +67,18 @@ import app.revanced.manager.ui.component.LongInputDialog
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticRadioButton import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.haptics.HapticSwitch import app.revanced.manager.ui.component.haptics.HapticSwitch
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.mutableStateSetOf import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.saver.snapshotStateListSaver import app.revanced.manager.util.saver.snapshotStateListSaver
import app.revanced.manager.util.saver.snapshotStateSetSaver import app.revanced.manager.util.saver.snapshotStateSetSaver
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyColumnState
import sh.calvin.reorderable.rememberReorderableLazyListState import sh.calvin.reorderable.rememberReorderableLazyListState
import java.io.Serializable import java.io.Serializable
import kotlin.random.Random import kotlin.random.Random
@ -113,7 +111,11 @@ private interface OptionEditor<T : Any> {
@Composable @Composable
fun ListItemTrailingContent(scope: OptionEditorScope<T>) { fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
IconButton(onClick = { clickAction(scope) }) { TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.edit),
onClick = { clickAction(scope) }
) {
Icon(Icons.Outlined.Edit, stringResource(R.string.edit)) Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
} }
} }
@ -249,13 +251,12 @@ private object StringOptionEditor : OptionEditor<String> {
}, },
trailingIcon = { trailingIcon = {
var showDropdownMenu by rememberSaveable { mutableStateOf(false) } var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
IconButton( TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.string_option_menu_description),
onClick = { showDropdownMenu = true } onClick = { showDropdownMenu = true }
) { ) {
Icon( Icon(Icons.Outlined.MoreVert, stringResource(R.string.string_option_menu_description))
Icons.Outlined.MoreVert,
stringResource(R.string.string_option_menu_description)
)
} }
DropdownMenu( DropdownMenu(
@ -551,7 +552,9 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
}, },
actions = { actions = {
if (deleteMode) { if (deleteMode) {
IconButton( TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.select_deselect_all),
onClick = { onClick = {
if (items.size == deletionTargets.size) deletionTargets.clear() if (items.size == deletionTargets.size) deletionTargets.clear()
else deletionTargets.addAll(items.map { it.key }) else deletionTargets.addAll(items.map { it.key })
@ -562,7 +565,9 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
stringResource(R.string.select_deselect_all) stringResource(R.string.select_deselect_all)
) )
} }
IconButton( TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.delete),
onClick = { onClick = {
items.removeIf { it.key in deletionTargets } items.removeIf { it.key in deletionTargets }
deletionTargets.clear() deletionTargets.clear()
@ -575,8 +580,15 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
) )
} }
} else { } else {
IconButton(onClick = items::clear) { TooltipIconButton(
Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) modifier = Modifier,
tooltip = stringResource(R.string.reset),
onClick = items::clear
) {
Icon(
Icons.Outlined.Restore,
stringResource(R.string.reset)
)
} }
} }
} }
@ -643,9 +655,10 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
), ),
tonalElevation = if (deleteMode && item.key in deletionTargets) 8.dp else 0.dp, tonalElevation = if (deleteMode && item.key in deletionTargets) 8.dp else 0.dp,
leadingContent = { leadingContent = {
IconButton( TooltipIconButton(
modifier = Modifier.draggableHandle(interactionSource = interactionSource), modifier = Modifier.draggableHandle(interactionSource = interactionSource),
onClick = {}, tooltip = stringResource(R.string.delete),
onClick = { }
) { ) {
Icon( Icon(
Icons.Filled.DragHandle, Icons.Filled.DragHandle,

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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
@ -17,6 +16,7 @@ import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.manager.base.Preference import app.revanced.manager.domain.manager.base.Preference
import app.revanced.manager.ui.component.IntInputDialog import app.revanced.manager.ui.component.IntInputDialog
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -65,10 +65,14 @@ fun IntegerItem(
headlineContent = stringResource(headline), headlineContent = stringResource(headline),
supportingContent = stringResource(description), supportingContent = stringResource(description),
trailingContent = { trailingContent = {
IconButton(onClick = { dialogOpen = true }) { TooltipIconButton(
modifier = modifier,
onClick = { dialogOpen = true },
tooltip = stringResource(R.string.edit),
) {
Icon( Icon(
Icons.Outlined.Edit, imageVector = Icons.Outlined.Edit,
contentDescription = stringResource(R.string.edit) contentDescription = stringResource(R.string.edit),
) )
} }
} }

View File

@ -0,0 +1,109 @@
package app.revanced.manager.ui.component.tooltip
import androidx.annotation.StringRes
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.FloatingActionButtonElevation
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.window.PopupPositionProvider
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
/**
* [HapticFloatingActionButton] with tooltip-specific params.
*
* @param tooltip [String] text to show in a tooltip.
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
* @param haptic Whether to perform haptic feedback when the tooltip shown.
* @param hapticFeedbackType The type of haptic feedback to perform.
*
* @see [HapticFloatingActionButton]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipFloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = FloatingActionButtonDefaults.shape,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
tooltip: String,
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
haptic: Boolean = true,
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
content: @Composable (() -> Unit)
) {
TooltipWrap(
modifier = modifier,
tooltip = tooltip,
positionProvider = positionProvider,
haptic = haptic,
hapticFeedbackType = hapticFeedbackType,
) {
HapticFloatingActionButton(
onClick = onClick,
modifier = modifier,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
interactionSource = interactionSource,
content = content,
)
}
}
/**
* [HapticFloatingActionButton] with tooltip-specific params.
*
* @param tooltip [Int] or `id` string resource to show in a tooltip.
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
* @param haptic Whether to perform haptic feedback when the tooltip shown.
* @param hapticFeedbackType The type of haptic feedback to perform.
*
* @see [HapticFloatingActionButton]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipFloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = FloatingActionButtonDefaults.shape,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@StringRes tooltip: Int,
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
haptic: Boolean = true,
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
content: @Composable (() -> Unit)
) {
TooltipWrap(
modifier = modifier,
tooltip = tooltip,
positionProvider = positionProvider,
haptic = haptic,
hapticFeedbackType = hapticFeedbackType,
) {
HapticFloatingActionButton(
onClick = onClick,
modifier = modifier,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
interactionSource = interactionSource,
content = content,
)
}
}

View File

@ -0,0 +1,98 @@
package app.revanced.manager.ui.component.tooltip
import androidx.annotation.StringRes
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.TooltipDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.window.PopupPositionProvider
/**
* [IconButton] with tooltip-specific params.
*
* @param tooltip [String] text to show in a tooltip.
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
* @param haptic Whether to perform haptic feedback when the tooltip shown.
* @param hapticFeedbackType The type of haptic feedback to perform.
*
* @see [IconButton]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipIconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
tooltip: String,
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
haptic: Boolean = true,
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
content: @Composable (() -> Unit),
) {
TooltipWrap(
modifier = modifier,
tooltip = tooltip,
positionProvider = positionProvider,
haptic = haptic,
hapticFeedbackType = hapticFeedbackType,
) {
IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = colors,
interactionSource = interactionSource,
content = content,
)
}
}
/**
* [IconButton] with tooltip-specific params.
*
* @param tooltip [Int] or `id` string resource to show in a tooltip.
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
* @param haptic Whether to perform haptic feedback when the tooltip shown.
* @param hapticFeedbackType The type of haptic feedback to perform.
*
* @see [IconButton]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipIconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@StringRes tooltip: Int,
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
haptic: Boolean = true,
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
content: @Composable (() -> Unit),
) {
TooltipWrap(
modifier = modifier,
tooltip = tooltip,
positionProvider = positionProvider,
haptic = haptic,
hapticFeedbackType = hapticFeedbackType,
) {
IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = colors,
interactionSource = interactionSource,
content = content,
)
}
}

View File

@ -0,0 +1,109 @@
package app.revanced.manager.ui.component.tooltip
import androidx.annotation.StringRes
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.FloatingActionButtonElevation
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.window.PopupPositionProvider
import app.revanced.manager.ui.component.haptics.HapticSmallFloatingActionButton
/**
* [HapticSmallFloatingActionButton] with tooltip-specific params.
*
* @param tooltip [String] text to show in a tooltip.
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
* @param haptic Whether to perform haptic feedback when the tooltip shown.
* @param hapticFeedbackType The type of haptic feedback to perform.
*
* @see [HapticSmallFloatingActionButton]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipSmallFloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = FloatingActionButtonDefaults.smallShape,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
tooltip: String,
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
haptic: Boolean = true,
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
content: @Composable (() -> Unit)
) {
TooltipWrap(
modifier = modifier,
tooltip = tooltip,
positionProvider = positionProvider,
haptic = haptic,
hapticFeedbackType = hapticFeedbackType,
) {
HapticSmallFloatingActionButton(
onClick = onClick,
modifier = modifier,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
interactionSource = interactionSource,
content = content,
)
}
}
/**
* [HapticSmallFloatingActionButton] with tooltip-specific params.
*
* @param tooltip [Int] or `id` string resource to show in a tooltip.
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
* @param haptic Whether to perform haptic feedback when the tooltip shown.
* @param hapticFeedbackType The type of haptic feedback to perform.
*
* @see [HapticSmallFloatingActionButton]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipSmallFloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = FloatingActionButtonDefaults.smallShape,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@StringRes tooltip: Int,
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
haptic: Boolean = true,
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
content: @Composable (() -> Unit)
) {
TooltipWrap(
modifier = modifier,
tooltip = tooltip,
positionProvider = positionProvider,
haptic = haptic,
hapticFeedbackType = hapticFeedbackType,
) {
HapticSmallFloatingActionButton(
onClick = onClick,
modifier = modifier,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
interactionSource = interactionSource,
content = content,
)
}
}

View File

@ -0,0 +1,96 @@
package app.revanced.manager.ui.component.tooltip
import androidx.annotation.StringRes
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.PopupPositionProvider
/**
* Wraps a composable with a tooltip.
*
* @param modifier the [Modifier] to applied to Tooltip.
* @param tooltip [String] text to show in a tooltip.
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
* @param content The composable UI to wrapped with.
* @param haptic Whether to perform haptic feedback when the tooltip shown.
* @param hapticFeedbackType The type of haptic feedback to perform.
*
* @see [TooltipBox]
*/
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun TooltipWrap(
modifier: Modifier,
tooltip: String,
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
haptic: Boolean = true,
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
content: @Composable () -> Unit
) {
val tooltipState = rememberTooltipState()
val localHaptic = LocalHapticFeedback.current
LaunchedEffect(tooltipState.isVisible) {
if (tooltipState.isVisible && haptic) {
localHaptic.performHapticFeedback(hapticFeedbackType)
}
}
TooltipBox(
modifier = modifier,
positionProvider = positionProvider,
tooltip = { PlainTooltip { Text(tooltip) } },
state = tooltipState,
content = content,
)
}
/**
* Wraps a composable with a tooltip.
*
* @param modifier the [Modifier] to applied to tooltip.
* @param tooltip [Int] or `id` string resource to show in a tooltip.
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
* @param content The composable UI to wrapped with.
* @param haptic Whether to perform haptic feedback when the tooltip shown.
* @param hapticFeedbackType The type of haptic feedback to perform.
*
* @see [TooltipBox]
*/
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun TooltipWrap(
modifier: Modifier,
@StringRes tooltip: Int,
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
haptic: Boolean = true,
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
content: @Composable () -> Unit
) {
val tooltipState = rememberTooltipState()
val localHaptic = LocalHapticFeedback.current
LaunchedEffect(tooltipState.isVisible) {
if (tooltipState.isVisible && haptic) {
localHaptic.performHapticFeedback(hapticFeedbackType)
}
}
TooltipBox(
modifier = modifier,
positionProvider = positionProvider,
tooltip = { PlainTooltip { Text(stringResource(tooltip)) } },
state = tooltipState,
content = content,
)
}

View File

@ -23,8 +23,6 @@ data class BundleInfo(
yieldAll(universal) yieldAll(universal)
} }
val patchCount get() = compatible.size + incompatible.size + universal.size
fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) { fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) {
all all
} else { } else {
@ -79,7 +77,7 @@ data class BundleInfo(
targetList.add(it) targetList.add(it)
} }
BundleInfo(source.getName(), source.currentVersion(), source.uid, compatible, incompatible, universal) BundleInfo(source.getName(), bundle.patchBundleManifestAttributes?.version, source.uid, compatible, incompatible, universal)
} }
} }

View File

@ -16,7 +16,6 @@ import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -44,6 +43,7 @@ import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.NonSuggestedVersionDialog import app.revanced.manager.ui.component.NonSuggestedVersionDialog
import app.revanced.manager.ui.component.SearchView import app.revanced.manager.ui.component.SearchView
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.APK_MIMETYPE
@ -162,8 +162,15 @@ fun AppSelectorScreen(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
onBackClick = onBackClick, onBackClick = onBackClick,
actions = { actions = {
IconButton(onClick = { search = true }) { TooltipIconButton(
Icon(Icons.Outlined.Search, stringResource(R.string.search)) modifier = Modifier,
tooltip = stringResource(R.string.search_patches),
onClick = { search = true }
) {
Icon(
Icons.Outlined.Search,
stringResource(R.string.search)
)
} }
} }
) )

View File

@ -62,12 +62,13 @@ import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.component.AvailableUpdateDialog import app.revanced.manager.ui.component.AvailableUpdateDialog
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.ConfirmDialog import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.tooltip.TooltipFloatingActionButton
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.util.RequestInstallAppsContract import app.revanced.manager.util.RequestInstallAppsContract
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@ -79,7 +80,7 @@ enum class DashboardPage(
val icon: ImageVector val icon: ImageVector
) { ) {
DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps), DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps),
BUNDLES(R.string.tab_bundles, Icons.Outlined.Source), BUNDLES(R.string.tab_patches, Icons.Outlined.Source),
} }
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
@ -93,7 +94,7 @@ fun DashboardScreen(
onDownloaderPluginClick: () -> Unit, onDownloaderPluginClick: () -> Unit,
onAppClick: (String) -> Unit onAppClick: (String) -> Unit
) { ) {
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.isNotEmpty() } }
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle( val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
false false
@ -164,8 +165,8 @@ fun DashboardScreen(
vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) }
vm.cancelSourceSelection() vm.cancelSourceSelection()
}, },
title = stringResource(R.string.bundle_delete_multiple_dialog_title), title = stringResource(R.string.delete),
description = stringResource(R.string.bundle_delete_multiple_dialog_description), description = stringResource(R.string.patches_delete_multiple_dialog_description),
icon = Icons.Outlined.Delete icon = Icons.Outlined.Delete
) )
} }
@ -174,7 +175,7 @@ fun DashboardScreen(
topBar = { topBar = {
if (bundlesSelectable) { if (bundlesSelectable) {
BundleTopBar( BundleTopBar(
title = stringResource(R.string.bundles_selected, vm.selectedSources.size), title = stringResource(R.string.patches_selected, vm.selectedSources.size),
onBackClick = vm::cancelSourceSelection, onBackClick = vm::cancelSourceSelection,
backIcon = { backIcon = {
Icon( Icon(
@ -183,21 +184,25 @@ fun DashboardScreen(
) )
}, },
actions = { actions = {
IconButton( TooltipIconButton(
modifier = Modifier,
onClick = { onClick = {
showDeleteConfirmationDialog = true showDeleteConfirmationDialog = true
} },
tooltip = stringResource(R.string.delete),
) { ) {
Icon( Icon(
Icons.Outlined.DeleteOutline, Icons.Outlined.DeleteOutline,
stringResource(R.string.delete) stringResource(R.string.delete)
) )
} }
IconButton( TooltipIconButton(
modifier = Modifier,
onClick = { onClick = {
vm.selectedSources.forEach { vm.update(it) } vm.selectedSources.forEach { vm.update(it) }
vm.cancelSourceSelection() vm.cancelSourceSelection()
} },
tooltip = stringResource(R.string.refresh),
) { ) {
Icon( Icon(
Icons.Outlined.Refresh, Icons.Outlined.Refresh,
@ -211,8 +216,10 @@ fun DashboardScreen(
title = stringResource(R.string.app_name), title = stringResource(R.string.app_name),
actions = { actions = {
if (!vm.updatedManagerVersion.isNullOrEmpty()) { if (!vm.updatedManagerVersion.isNullOrEmpty()) {
IconButton( TooltipIconButton(
modifier = Modifier,
onClick = onUpdateClick, onClick = onUpdateClick,
tooltip = stringResource(R.string.update),
) { ) {
BadgedBox( BadgedBox(
badge = { badge = {
@ -223,33 +230,45 @@ fun DashboardScreen(
} }
} }
} }
IconButton(onClick = onSettingsClick) { TooltipIconButton(
modifier = Modifier,
onClick = onSettingsClick,
tooltip = stringResource(R.string.settings),
) {
BadgedBox(
badge = {
Badge(modifier = Modifier.size(6.dp))
}
) {
Icon(Icons.Outlined.Settings, stringResource(R.string.settings)) Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
} }
}
}, },
applyContainerColor = true applyContainerColor = true
) )
} }
}, },
floatingActionButton = { floatingActionButton = {
HapticFloatingActionButton( TooltipFloatingActionButton(
modifier = Modifier,
tooltip = stringResource(R.string.add),
onClick = { onClick = {
vm.cancelSourceSelection() vm.cancelSourceSelection()
when (pagerState.currentPage) { when (pagerState.currentPage) {
DashboardPage.DASHBOARD.ordinal -> { DashboardPage.DASHBOARD.ordinal -> {
if (availablePatches < 1) { if (availablePatches < 1) {
androidContext.toast(androidContext.getString(R.string.patches_unavailable)) androidContext.toast(androidContext.getString(R.string.no_patch_found))
composableScope.launch { composableScope.launch {
pagerState.animateScrollToPage( pagerState.animateScrollToPage(
DashboardPage.BUNDLES.ordinal DashboardPage.BUNDLES.ordinal
) )
} }
return@HapticFloatingActionButton return@TooltipFloatingActionButton
} }
if (vm.android11BugActive) { if (vm.android11BugActive) {
showAndroid11Dialog = true showAndroid11Dialog = true
return@HapticFloatingActionButton return@TooltipFloatingActionButton
} }
onAppSelectorClick() onAppSelectorClick()

View File

@ -25,7 +25,6 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -51,6 +50,7 @@ import app.revanced.manager.ui.component.InstallerStatusDialog
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
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.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.viewmodel.PatcherViewModel import app.revanced.manager.ui.viewmodel.PatcherViewModel
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.APK_MIMETYPE
@ -164,15 +164,19 @@ fun PatcherScreen(
bottomBar = { bottomBar = {
BottomAppBar( BottomAppBar(
actions = { actions = {
IconButton( TooltipIconButton(
modifier = Modifier,
onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") }, onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") },
enabled = patcherSucceeded == true enabled = patcherSucceeded == true,
tooltip = stringResource(R.string.save_apk),
) { ) {
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
} }
IconButton( TooltipIconButton(
modifier = Modifier,
onClick = { viewModel.exportLogs(context) }, onClick = { viewModel.exportLogs(context) },
enabled = patcherSucceeded != null enabled = patcherSucceeded != null,
tooltip = stringResource(R.string.save_logs),
) { ) {
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs)) Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
} }

View File

@ -41,7 +41,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
@ -73,6 +72,9 @@ import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.component.tooltip.TooltipFloatingActionButton
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.component.tooltip.TooltipSmallFloatingActionButton
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
@ -259,14 +261,16 @@ fun PatchesSelectorScreen(
animationSpec = tween(durationMillis = 400, easing = EaseInOut), animationSpec = tween(durationMillis = 400, easing = EaseInOut),
label = "SearchBar back button" label = "SearchBar back button"
) )
IconButton( TooltipIconButton(
modifier = Modifier.rotate(rotation),
onClick = { onClick = {
if (searchExpanded) { if (searchExpanded) {
setSearchExpanded(false) setSearchExpanded(false)
} else { } else {
onBackClick() onBackClick()
} }
} },
tooltip = stringResource(R.string.back),
) { ) {
Icon( Icon(
modifier = Modifier.rotate(rotation), modifier = Modifier.rotate(rotation),
@ -282,9 +286,11 @@ fun PatchesSelectorScreen(
transitionSpec = { fadeIn() togetherWith fadeOut() } transitionSpec = { fadeIn() togetherWith fadeOut() }
) { searchExpanded -> ) { searchExpanded ->
if (searchExpanded) { if (searchExpanded) {
IconButton( TooltipIconButton(
modifier = Modifier,
onClick = { setQuery("") }, onClick = { setQuery("") },
enabled = query.isNotEmpty() enabled = query.isNotEmpty(),
tooltip = stringResource(R.string.clear),
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Close, imageVector = Icons.Filled.Close,
@ -292,7 +298,11 @@ fun PatchesSelectorScreen(
) )
} }
} else { } else {
IconButton(onClick = { showBottomSheet = true }) { TooltipIconButton(
modifier = Modifier,
onClick = { showBottomSheet = true },
tooltip = stringResource(R.string.more),
) {
Icon( Icon(
imageVector = Icons.Outlined.FilterList, imageVector = Icons.Outlined.FilterList,
contentDescription = stringResource(R.string.more) contentDescription = stringResource(R.string.more)
@ -354,7 +364,20 @@ fun PatchesSelectorScreen(
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
SmallFloatingActionButton( TooltipSmallFloatingActionButton(
modifier = Modifier,
tooltip = stringResource(R.string.more),
onClick = { showBottomSheet = true },
containerColor = MaterialTheme.colorScheme.tertiaryContainer
) {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = stringResource(R.string.more)
)
}
TooltipSmallFloatingActionButton(
modifier = Modifier,
tooltip = stringResource(R.string.reset),
onClick = viewModel::reset, onClick = viewModel::reset,
containerColor = MaterialTheme.colorScheme.tertiaryContainer containerColor = MaterialTheme.colorScheme.tertiaryContainer
) { ) {
@ -389,6 +412,7 @@ fun PatchesSelectorScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.padding(top = 16.dp)
) { ) {
if (bundles.size > 1) { if (bundles.size > 1) {
ScrollableTabRow( ScrollableTabRow(
@ -516,6 +540,7 @@ private fun PatchItem(
supportingContent = patch.description?.let { { Text(it) } }, supportingContent = patch.description?.let { { Text(it) } },
trailingContent = { trailingContent = {
if (patch.options?.isNotEmpty() == true) { if (patch.options?.isNotEmpty() == true) {
// TODO: Determine if this button should be [TooltipWrap]
IconButton(onClick = onOptionsDialog, enabled = compatible) { IconButton(onClick = onOptionsDialog, enabled = compatible) {
Icon(Icons.Outlined.Settings, null) Icon(Icons.Outlined.Settings, null)
} }
@ -539,7 +564,11 @@ fun ListHeader(
}, },
trailingContent = onHelpClick?.let { trailingContent = onHelpClick?.let {
{ {
IconButton(onClick = it) { TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.help),
onClick = it
) {
Icon( Icon(
Icons.AutoMirrored.Outlined.HelpOutline, Icons.AutoMirrored.Outlined.HelpOutline,
stringResource(R.string.help) stringResource(R.string.help)
@ -618,7 +647,11 @@ private fun OptionsDialog(
title = patch.name, title = patch.name,
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
actions = { actions = {
IconButton(onClick = reset) { TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.reset),
onClick = reset
) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
} }
} }

View File

@ -19,7 +19,6 @@ import androidx.compose.material.icons.outlined.MailOutline
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -50,6 +49,7 @@ import app.revanced.manager.network.dto.ReVancedSocial
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.model.navigation.Settings import app.revanced.manager.ui.model.navigation.Settings
import app.revanced.manager.ui.viewmodel.AboutViewModel import app.revanced.manager.ui.viewmodel.AboutViewModel
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.DEVELOPER_OPTIONS_TAPS import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.DEVELOPER_OPTIONS_TAPS
@ -252,9 +252,10 @@ fun AboutSettingsScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
) { ) {
socialButtons.forEach { (icon, text, onClick) -> socialButtons.forEach { (icon, text, onClick) ->
IconButton( TooltipIconButton(
onClick = onClick,
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
tooltip = text,
onClick = onClick
) { ) {
Icon( Icon(
icon, icon,

View File

@ -21,7 +21,6 @@ import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -52,6 +51,7 @@ 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.SafeguardBooleanItem import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.withHapticFeedback import app.revanced.manager.util.withHapticFeedback
@ -243,7 +243,11 @@ private fun APIUrlDialog(currentUrl: String, defaultUrl: String, onSubmit: (Stri
onValueChange = { url = it }, onValueChange = { url = it },
label = { Text(stringResource(R.string.api_url)) }, label = { Text(stringResource(R.string.api_url)) },
trailingIcon = { trailingIcon = {
IconButton(onClick = { url = defaultUrl }) { TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.api_url_dialog_reset),
onClick = { url = defaultUrl }
) {
Icon(Icons.Outlined.Restore, stringResource(R.string.api_url_dialog_reset)) Icon(Icons.Outlined.Restore, stringResource(R.string.api_url_dialog_reset))
} }
} }

View File

@ -48,13 +48,13 @@ fun DeveloperSettingsScreen(
description = R.string.developer_options_description, description = R.string.developer_options_description,
) )
GroupHeader(stringResource(R.string.patch_bundles_section)) GroupHeader(stringResource(R.string.patches))
SettingsListItem( SettingsListItem(
headlineContent = stringResource(R.string.patch_bundles_force_download), headlineContent = stringResource(R.string.patches_force_download),
modifier = Modifier.clickable(onClick = vm::redownloadBundles) modifier = Modifier.clickable(onClick = vm::redownloadBundles)
) )
SettingsListItem( SettingsListItem(
headlineContent = stringResource(R.string.patch_bundles_reset), headlineContent = stringResource(R.string.patches_reset),
modifier = Modifier.clickable(onClick = vm::redownloadBundles) modifier = Modifier.clickable(onClick = vm::redownloadBundles)
) )
} }

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -45,8 +46,10 @@ import app.revanced.manager.ui.component.ExceptionViewerDialog
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.ConfirmDialog import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.tooltip.TooltipWrap
import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.viewmodel.DownloadsViewModel import app.revanced.manager.ui.viewmodel.DownloadsViewModel
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import java.security.MessageDigest import java.security.MessageDigest
@ -81,7 +84,11 @@ fun DownloadsSettingsScreen(
onBackClick = onBackClick, onBackClick = onBackClick,
actions = { actions = {
if (viewModel.appSelection.isNotEmpty()) { if (viewModel.appSelection.isNotEmpty()) {
IconButton(onClick = { showDeleteConfirmationDialog = true }) { TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.delete),
onClick = { showDeleteConfirmationDialog = true }
) {
Icon(Icons.Default.Delete, stringResource(R.string.delete)) Icon(Icons.Default.Delete, stringResource(R.string.delete))
} }
} }

View File

@ -240,8 +240,8 @@ fun ImportExportSettingsScreen(
} }
} }
}, },
headline = R.string.patch_selection_reset_bundle, headline = R.string.patch_selection_reset_patches,
description = R.string.patch_selection_reset_bundle_description description = R.string.patch_selection_reset_patches_description
) )
} }
} }
@ -296,8 +296,8 @@ fun ImportExportSettingsScreen(
} }
} }
}, },
headline = R.string.patch_options_reset_bundle, headline = R.string.patch_options_reset,
description = R.string.patch_options_reset_bundle_description, description = R.string.patch_options_reset_all,
) )
} }
} }

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
@ -50,6 +51,8 @@ fun UpdatesSettingsScreen(
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
GroupHeader(stringResource(R.string.manager))
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { modifier = Modifier.clickable {
coroutineScope.launch { coroutineScope.launch {

View File

@ -135,13 +135,13 @@ class DashboardViewModel(
uiSafe( uiSafe(
app, app,
R.string.source_download_fail, R.string.patches_download_fail,
RemotePatchBundle.updateFailMsg RemotePatchBundle.updateFailMsg
) { ) {
if (bundle.update()) if (bundle.update())
app.toast(app.getString(R.string.bundle_update_success, bundle.getName())) app.toast(app.getString(R.string.patches_update_success, bundle.getName()))
else else
app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName())) app.toast(app.getString(R.string.patches_update_unavailable, bundle.getName()))
} }
} }
} }

View File

@ -16,7 +16,7 @@ class DeveloperOptionsViewModel(
private val patchBundleRepository: PatchBundleRepository private val patchBundleRepository: PatchBundleRepository
) : ViewModel() { ) : ViewModel() {
fun redownloadBundles() = viewModelScope.launch { fun redownloadBundles() = viewModelScope.launch {
uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) { uiSafe(app, R.string.patches_download_fail, RemotePatchBundle.updateFailMsg) {
patchBundleRepository.redownloadRemoteBundles() patchBundleRepository.redownloadRemoteBundles()
} }
} }

View File

@ -61,8 +61,8 @@ sealed class ResetDialogState(
) )
class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState( class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_bundle, titleResId = R.string.patch_selection_reset_patches,
descriptionResId = R.string.patch_selection_reset_bundle_dialog_description, descriptionResId = R.string.patch_selection_reset_patches_dialog_description,
onConfirm = onConfirm, onConfirm = onConfirm,
dialogOptionName = dialogOptionName dialogOptionName = dialogOptionName
) )
@ -81,8 +81,8 @@ sealed class ResetDialogState(
) )
class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState( class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_bundle, titleResId = R.string.patch_options_reset,
descriptionResId = R.string.patch_options_reset_bundle_dialog_description, descriptionResId = R.string.patch_options_reset_dialog_description,
onConfirm = onConfirm, onConfirm = onConfirm,
dialogOptionName = dialogOptionName dialogOptionName = dialogOptionName
) )

View File

@ -14,26 +14,24 @@
<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>
<string name="patches_selected">%1$d/%2$d selected</string> <string name="patches_count_selected">%1$d/%2$d selected</string>
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string> <string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string>
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string> <string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
<string name="import_">Import</string> <string name="import_">Import</string>
<string name="import_bundle">Import patch bundle</string> <string name="import_patches">Import patches</string>
<string name="bundle_patches">Bundle patches</string>
<string name="patch_bundle_field">Patch bundle</string>
<string name="file_field_set">Selected</string> <string name="file_field_set">Selected</string>
<string name="file_field_not_set">Not selected</string> <string name="file_field_not_set">Not selected</string>
<string name="field_not_set">Not set</string> <string name="field_not_set">Not set</string>
<string name="bundle_missing">Missing</string> <string name="patches_missing">Missing</string>
<string name="bundle_error">Error</string> <string name="patches_error">Error</string>
<string name="bundle_error_description">Bundle could not be loaded. Click to view the error</string> <string name="patches_error_description">Patches could not be loaded. Click to view the error</string>
<string name="bundle_not_downloaded">Bundle has not been downloaded. Click here to download it</string> <string name="patches_not_downloaded">Patches has not been downloaded. Click here to download it</string>
<string name="bundle_name_default">Default</string> <string name="patches_name_default">Patches</string>
<string name="bundle_name_fallback">Unnamed</string> <string name="patches_name_fallback">Unnamed</string>
<string name="android_11_bug_dialog_title">Android 11 bug</string> <string name="android_11_bug_dialog_title">Android 11 bug</string>
<string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string> <string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string>
@ -139,16 +137,15 @@
<string name="patch_selection_reset_package">Reset patch selection for app</string> <string name="patch_selection_reset_package">Reset patch selection for app</string>
<string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string> <string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string>
<string name="patch_selection_reset_package_description">Resets patch selection for a single app</string> <string name="patch_selection_reset_package_description">Resets patch selection for a single app</string>
<string name="patch_selection_reset_bundle">Resets patch selection for bundle</string> <string name="patch_selection_reset_patches">Resets patch selection for a specific patches</string>
<string name="patch_selection_reset_bundle_dialog_description">You are about to reset the patch selection for the bundle \"%s\". You will have to manually select each patch again.</string> <string name="patch_selection_reset_patches_dialog_description">You are about to reset the patch selection for \"%s\". You will have to manually select each patch again.</string>
<string name="patch_selection_reset_bundle_description">Resets the patch selection for all patches in a bundle</string> <string name="patch_selection_reset_patches_description">Resets the patch selection for a specific patches</string>
<string name="patch_options_reset_package">Reset patch options for app</string> <string name="patch_options_reset_package">Reset patch options for app</string>
<string name="patch_options_reset_package_dialog_description">You are about to reset the patch options for the app \"%s\". You will have to reapply each option again.</string> <string name="patch_options_reset_package_dialog_description">You are about to reset the patch options for the app \"%s\". You will have to reapply each option again.</string>
<string name="patch_options_reset_package_description">Resets patch options for a single app</string> <string name="patch_options_reset_package_description">Resets patch options for a single app</string>
<string name="patch_options_reset_bundle">Resets patch options for bundle</string> <string name="patch_options_reset">Reset patch options</string>
<string name="patch_options_reset_bundle_dialog_description">You are about to reset the patch options for the bundle \"%s\". You will have to reapply each option again.</string> <string name="patch_options_reset_dialog_description">You are about to reset the patch options for \"%s\". You will have to reapply each option again.</string>
<string name="patch_options_reset_bundle_description">Resets patch options for all patches in a bundle</string> <string name="patch_options_reset_all">Reset patch options for all</string>
<string name="patch_options_reset_all">Reset patch options</string>
<string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string> <string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string>
<string name="patch_options_reset_all_description">Resets all patch options</string> <string name="patch_options_reset_all_description">Resets all patch options</string>
<string name="downloader_plugins">Plugins</string> <string name="downloader_plugins">Plugins</string>
@ -164,7 +161,7 @@
<string name="search_apps">Search apps…</string> <string name="search_apps">Search apps…</string>
<string name="loading_body">Loading…</string> <string name="loading_body">Loading…</string>
<string name="downloading_patches">Downloading patch bundle…</string> <string name="downloading_patches">Downloading patches</string>
<string name="options">Options</string> <string name="options">Options</string>
<string name="ok">OK</string> <string name="ok">OK</string>
@ -202,8 +199,8 @@
<string name="debug_logs_export_success">Exported logs</string> <string name="debug_logs_export_success">Exported logs</string>
<string name="api_url">API URL</string> <string name="api_url">API URL</string>
<string name="api_url_description">The API used to download necessary files.</string> <string name="api_url_description">The API used to download necessary files.</string>
<string name="api_url_dialog_title">Set custom API URL</string> <string name="api_url_dialog_title">Change API URL</string>
<string name="api_url_dialog_description">Set the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string> <string name="api_url_dialog_description">Change the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string>
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string> <string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string>
<string name="api_url_dialog_save">Set</string> <string name="api_url_dialog_save">Set</string>
<string name="api_url_dialog_reset">Reset API URL</string> <string name="api_url_dialog_reset">Reset API URL</string>
@ -213,26 +210,25 @@
<string name="device_architectures">CPU Architectures</string> <string name="device_architectures">CPU Architectures</string>
<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="patches_force_download">Force download all patches</string>
<string name="patch_bundles_force_download">Force download all patch bundles</string> <string name="patches_reset">Reset patches</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>
<string name="storage">Storage</string> <string name="storage">Storage</string>
<string name="patches_unavailable">No patches are available. Check your bundles</string> <string name="no_patch_found">No patch can be found. Check your patches</string>
<string name="tab_apps">Apps</string> <string name="tab_apps">Apps</string>
<string name="tab_bundles">Patch bundles</string> <string name="tab_patches">Patches</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="refresh">Refresh</string> <string name="refresh">Refresh</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>
<string name="download_apk">Download APK file</string> <string name="download_apk">Download APK file</string>
<string name="source_download_fail">Failed to download patch bundle: %s</string> <string name="patches_download_fail">Failed to download patches: %s</string>
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string> <string name="patches_replace_fail">Failed to load updated patches: %s</string>
<string name="no_patched_apps_found">No patched apps found</string> <string name="no_patched_apps_found">No patched apps found</string>
<string name="tap_on_patches">Tap on the patches to get more information about them</string> <string name="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="bundles_selected">%s selected</string> <string name="patches_selected">%s selected</string>
<string name="incompatible_patches">Incompatible patches</string> <string name="incompatible_patches">Incompatible patches</string>
<string name="universal_patches">Universal patches</string> <string name="universal_patches">Universal patches</string>
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string> <string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
@ -317,7 +313,8 @@
<string name="patcher_step_group_saving">Saving</string> <string name="patcher_step_group_saving">Saving</string>
<string name="patcher_step_write_patched">Write patched APK file</string> <string name="patcher_step_write_patched">Write patched APK file</string>
<string name="patcher_step_sign_apk">Sign patched APK file</string> <string name="patcher_step_sign_apk">Sign patched APK file</string>
<string name="patcher_notification_message">Patching in progress…</string> <string name="patcher_notification_title">Patching in progress…</string>
<string name="patcher_notification_text">Tap to return to the patcher</string>
<string name="patcher_stop_confirm_title">Stop patcher</string> <string name="patcher_stop_confirm_title">Stop patcher</string>
<string name="patcher_stop_confirm_description">Are you sure you want to stop the patching process?</string> <string name="patcher_stop_confirm_description">Are you sure you want to stop the patching process?</string>
<string name="execute_patches">Execute patches</string> <string name="execute_patches">Execute patches</string>
@ -347,19 +344,13 @@
<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_source_url">Source URL</string> <string name="patches_update_success">Successfully updated %s</string>
<string name="bundle_update_success">Successfully updated %s</string> <string name="patches_update_unavailable">No update available for %s</string>
<string name="bundle_update_unavailable">No update available for %s</string> <string name="view_patches">View patches</string>
<string name="bundle_auto_update">Auto update</string> <string name="patches_view_any_version">Any version</string>
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string> <string name="patches_view_any_package">Any package</string>
<string name="bundle_view_patches">View patches</string> <string name="patches_delete_single_dialog_description">Are you sure you want to delete \"%s\"?</string>
<string name="bundle_view_patches_any_version">Any version</string> <string name="patches_delete_multiple_dialog_description">Are you sure you want to delete the selected patches?</string>
<string name="bundle_view_patches_any_package">Any package</string>
<string name="bundle_delete_single_dialog_title">Delete bundle</string>
<string name="bundle_delete_multiple_dialog_title">Delete bundles</string>
<string name="bundle_delete_single_dialog_description">Are you sure you want to delete the bundle \"%s\"?</string>
<string name="bundle_delete_multiple_dialog_description">Are you sure you want to delete the selected bundles?</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 Android application that uses ReVanced Patcher to patch Android apps. It allows you to download and patch apps with custom patches, and manage the patching process.</string> <string name="revanced_manager_description">ReVanced Manager is an Android application that uses ReVanced Patcher to patch Android apps. It allows you to download and patch apps with custom patches, and manage the patching process.</string>
@ -413,10 +404,9 @@
<string name="no_contributors_found">No contributors found</string> <string name="no_contributors_found">No contributors found</string>
<string name="select">Select</string> <string name="select">Select</string>
<string name="select_deselect_all">Select or deselect all</string> <string name="select_deselect_all">Select or deselect all</string>
<string name="select_bundle_type_dialog_title">Add new bundle</string> <string name="select_patches_type_dialog_description">Add new patches from URL or local files</string>
<string name="select_bundle_type_dialog_description">Add a new bundle from a URL or storage</string> <string name="local_patches_description">Add patches from local storage.</string>
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string> <string name="remote_patches_description">Add patches from URL. Patches 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_failed_dialog_title">Installation failed</string>
@ -441,9 +431,10 @@
<string name="about_device">About device</string> <string name="about_device">About device</string>
<string name="enter_url">Enter URL</string> <string name="enter_url">Enter URL</string>
<string name="next">Next</string> <string name="next">Next</string>
<string name="add_patch_bundle">Add patch bundle</string>
<string name="bundle_url">Bundle URL</string>
<string name="auto_update">Auto update</string> <string name="auto_update">Auto update</string>
<string name="add_patches">Add patches</string>
<string name="auto_update_description">Automatically update when a new version is available</string>
<string name="patches_url">Patches URL</string>
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string> <string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
<string name="incompatible_patch">Incompatible patch</string> <string name="incompatible_patch">Incompatible patch</string>
<string name="any_version">Any</string> <string name="any_version">Any</string>