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:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ class LocalPatchBundle(name: String, id: Int, directory: File) :
}
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)
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()
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.
*/
@ -71,7 +74,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
val bundle = newState.patchBundleOrNull()
// Try to read the name from the patch bundle manifest if the bundle does not have a name.
if (bundle != null && _nameFlow.value.isEmpty()) {
bundle.readManifestAttribute("Name")?.let { setName(it) }
bundle.patchBundleManifestAttributes?.name?.let { setName(it) }
}
return bundle
@ -84,9 +87,9 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
suspend fun getProps() = propsFlow().first()!!
suspend fun currentVersion() = getProps().version
protected suspend fun saveVersion(version: String?) =
configRepository.updateVersion(uid, version)
suspend fun currentVersionHash() = getProps().versionHash
protected suspend fun saveVersionHash(version: String?) =
configRepository.updateVersionHash(uid, version)
suspend fun setName(name: String) {
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()
}
@ -35,7 +35,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
val info = getLatestInfo()
if (hasInstalled() && info.version == currentVersion())
if (hasInstalled() && info.version == currentVersionHash())
return@withContext false
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)
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,
expectedPackageName: String,
expectedVersion: String?,
appCompatibilityCheck: Boolean,
patchesCompatibilityCheck: Boolean,
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
): File {
// 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 =
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 (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).
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {

View File

@ -25,7 +25,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
PatchBundleEntity(
uid = generateUid(),
name = name,
version = null,
versionHash = null,
source = source,
autoUpdate = autoUpdate
).also {
@ -34,8 +34,11 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
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)
@ -47,7 +50,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
val defaultSource = PatchBundleEntity(
uid = 0,
name = "",
version = null,
versionHash = null,
source = Source.API,
autoUpdate = false
)

View File

@ -165,7 +165,7 @@ class PatchBundleRepository(
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
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 {
if (!networkInfo.isSafe()) {
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.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) {
private val loader = object : Iterable<Patch<*>> {
private fun load(): Iterable<Patch<*>> {
@ -36,7 +47,20 @@ class PatchBundle(val patchesJar: File) {
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.

View File

@ -14,9 +14,9 @@ import android.os.Parcelable
import android.os.PowerManager
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.core.content.ContextCompat
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import app.revanced.manager.MainActivity
import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.room.apps.installed.InstallType
@ -88,22 +88,25 @@ class PatcherWorker(
)
private fun createNotification(): Notification {
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
val pendingIntent: PendingIntent = PendingIntent.getActivity(
val notificationIntent = Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
)
val channel = NotificationChannel(
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW
)
val notificationManager =
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
notificationManager!!.createNotificationChannel(channel)
applicationContext.getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
return Notification.Builder(applicationContext, channel.id)
.setContentTitle(applicationContext.getText(R.string.app_name))
.setContentText(applicationContext.getText(R.string.patcher_notification_message))
.setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
.setContentTitle(applicationContext.getText(R.string.patcher_notification_title))
.setContentText(applicationContext.getText(R.string.patcher_notification_text))
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
.setContentIntent(pendingIntent).build()
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
}
override suspend fun doWork(): Result {
@ -158,6 +161,8 @@ class PatcherWorker(
data,
args.packageName,
args.input.version,
prefs.suggestedVersionSafeguard.get(),
!prefs.disablePatchVersionCompatCheck.get(),
onDownload = args.onDownloadProgress
).also {
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -69,7 +69,11 @@ fun AppTopBar(
scrollBehavior = scrollBehavior,
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
TooltipIconButton(
modifier = Modifier,
onClick = onBackClick,
tooltip = stringResource(R.string.back),
) {
backIcon()
}
}
@ -108,7 +112,11 @@ fun AppTopBar(
scrollBehavior = scrollBehavior,
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
TooltipIconButton(
modifier = Modifier,
onClick = onBackClick,
tooltip = stringResource(R.string.back),
) {
backIcon()
}
}

View File

@ -4,13 +4,13 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@Composable
fun ArrowButton(
@ -27,7 +27,11 @@ fun ArrowButton(
)
onClick?.let {
IconButton(onClick = it) {
TooltipIconButton(
modifier = Modifier,
onClick = it,
tooltip = stringResource(description),
) {
Icon(
imageVector = Icons.Filled.KeyboardArrowUp,
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -18,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -30,7 +30,7 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
Scaffold(
topBar = {
BundleTopBar(
title = stringResource(R.string.bundle_error),
title = stringResource(R.string.patches_error),
onBackClick = onDismiss,
backIcon = {
Icon(
@ -39,7 +39,8 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
)
},
actions = {
IconButton(
TooltipIconButton(
modifier = Modifier,
onClick = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
@ -52,7 +53,8 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
},
tooltip = stringResource(R.string.share),
) {
Icon(
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.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@Composable
fun NotificationCard(
@ -138,7 +138,11 @@ fun NotificationCard(
)
}
if (onDismiss != null) {
IconButton(onClick = onDismiss) {
TooltipIconButton(
modifier = modifier,
onClick = onDismiss,
tooltip = stringResource(R.string.close),
) {
Icon(
imageVector = Icons.Outlined.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.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
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.VisualTransformation
import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@Composable
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,
modifier = modifier,
trailingIcon = {
IconButton(onClick = {
TooltipIconButton(
modifier = Modifier,
onClick = {
visible = !visible
}) {
},
tooltip = if (visible) stringResource(R.string.show_password_field) else stringResource(
R.string.hide_password_field
),
) {
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
}

View File

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

View File

@ -2,13 +2,26 @@ package app.revanced.manager.ui.component.bundle
import android.webkit.URLUtil
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.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.Extension
import androidx.compose.material.icons.outlined.Inventory2
import androidx.compose.material.icons.automirrored.outlined.Send
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.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.getValue
import androidx.compose.runtime.mutableStateOf
@ -17,10 +30,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
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.TextInputDialog
import app.revanced.manager.ui.component.haptics.HapticSwitch
@ -29,12 +43,12 @@ import app.revanced.manager.ui.component.haptics.HapticSwitch
fun BaseBundleDialog(
modifier: Modifier = Modifier,
isDefault: Boolean,
name: String?,
remoteUrl: String?,
onRemoteUrlChange: ((String) -> Unit)? = null,
patchCount: Int,
version: String?,
autoUpdate: Boolean,
bundleManifestAttributes: PatchBundleManifestAttributes?,
onAutoUpdateChange: (Boolean) -> Unit,
onPatchesClick: () -> Unit,
extraFields: @Composable ColumnScope.() -> Unit = {}
@ -47,36 +61,27 @@ fun BaseBundleDialog(
Column(
modifier = Modifier.padding(16.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 {
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) {
BundleListItem(
headlineText = stringResource(R.string.bundle_auto_update),
supportingText = stringResource(R.string.bundle_auto_update_description),
headlineText = stringResource(R.string.auto_update),
supportingText = stringResource(R.string.auto_update_description),
trailingContent = {
HapticSwitch(
checked = autoUpdate,
@ -108,7 +113,7 @@ fun BaseBundleDialog(
if (showUrlInputDialog) {
TextInputDialog(
initial = url,
title = stringResource(R.string.bundle_input_source_url),
title = stringResource(R.string.patches_url),
onDismissRequest = { showUrlInputDialog = false },
onConfirm = {
showUrlInputDialog = false
@ -129,7 +134,7 @@ fun BaseBundleDialog(
showUrlInputDialog = true
}
),
headlineText = stringResource(R.string.bundle_input_source_url),
headlineText = stringResource(R.string.patches_url),
supportingText = url.ifEmpty {
stringResource(R.string.field_not_set)
}
@ -139,7 +144,7 @@ fun BaseBundleDialog(
val patchesClickable = patchCount > 0
BundleListItem(
headlineText = stringResource(R.string.patches),
supportingText = stringResource(R.string.bundle_view_patches),
supportingText = stringResource(R.string.view_patches),
modifier = Modifier.clickable(
enabled = patchesClickable,
onClick = onPatchesClick
@ -160,22 +165,34 @@ fun BaseBundleDialog(
@Composable
private fun Tag(
icon: ImageVector,
text: String
text: String,
isUrl: Boolean = false
) {
val uriHandler = LocalUriHandler.current
Row(
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(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier.size(16.dp)
)
Text(
text,
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.ui.component.ExceptionViewerDialog
import app.revanced.manager.ui.component.FullscreenDialog
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@ -42,16 +43,16 @@ fun BundleInformationDialog(
val props by remember(bundle) {
bundle.propsFlow()
}.collectAsStateWithLifecycle(null)
val patchCount = remember(state) {
state.patchBundleOrNull()?.patches?.size ?: 0
}
val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0)
val version by bundle.versionFlow.collectAsStateWithLifecycle(null)
val bundleManifestAttributes = state.patchBundleOrNull()?.patchBundleManifestAttributes
if (viewCurrentBundlePatches) {
BundlePatchesDialog(
onDismissRequest = {
viewCurrentBundlePatches = false
},
bundle = bundle,
bundle = bundle
)
}
@ -63,7 +64,7 @@ fun BundleInformationDialog(
Scaffold(
topBar = {
BundleTopBar(
title = stringResource(R.string.patch_bundle_field),
title = bundleName,
onBackClick = onDismissRequest,
backIcon = {
Icon(
@ -73,7 +74,11 @@ fun BundleInformationDialog(
},
actions = {
if (!bundle.isDefault) {
IconButton(onClick = onDeleteRequest) {
TooltipIconButton(
modifier = Modifier,
onClick = onDeleteRequest,
tooltip = stringResource(R.string.delete),
) {
Icon(
Icons.Outlined.DeleteOutline,
stringResource(R.string.delete)
@ -81,7 +86,11 @@ fun BundleInformationDialog(
}
}
if (!isLocal && hasNetwork) {
IconButton(onClick = onUpdate) {
TooltipIconButton(
modifier = Modifier,
onClick = onUpdate,
tooltip = stringResource(R.string.refresh),
) {
Icon(
Icons.Outlined.Update,
stringResource(R.string.refresh)
@ -95,11 +104,11 @@ fun BundleInformationDialog(
BaseBundleDialog(
modifier = Modifier.padding(paddingValues),
isDefault = bundle.isDefault,
name = bundleName,
remoteUrl = bundle.asRemoteOrNull?.endpoint,
patchCount = patchCount,
version = props?.version,
autoUpdate = props?.autoUpdate ?: false,
version = version,
autoUpdate = props?.autoUpdate == true,
bundleManifestAttributes = bundleManifestAttributes,
onAutoUpdateChange = {
composableScope.launch {
bundle.asRemoteOrNull?.setAutoUpdate(it)
@ -119,8 +128,8 @@ fun BundleInformationDialog(
)
BundleListItem(
headlineText = stringResource(R.string.bundle_error),
supportingText = stringResource(R.string.bundle_error_description),
headlineText = stringResource(R.string.patches_error),
supportingText = stringResource(R.string.patches_error_description),
trailingContent = {
Icon(
Icons.AutoMirrored.Outlined.ArrowRight,
@ -133,8 +142,8 @@ fun BundleInformationDialog(
if (state is PatchBundleSource.State.Missing && !isLocal) {
BundleListItem(
headlineText = stringResource(R.string.bundle_error),
supportingText = stringResource(R.string.bundle_not_downloaded),
headlineText = stringResource(R.string.patches_error),
supportingText = stringResource(R.string.patches_not_downloaded),
modifier = Modifier.clickable(onClick = onUpdate)
)
}

View File

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

View File

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

View File

@ -13,14 +13,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -50,16 +50,14 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
.fillMaxWidth()
) {
Text(
text = "Select bundle",
text = stringResource(R.string.select),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
)
}
bundles.forEach {
val name by it.nameState
val version by remember(it) {
it.propsFlow().map { props -> props?.version }
}.collectAsStateWithLifecycle(null)
val version by it.versionFlow.collectAsStateWithLifecycle(null)
Row(
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@ -10,7 +9,11 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -33,7 +36,11 @@ fun BundleTopBar(
scrollBehavior = scrollBehavior,
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.back),
onClick = onBackClick
) {
backIcon()
}
}

View File

@ -77,7 +77,7 @@ fun ImportPatchBundleDialog(
AlertDialogExtended(
onDismissRequest = onDismiss,
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 = {
steps[currentStep]()
@ -126,7 +126,7 @@ fun SelectBundleTypeStep(
) {
Text(
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 {
ListItem(
@ -136,7 +136,7 @@ fun SelectBundleTypeStep(
),
headlineContent = { Text(stringResource(R.string.enter_url)) },
overlineContent = { Text(stringResource(R.string.recommended)) },
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
supportingContent = { Text(stringResource(R.string.remote_patches_description)) },
leadingContent = {
HapticRadioButton(
selected = bundleType == BundleType.Remote,
@ -152,7 +152,7 @@ fun SelectBundleTypeStep(
onClick = { onBundleTypeSelected(BundleType.Local) }
),
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 = { },
leadingContent = {
HapticRadioButton(
@ -185,10 +185,11 @@ fun ImportBundleStep(
) {
ListItem(
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)) },
trailingContent = {
// TODO: Determine if this button should be [TooltipWrap]'ped
IconButton(onClick = launchPatchActivity) {
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
}
@ -206,11 +207,11 @@ fun ImportBundleStep(
OutlinedTextField(
value = remoteUrl,
onValueChange = onRemoteUrlChange,
label = { Text(stringResource(R.string.bundle_url)) }
label = { Text(stringResource(R.string.patches_url)) }
)
}
Column(
modifier = Modifier.padding(horizontal = 8.dp)
modifier = Modifier.padding(horizontal = 8.dp, vertical = 5.dp)
) {
ListItem(
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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
@Composable
fun HapticSwitch(
@ -20,16 +21,19 @@ fun HapticSwitch(
colors: SwitchColors = SwitchDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val view = LocalView.current
Switch(
checked = checked,
onCheckedChange = { newChecked ->
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
when {
val hapticFeedbackType = when {
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
!newChecked -> HapticFeedbackConstants.CLOCK_TICK
else -> {HapticFeedbackConstants.VIRTUAL_KEY}
}
view.performHapticFeedback(hapticFeedbackType)
onCheckedChange(newChecked)
},
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.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
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.HapticRadioButton
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.mutableStateSetOf
import app.revanced.manager.util.saver.snapshotStateListSaver
import app.revanced.manager.util.saver.snapshotStateSetSaver
import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.parcelize.Parcelize
import org.koin.compose.koinInject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyColumnState
import sh.calvin.reorderable.rememberReorderableLazyListState
import java.io.Serializable
import kotlin.random.Random
@ -113,7 +111,11 @@ private interface OptionEditor<T : Any> {
@Composable
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))
}
}
@ -249,13 +251,12 @@ private object StringOptionEditor : OptionEditor<String> {
},
trailingIcon = {
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
IconButton(
TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.string_option_menu_description),
onClick = { showDropdownMenu = true }
) {
Icon(
Icons.Outlined.MoreVert,
stringResource(R.string.string_option_menu_description)
)
Icon(Icons.Outlined.MoreVert, stringResource(R.string.string_option_menu_description))
}
DropdownMenu(
@ -551,7 +552,9 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
},
actions = {
if (deleteMode) {
IconButton(
TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.select_deselect_all),
onClick = {
if (items.size == deletionTargets.size) deletionTargets.clear()
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)
)
}
IconButton(
TooltipIconButton(
modifier = Modifier,
tooltip = stringResource(R.string.delete),
onClick = {
items.removeIf { it.key in deletionTargets }
deletionTargets.clear()
@ -575,8 +580,15 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
)
}
} else {
IconButton(onClick = items::clear) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
TooltipIconButton(
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,
leadingContent = {
IconButton(
TooltipIconButton(
modifier = Modifier.draggableHandle(interactionSource = interactionSource),
onClick = {},
tooltip = stringResource(R.string.delete),
onClick = { }
) {
Icon(
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.outlined.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -17,6 +16,7 @@ import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.domain.manager.base.Preference
import app.revanced.manager.ui.component.IntInputDialog
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -65,10 +65,14 @@ fun IntegerItem(
headlineContent = stringResource(headline),
supportingContent = stringResource(description),
trailingContent = {
IconButton(onClick = { dialogOpen = true }) {
TooltipIconButton(
modifier = modifier,
onClick = { dialogOpen = true },
tooltip = stringResource(R.string.edit),
) {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.edit)
imageVector = Icons.Outlined.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)
}
val patchCount get() = compatible.size + incompatible.size + universal.size
fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) {
all
} else {
@ -79,7 +77,7 @@ data class BundleInfo(
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.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
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.NonSuggestedVersionDialog
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.viewmodel.AppSelectorViewModel
import app.revanced.manager.util.APK_MIMETYPE
@ -162,8 +162,15 @@ fun AppSelectorScreen(
scrollBehavior = scrollBehavior,
onBackClick = onBackClick,
actions = {
IconButton(onClick = { search = true }) {
Icon(Icons.Outlined.Search, stringResource(R.string.search))
TooltipIconButton(
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.AutoUpdatesDialog
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.NotificationCard
import app.revanced.manager.ui.component.bundle.BundleTopBar
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.tooltip.TooltipFloatingActionButton
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.util.RequestInstallAppsContract
import app.revanced.manager.util.toast
@ -79,7 +80,7 @@ enum class DashboardPage(
val icon: ImageVector
) {
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")
@ -93,7 +94,7 @@ fun DashboardScreen(
onDownloaderPluginClick: () -> 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 showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
false
@ -164,8 +165,8 @@ fun DashboardScreen(
vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) }
vm.cancelSourceSelection()
},
title = stringResource(R.string.bundle_delete_multiple_dialog_title),
description = stringResource(R.string.bundle_delete_multiple_dialog_description),
title = stringResource(R.string.delete),
description = stringResource(R.string.patches_delete_multiple_dialog_description),
icon = Icons.Outlined.Delete
)
}
@ -174,7 +175,7 @@ fun DashboardScreen(
topBar = {
if (bundlesSelectable) {
BundleTopBar(
title = stringResource(R.string.bundles_selected, vm.selectedSources.size),
title = stringResource(R.string.patches_selected, vm.selectedSources.size),
onBackClick = vm::cancelSourceSelection,
backIcon = {
Icon(
@ -183,21 +184,25 @@ fun DashboardScreen(
)
},
actions = {
IconButton(
TooltipIconButton(
modifier = Modifier,
onClick = {
showDeleteConfirmationDialog = true
}
},
tooltip = stringResource(R.string.delete),
) {
Icon(
Icons.Outlined.DeleteOutline,
stringResource(R.string.delete)
)
}
IconButton(
TooltipIconButton(
modifier = Modifier,
onClick = {
vm.selectedSources.forEach { vm.update(it) }
vm.cancelSourceSelection()
}
},
tooltip = stringResource(R.string.refresh),
) {
Icon(
Icons.Outlined.Refresh,
@ -211,8 +216,10 @@ fun DashboardScreen(
title = stringResource(R.string.app_name),
actions = {
if (!vm.updatedManagerVersion.isNullOrEmpty()) {
IconButton(
TooltipIconButton(
modifier = Modifier,
onClick = onUpdateClick,
tooltip = stringResource(R.string.update),
) {
BadgedBox(
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))
}
}
},
applyContainerColor = true
)
}
},
floatingActionButton = {
HapticFloatingActionButton(
TooltipFloatingActionButton(
modifier = Modifier,
tooltip = stringResource(R.string.add),
onClick = {
vm.cancelSourceSelection()
when (pagerState.currentPage) {
DashboardPage.DASHBOARD.ordinal -> {
if (availablePatches < 1) {
androidContext.toast(androidContext.getString(R.string.patches_unavailable))
androidContext.toast(androidContext.getString(R.string.no_patch_found))
composableScope.launch {
pagerState.animateScrollToPage(
DashboardPage.BUNDLES.ordinal
)
}
return@HapticFloatingActionButton
return@TooltipFloatingActionButton
}
if (vm.android11BugActive) {
showAndroid11Dialog = true
return@HapticFloatingActionButton
return@TooltipFloatingActionButton
}
onAppSelectorClick()

View File

@ -25,7 +25,6 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
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.patcher.InstallPickerDialog
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.viewmodel.PatcherViewModel
import app.revanced.manager.util.APK_MIMETYPE
@ -164,15 +164,19 @@ fun PatcherScreen(
bottomBar = {
BottomAppBar(
actions = {
IconButton(
TooltipIconButton(
modifier = Modifier,
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))
}
IconButton(
TooltipIconButton(
modifier = Modifier,
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))
}

View File

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

View File

@ -21,7 +21,6 @@ import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
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.SafeguardBooleanItem
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.util.toast
import app.revanced.manager.util.withHapticFeedback
@ -243,7 +243,11 @@ private fun APIUrlDialog(currentUrl: String, defaultUrl: String, onSubmit: (Stri
onValueChange = { url = it },
label = { Text(stringResource(R.string.api_url)) },
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))
}
}

View File

@ -48,13 +48,13 @@ fun DeveloperSettingsScreen(
description = R.string.developer_options_description,
)
GroupHeader(stringResource(R.string.patch_bundles_section))
GroupHeader(stringResource(R.string.patches))
SettingsListItem(
headlineContent = stringResource(R.string.patch_bundles_force_download),
headlineContent = stringResource(R.string.patches_force_download),
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
)
SettingsListItem(
headlineContent = stringResource(R.string.patch_bundles_reset),
headlineContent = stringResource(R.string.patches_reset),
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.filled.Delete
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
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.LazyColumnWithScrollbar
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.settings.SettingsListItem
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
import org.koin.androidx.compose.koinViewModel
import java.security.MessageDigest
@ -81,7 +84,11 @@ fun DownloadsSettingsScreen(
onBackClick = onBackClick,
actions = {
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))
}
}

View File

@ -240,8 +240,8 @@ fun ImportExportSettingsScreen(
}
}
},
headline = R.string.patch_selection_reset_bundle,
description = R.string.patch_selection_reset_bundle_description
headline = R.string.patch_selection_reset_patches,
description = R.string.patch_selection_reset_patches_description
)
}
}
@ -296,8 +296,8 @@ fun ImportExportSettingsScreen(
}
}
},
headline = R.string.patch_options_reset_bundle,
description = R.string.patch_options_reset_bundle_description,
headline = R.string.patch_options_reset,
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.ui.component.AppTopBar
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.SettingsListItem
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
@ -50,6 +51,8 @@ fun UpdatesSettingsScreen(
.fillMaxSize()
.padding(paddingValues)
) {
GroupHeader(stringResource(R.string.manager))
SettingsListItem(
modifier = Modifier.clickable {
coroutineScope.launch {

View File

@ -135,13 +135,13 @@ class DashboardViewModel(
uiSafe(
app,
R.string.source_download_fail,
R.string.patches_download_fail,
RemotePatchBundle.updateFailMsg
) {
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
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
) : ViewModel() {
fun redownloadBundles() = viewModelScope.launch {
uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) {
uiSafe(app, R.string.patches_download_fail, RemotePatchBundle.updateFailMsg) {
patchBundleRepository.redownloadRemoteBundles()
}
}

View File

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

View File

@ -14,26 +14,24 @@
<string name="dashboard">Dashboard</string>
<string name="settings">Settings</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="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
<string name="import_">Import</string>
<string name="import_bundle">Import patch bundle</string>
<string name="bundle_patches">Bundle patches</string>
<string name="patch_bundle_field">Patch bundle</string>
<string name="import_patches">Import patches</string>
<string name="file_field_set">Selected</string>
<string name="file_field_not_set">Not selected</string>
<string name="field_not_set">Not set</string>
<string name="bundle_missing">Missing</string>
<string name="bundle_error">Error</string>
<string name="bundle_error_description">Bundle 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="bundle_name_default">Default</string>
<string name="bundle_name_fallback">Unnamed</string>
<string name="patches_missing">Missing</string>
<string name="patches_error">Error</string>
<string name="patches_error_description">Patches could not be loaded. Click to view the error</string>
<string name="patches_not_downloaded">Patches has not been downloaded. Click here to download it</string>
<string name="patches_name_default">Patches</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_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_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_bundle">Resets patch selection for bundle</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_bundle_description">Resets the patch selection for all patches in a bundle</string>
<string name="patch_selection_reset_patches">Resets patch selection for a specific patches</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_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_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_bundle">Resets patch options for bundle</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_bundle_description">Resets patch options for all patches in a bundle</string>
<string name="patch_options_reset_all">Reset patch options</string>
<string name="patch_options_reset">Reset patch options</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_all">Reset patch options for all</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="downloader_plugins">Plugins</string>
@ -164,7 +161,7 @@
<string name="search_apps">Search apps…</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="ok">OK</string>
@ -202,8 +199,8 @@
<string name="debug_logs_export_success">Exported logs</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_dialog_title">Set custom 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_title">Change API URL</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_save">Set</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_memory_limit">Memory limits</string>
<string name="device_memory_limit_format">%1$dMB (Normal) - %2$dMB (Large)</string>
<string name="patch_bundles_section">Patch bundles</string>
<string name="patch_bundles_force_download">Force download all patch bundles</string>
<string name="patch_bundles_reset">Reset patch bundles</string>
<string name="patches_force_download">Force download all patches</string>
<string name="patches_reset">Reset patches</string>
<string name="patching">Patching</string>
<string name="signing">Signing</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_bundles">Patch bundles</string>
<string name="tab_patches">Patches</string>
<string name="delete">Delete</string>
<string name="refresh">Refresh</string>
<string name="continue_anyways">Continue anyways</string>
<string name="download_another_version">Download another version</string>
<string name="download_app">Download app</string>
<string name="download_apk">Download APK file</string>
<string name="source_download_fail">Failed to download patch bundle: %s</string>
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
<string name="patches_download_fail">Failed to download patches: %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="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="universal_patches">Universal patches</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_write_patched">Write 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_description">Are you sure you want to stop the patching process?</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="developer_options">Developer options</string>
<string name="developer_options_description">Options for debugging issues</string>
<string name="bundle_input_source_url">Source URL</string>
<string name="bundle_update_success">Successfully updated %s</string>
<string name="bundle_update_unavailable">No update available for %s</string>
<string name="bundle_auto_update">Auto update</string>
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string>
<string name="bundle_view_patches">View patches</string>
<string name="bundle_view_patches_any_version">Any version</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="patches_update_success">Successfully updated %s</string>
<string name="patches_update_unavailable">No update available for %s</string>
<string name="view_patches">View patches</string>
<string name="patches_view_any_version">Any version</string>
<string name="patches_view_any_package">Any package</string>
<string name="patches_delete_single_dialog_description">Are you sure you want to delete \"%s\"?</string>
<string name="patches_delete_multiple_dialog_description">Are you sure you want to delete the selected patches?</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>
@ -389,7 +380,7 @@
<string name="save_with_count">Save (%1$s)</string>
<string name="update">Update</string>
<string name="empty">Empty</string>
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>
<string name="installing_message">Tap on <b>Update</b> when prompted.\nReVanced Manager will close when updating.</string>
<string name="no_changelogs_found">No changelogs found</string>
<string name="just_now">Just now</string>
<string name="minutes_ago">%sm ago</string>
@ -413,10 +404,9 @@
<string name="no_contributors_found">No contributors found</string>
<string name="select">Select</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_bundle_type_dialog_description">Add a new bundle from a URL or storage</string>
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
<string name="remote_bundle_description">Import remote files from a URL, can automatically update</string>
<string name="select_patches_type_dialog_description">Add new patches from URL or local files</string>
<string name="local_patches_description">Add patches from local storage.</string>
<string name="remote_patches_description">Add patches from URL. Patches can automatically update.</string>
<string name="recommended">Recommended</string>
<string name="installation_failed_dialog_title">Installation failed</string>
@ -441,9 +431,10 @@
<string name="about_device">About device</string>
<string name="enter_url">Enter URL</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="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_patch">Incompatible patch</string>
<string name="any_version">Any</string>