Compare commits

..

11 Commits

30 changed files with 8771 additions and 164 deletions

View File

@ -22,4 +22,12 @@ jobs:
- name: Build - name: Build
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build --no-daemon run: ./gradlew assembleRelease --no-daemon
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: revanced-manager
path: |
app/build/outputs/apk/release/revanced-manager*.apk
app/build/outputs/apk/release/revanced-manager*.apk.asc

View File

@ -44,6 +44,13 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Setup keystore - name: Setup keystore
run: | run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks" echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
@ -62,4 +69,4 @@ jobs:
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-name: 'ReVanced Manager ${{ steps.release.outputs.new_release_git_tag }}' subject-name: 'ReVanced Manager ${{ steps.release.outputs.new_release_git_tag }}'
subject-path: build/app/outputs/apk/release/revanced-manager-*.apk subject-path: app/build/outputs/apk/release/revanced-manager*.apk

View File

@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
<application <application
android:name=".ManagerApplication" android:name=".ManagerApplication"

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

@ -27,10 +27,10 @@ abstract class OptionDao {
abstract suspend fun createOptionGroup(group: OptionGroup) abstract suspend fun createOptionGroup(group: OptionGroup)
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid") @Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
abstract suspend fun clearForPatchBundle(uid: Int) abstract suspend fun resetOptionsForPatchBundle(uid: Int)
@Query("DELETE FROM option_groups WHERE package_name = :packageName") @Query("DELETE FROM option_groups WHERE package_name = :packageName")
abstract suspend fun clearForPackage(packageName: String) abstract suspend fun resetOptionsForPackage(packageName: String)
@Query("DELETE FROM option_groups") @Query("DELETE FROM option_groups")
abstract suspend fun reset() abstract suspend fun reset()

View File

@ -5,6 +5,7 @@ import androidx.room.Insert
import androidx.room.MapColumn import androidx.room.MapColumn
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao @Dao
abstract class SelectionDao { abstract class SelectionDao {
@ -34,11 +35,14 @@ abstract class SelectionDao {
@Insert @Insert
abstract suspend fun createSelection(selection: PatchSelection) abstract suspend fun createSelection(selection: PatchSelection)
@Query("SELECT package_name FROM patch_selections")
abstract fun getPackagesWithSelection(): Flow<List<String>>
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid") @Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
abstract suspend fun clearForPatchBundle(uid: Int) abstract suspend fun resetForPatchBundle(uid: Int)
@Query("DELETE FROM patch_selections WHERE package_name = :packageName") @Query("DELETE FROM patch_selections WHERE package_name = :packageName")
abstract suspend fun clearForPackage(packageName: String) abstract suspend fun resetForPackage(packageName: String)
@Query("DELETE FROM patch_selections") @Query("DELETE FROM patch_selections")
abstract suspend fun reset() abstract suspend fun reset()

View File

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

View File

@ -38,6 +38,9 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
suspend fun getName() = nameFlow.first() suspend fun getName() = nameFlow.first()
val versionFlow = state.map { it.patchBundleOrNull()?.readManifestAttribute("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.
*/ */
@ -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)

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

@ -76,7 +76,7 @@ class PatchOptionsRepository(db: AppDatabase) {
fun getPackagesWithSavedOptions() = fun getPackagesWithSavedOptions() =
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged() dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName) suspend fun resetOptionsForPackage(packageName: String) = dao.resetOptionsForPackage(packageName)
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid) suspend fun resetOptionsForPatchBundle(uid: Int) = dao.resetOptionsForPatchBundle(uid)
suspend fun reset() = dao.reset() suspend fun reset() = dao.reset()
} }

View File

@ -3,6 +3,8 @@ package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.selection.PatchSelection import app.revanced.manager.data.room.selection.PatchSelection
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
class PatchSelectionRepository(db: AppDatabase) { class PatchSelectionRepository(db: AppDatabase) {
private val dao = db.selectionDao() private val dao = db.selectionDao()
@ -25,8 +27,15 @@ class PatchSelectionRepository(db: AppDatabase) {
) )
}) })
suspend fun clearSelection(packageName: String) { fun getPackagesWithSavedSelection() =
dao.clearForPackage(packageName) dao.getPackagesWithSelection().map(Iterable<String>::toSet).distinctUntilChanged()
suspend fun resetSelectionForPackage(packageName: String) {
dao.resetForPackage(packageName)
}
suspend fun resetSelectionForPatchBundle(uid: Int) {
dao.resetForPatchBundle(uid)
} }
suspend fun reset() = dao.reset() suspend fun reset() = dao.reset()
@ -34,7 +43,7 @@ class PatchSelectionRepository(db: AppDatabase) {
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid) suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
suspend fun import(bundleUid: Int, selection: SerializedSelection) { suspend fun import(bundleUid: Int, selection: SerializedSelection) {
dao.clearForPatchBundle(bundleUid) dao.resetForPatchBundle(bundleUid)
dao.updateSelections(selection.entries.associate { (packageName, patches) -> dao.updateSelections(selection.entries.associate { (packageName, patches) ->
getOrCreateSelection(bundleUid, packageName) to patches.toSet() getOrCreateSelection(bundleUid, packageName) to patches.toSet()
}) })

View File

@ -42,9 +42,8 @@ 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)
}
if (viewCurrentBundlePatches) { if (viewCurrentBundlePatches) {
BundlePatchesDialog( BundlePatchesDialog(
@ -98,8 +97,8 @@ fun BundleInformationDialog(
name = bundleName, 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,
onAutoUpdateChange = { onAutoUpdateChange = {
composableScope.launch { composableScope.launch {
bundle.asRemoteOrNull?.setAutoUpdate(it) bundle.asRemoteOrNull?.setAutoUpdate(it)

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) {
@ -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))
} }
}, },

View File

@ -13,11 +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.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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
@ -54,6 +57,8 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
} }
bundles.forEach { bundles.forEach {
val name by it.nameState val name by it.nameState
val version by it.versionFlow.collectAsStateWithLifecycle(null)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
@ -65,7 +70,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
} }
) { ) {
Text( Text(
name, "$name $version",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )

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

@ -1,6 +1,17 @@
package app.revanced.manager.ui.component.settings package app.revanced.manager.ui.component.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -10,6 +21,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable @Composable
fun SettingsListItem( fun SettingsListItem(
@ -67,4 +82,48 @@ fun SettingsListItem(
colors = colors, colors = colors,
tonalElevation = tonalElevation, tonalElevation = tonalElevation,
shadowElevation = shadowElevation shadowElevation = shadowElevation
) )
@Composable
fun ExpandableSettingListItem(
headlineContent: String,
supportingContent: String,
expandableContent: @Composable () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
) {
SettingsListItem(
modifier = Modifier
.clickable{ expanded = !expanded },
headlineContent = headlineContent,
supportingContent = supportingContent,
trailingContent = {
Icon(
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = null
)
}
)
AnimatedVisibility(visible = expanded) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp, start = 16.dp, end = 16.dp)
.animateContentSize(
animationSpec = tween(
durationMillis = 500,
easing = FastOutSlowInEasing
)
)
) {
expandableContent()
}
}
}
}

View File

@ -79,7 +79,7 @@ data class BundleInfo(
targetList.add(it) targetList.add(it)
} }
BundleInfo(source.getName(), source.currentVersion(), source.uid, compatible, incompatible, universal) BundleInfo(source.getName(), bundle.readManifestAttribute("Version"), source.uid, compatible, incompatible, universal)
} }
} }

View File

@ -62,6 +62,10 @@ fun PatcherScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
viewModel: PatcherViewModel viewModel: PatcherViewModel
) { ) {
fun onLeave() {
viewModel.onBack()
onBackClick()
}
val context = LocalContext.current val context = LocalContext.current
val exportApkLauncher = val exportApkLauncher =
@ -72,7 +76,14 @@ fun PatcherScreen(
var showInstallPicker by rememberSaveable { mutableStateOf(false) } var showInstallPicker by rememberSaveable { mutableStateOf(false) }
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) } var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
BackHandler(onBack = { showDismissConfirmationDialog = true }) fun onPageBack() {
if(patcherSucceeded == null)
showDismissConfirmationDialog = true
else
onLeave()
}
BackHandler(onBack = ::onPageBack)
val steps by remember { val steps by remember {
derivedStateOf { derivedStateOf {
@ -99,10 +110,7 @@ fun PatcherScreen(
if (showDismissConfirmationDialog) { if (showDismissConfirmationDialog) {
ConfirmDialog( ConfirmDialog(
onDismiss = { showDismissConfirmationDialog = false }, onDismiss = { showDismissConfirmationDialog = false },
onConfirm = { onConfirm = ::onLeave,
viewModel.onBack()
onBackClick()
},
title = stringResource(R.string.patcher_stop_confirm_title), title = stringResource(R.string.patcher_stop_confirm_title),
description = stringResource(R.string.patcher_stop_confirm_description), description = stringResource(R.string.patcher_stop_confirm_description),
icon = Icons.Outlined.Cancel icon = Icons.Outlined.Cancel
@ -150,7 +158,7 @@ fun PatcherScreen(
AppTopBar( AppTopBar(
title = stringResource(R.string.patcher), title = stringResource(R.string.patcher),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
onBackClick = { showDismissConfirmationDialog = true } onBackClick = ::onPageBack
) )
}, },
bottomBar = { bottomBar = {
@ -229,4 +237,4 @@ fun PatcherScreen(
} }
} }
} }
} }

View File

@ -2,9 +2,7 @@ package app.revanced.manager.ui.screen.settings
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -13,13 +11,10 @@ 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.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
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.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -37,7 +32,6 @@ 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.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -63,7 +57,6 @@ fun DownloadsSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
viewModel: DownloadsViewModel = koinViewModel() viewModel: DownloadsViewModel = koinViewModel()
) { ) {
val context = LocalContext.current
val pullRefreshState = rememberPullToRefreshState() val pullRefreshState = rememberPullToRefreshState()
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList()) val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle() val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
@ -88,7 +81,7 @@ fun DownloadsSettingsScreen(
onBackClick = onBackClick, onBackClick = onBackClick,
actions = { actions = {
if (viewModel.appSelection.isNotEmpty()) { if (viewModel.appSelection.isNotEmpty()) {
IconButton(onClick = { viewModel.deleteApps() }) { IconButton(onClick = { showDeleteConfirmationDialog = true }) {
Icon(Icons.Default.Delete, stringResource(R.string.delete)) Icon(Icons.Default.Delete, stringResource(R.string.delete))
} }
} }
@ -149,20 +142,15 @@ fun DownloadsSettingsScreen(
.digest(androidSignature.toByteArray()) .digest(androidSignature.toByteArray())
hash.toHexString(format = HexFormat.UpperCase) hash.toHexString(format = HexFormat.UpperCase)
} }
val appName = remember {
packageInfo.applicationInfo?.loadLabel(context.packageManager)
?.toString()
?: packageName
}
when (state) { when (state) {
is DownloaderPluginState.Loaded -> TrustDialog( is DownloaderPluginState.Loaded -> TrustDialog(
title = R.string.downloader_plugin_revoke_trust_dialog_title, title = R.string.downloader_plugin_revoke_trust_dialog_title,
body = stringResource( body = stringResource(
R.string.downloader_plugin_trust_dialog_body, R.string.downloader_plugin_trust_dialog_body,
packageName,
signature
), ),
pluginName = appName,
signature = signature,
onDismiss = ::dismiss, onDismiss = ::dismiss,
onConfirm = { onConfirm = {
viewModel.revokePluginTrust(packageName) viewModel.revokePluginTrust(packageName)
@ -177,20 +165,19 @@ fun DownloadsSettingsScreen(
onDismiss = ::dismiss onDismiss = ::dismiss
) )
is DownloaderPluginState.Untrusted -> is DownloaderPluginState.Untrusted -> TrustDialog(
TrustDialog( title = R.string.downloader_plugin_trust_dialog_title,
title = R.string.downloader_plugin_trust_dialog_title, body = stringResource(
body = stringResource( R.string.downloader_plugin_trust_dialog_body,
R.string.downloader_plugin_trust_dialog_body packageName,
), signature
pluginName = appName, ),
signature = signature, onDismiss = ::dismiss,
onDismiss = ::dismiss, onConfirm = {
onConfirm = { viewModel.trustPlugin(packageName)
viewModel.trustPlugin(packageName) dismiss()
dismiss() }
} )
)
} }
} }
@ -259,8 +246,6 @@ fun DownloadsSettingsScreen(
private fun TrustDialog( private fun TrustDialog(
@StringRes title: Int, @StringRes title: Int,
body: String, body: String,
pluginName: String,
signature: String,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onConfirm: () -> Unit onConfirm: () -> Unit
) { ) {
@ -277,35 +262,6 @@ private fun TrustDialog(
} }
}, },
title = { Text(stringResource(title)) }, title = { Text(stringResource(title)) },
text = { text = { Text(body) }
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(body)
Card {
Column(
Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
stringResource(
R.string.downloader_plugin_trust_dialog_plugin,
pluginName
),
)
OutlinedCard(
colors = CardDefaults.outlinedCardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
)
) {
Text(
stringResource(
R.string.downloader_plugin_trust_dialog_signature,
signature.chunked(2).joinToString(" ")
), modifier = Modifier.padding(12.dp)
)
}
}
}
}
}
) )
} }

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.WarningAmber
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
@ -28,6 +29,7 @@ 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.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -42,11 +44,14 @@ import androidx.lifecycle.viewModelScope
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.ConfirmDialog
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.PasswordField import app.revanced.manager.ui.component.PasswordField
import app.revanced.manager.ui.component.bundle.BundleSelector import app.revanced.manager.ui.component.bundle.BundleSelector
import app.revanced.manager.ui.component.settings.ExpandableSettingListItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.ImportExportViewModel import app.revanced.manager.ui.viewmodel.ImportExportViewModel
import app.revanced.manager.ui.viewmodel.ResetDialogState
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -59,6 +64,8 @@ fun ImportExportSettingsScreen(
vm: ImportExportViewModel = koinViewModel() vm: ImportExportViewModel = koinViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
val importKeystoreLauncher = val importKeystoreLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
@ -70,6 +77,7 @@ fun ImportExportSettingsScreen(
} }
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList()) val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
val packagesWithSelections by vm.packagesWithSelection.collectAsStateWithLifecycle(initialValue = emptySet())
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet()) val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
vm.selectionAction?.let { action -> vm.selectionAction?.let { action ->
@ -107,6 +115,20 @@ fun ImportExportSettingsScreen(
) )
} }
vm.resetDialogState?.let {
with(vm.resetDialogState!!) {
ConfirmDialog(
onDismiss = { vm.resetDialogState = null },
onConfirm = onConfirm,
title = stringResource(titleResId),
description = dialogOptionName?.let {
stringResource(descriptionResId, it)
} ?: stringResource(descriptionResId),
icon = Icons.Outlined.WarningAmber
)
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
@ -124,28 +146,7 @@ fun ImportExportSettingsScreen(
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
var showPackageSelector by rememberSaveable { selectorDialog?.invoke()
mutableStateOf(false)
}
var showBundleSelector by rememberSaveable {
mutableStateOf(false)
}
if (showPackageSelector) {
PackageSelector(packages = packagesWithOptions) { selected ->
selected?.let(vm::resetOptionsForPackage)
showPackageSelector = false
}
}
if (showBundleSelector) {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.let(vm::clearOptionsForBundle)
showBundleSelector = false
}
}
GroupHeader(stringResource(R.string.import_)) GroupHeader(stringResource(R.string.import_))
GroupItem( GroupItem(
@ -181,32 +182,126 @@ fun ImportExportSettingsScreen(
GroupHeader(stringResource(R.string.reset)) GroupHeader(stringResource(R.string.reset))
GroupItem( GroupItem(
onClick = vm::regenerateKeystore, onClick = {
vm.resetDialogState = ResetDialogState.Keystore {
vm.regenerateKeystore()
}
},
headline = R.string.regenerate_keystore, headline = R.string.regenerate_keystore,
description = R.string.regenerate_keystore_description description = R.string.regenerate_keystore_description
) )
GroupItem(
onClick = vm::resetSelection, // TODO: allow resetting selection for specific bundle or package name. ExpandableSettingListItem(
headline = R.string.reset_patch_selection, headlineContent = stringResource(R.string.reset_patch_selection),
description = R.string.reset_patch_selection_description supportingContent = stringResource(R.string.reset_patch_selection_description),
expandableContent = {
GroupItem(
onClick = {
vm.resetDialogState = ResetDialogState.PatchSelectionAll {
vm.resetSelection()
}
},
headline = R.string.patch_selection_reset_all,
description = R.string.patch_selection_reset_all_description
)
GroupItem(
onClick = {
selectorDialog = {
PackageSelector(packages = packagesWithSelections) { packageName ->
packageName?.also {
vm.resetDialogState =
ResetDialogState.PatchSelectionPackage(packageName) {
vm.resetSelectionForPackage(packageName)
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_selection_reset_package,
description = R.string.patch_selection_reset_package_description
)
if (patchBundles.isNotEmpty()) {
GroupItem(
onClick = {
selectorDialog = {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.also {
coroutineScope.launch {
vm.resetDialogState =
ResetDialogState.PatchSelectionBundle(bundle.getName()) {
vm.resetSelectionForPatchBundle(bundle)
}
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_selection_reset_bundle,
description = R.string.patch_selection_reset_bundle_description
)
}
}
) )
GroupItem(
onClick = vm::resetOptions, // TODO: patch options import/export. ExpandableSettingListItem(
headline = R.string.patch_options_reset_all, headlineContent = stringResource(R.string.reset_patch_options),
description = R.string.patch_options_reset_all_description, supportingContent = stringResource(R.string.reset_patch_options_description),
expandableContent = {
GroupItem(
onClick = {
vm.resetDialogState = ResetDialogState.PatchOptionsAll {
vm.resetOptions()
}
}, // TODO: patch options import/export.
headline = R.string.patch_options_reset_all,
description = R.string.patch_options_reset_all_description,
)
GroupItem(
onClick = {
selectorDialog = {
PackageSelector(packages = packagesWithOptions) { packageName ->
packageName?.also {
vm.resetDialogState =
ResetDialogState.PatchOptionPackage(packageName) {
vm.resetOptionsForPackage(packageName)
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_options_reset_package,
description = R.string.patch_options_reset_package_description
)
if (patchBundles.isNotEmpty()) {
GroupItem(
onClick = {
selectorDialog = {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.also {
coroutineScope.launch {
vm.resetDialogState =
ResetDialogState.PatchOptionBundle(bundle.getName()) {
vm.resetOptionsForBundle(bundle)
}
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_options_reset_bundle,
description = R.string.patch_options_reset_bundle_description,
)
}
}
) )
GroupItem(
onClick = { showPackageSelector = true },
headline = R.string.patch_options_reset_package,
description = R.string.patch_options_reset_package_description
)
if (patchBundles.size > 1) {
GroupItem(
onClick = { showBundleSelector = true },
headline = R.string.patch_options_reset_bundle,
description = R.string.patch_options_reset_bundle_description,
)
}
} }
} }
} }

View File

@ -87,7 +87,7 @@ fun UpdatesSettingsScreen(
BooleanItem( BooleanItem(
preference = vm.showManagerUpdateDialogOnLaunch, preference = vm.showManagerUpdateDialogOnLaunch,
headline = R.string.show_manager_update_dialog_on_launch, headline = R.string.show_manager_update_dialog_on_launch,
description = R.string.update_checking_manager_description description = R.string.show_manager_update_dialog_on_launch_description
) )
} }
} }

View File

@ -4,6 +4,7 @@ import android.app.Application
import android.net.Uri import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -34,6 +35,59 @@ import java.nio.file.StandardCopyOption
import kotlin.io.path.deleteExisting import kotlin.io.path.deleteExisting
import kotlin.io.path.inputStream import kotlin.io.path.inputStream
sealed class ResetDialogState(
@StringRes val titleResId: Int,
@StringRes val descriptionResId: Int,
val onConfirm: () -> Unit,
val dialogOptionName: String? = null
) {
class Keystore(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.regenerate_keystore,
descriptionResId = R.string.regenerate_keystore_dialog_description,
onConfirm = onConfirm
)
class PatchSelectionAll(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_all,
descriptionResId = R.string.patch_selection_reset_all_dialog_description,
onConfirm = onConfirm
)
class PatchSelectionPackage(dialogOptionName:String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_package,
descriptionResId = R.string.patch_selection_reset_package_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_bundle,
descriptionResId = R.string.patch_selection_reset_bundle_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchOptionsAll(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_all,
descriptionResId = R.string.patch_options_reset_all_dialog_description,
onConfirm = onConfirm
)
class PatchOptionPackage(dialogOptionName:String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_package,
descriptionResId = R.string.patch_options_reset_package_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_bundle,
descriptionResId = R.string.patch_options_reset_bundle_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
}
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
class ImportExportViewModel( class ImportExportViewModel(
private val app: Application, private val app: Application,
@ -51,15 +105,18 @@ class ImportExportViewModel(
private var keystoreImportPath by mutableStateOf<Path?>(null) private var keystoreImportPath by mutableStateOf<Path?>(null)
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null } val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
var resetDialogState by mutableStateOf<ResetDialogState?>(null)
val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions() val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions()
val packagesWithSelection = selectionRepository.getPackagesWithSavedSelection()
fun resetOptionsForPackage(packageName: String) = viewModelScope.launch { fun resetOptionsForPackage(packageName: String) = viewModelScope.launch {
optionsRepository.clearOptionsForPackage(packageName) optionsRepository.resetOptionsForPackage(packageName)
app.toast(app.getString(R.string.patch_options_reset_toast)) app.toast(app.getString(R.string.patch_options_reset_toast))
} }
fun clearOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch { fun resetOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
optionsRepository.clearOptionsForPatchBundle(patchBundle.uid) optionsRepository.resetOptionsForPatchBundle(patchBundle.uid)
app.toast(app.getString(R.string.patch_options_reset_toast)) app.toast(app.getString(R.string.patch_options_reset_toast))
} }
@ -135,6 +192,16 @@ class ImportExportViewModel(
app.toast(app.getString(R.string.reset_patch_selection_success)) app.toast(app.getString(R.string.reset_patch_selection_success))
} }
fun resetSelectionForPackage(packageName: String) = viewModelScope.launch {
selectionRepository.resetSelectionForPackage(packageName)
app.toast(app.getString(R.string.reset_patch_selection_success))
}
fun resetSelectionForPatchBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
selectionRepository.resetSelectionForPatchBundle(patchBundle.uid)
app.toast(app.getString(R.string.reset_patch_selection_success))
}
fun executeSelectionAction(target: Uri) = viewModelScope.launch { fun executeSelectionAction(target: Uri) = viewModelScope.launch {
val source = selectedBundle!! val source = selectedBundle!!
val action = selectionAction!! val action = selectionAction!!

View File

@ -131,7 +131,7 @@ class SelectedAppInfoViewModel(
viewModelScope.launch { viewModelScope.launch {
if (!persistConfiguration) return@launch // TODO: save options for patched apps. if (!persistConfiguration) return@launch // TODO: save options for patched apps.
state.value = withContext(Dispatchers.Default) { options = withContext(Dispatchers.Default) {
val bundlePatches = bundleRepository.bundles.first() val bundlePatches = bundleRepository.bundles.first()
.mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } } .mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } }
@ -143,7 +143,7 @@ class SelectedAppInfoViewModel(
} }
private set private set
private var selectionState by savedStateHandle.saveable { private var selectionState: SelectionState by savedStateHandle.saveable {
if (input.patches != null) if (input.patches != null)
return@saveable mutableStateOf(SelectionState.Customized(input.patches)) return@saveable mutableStateOf(SelectionState.Customized(input.patches))
@ -155,7 +155,7 @@ class SelectedAppInfoViewModel(
val previous = selectionRepository.getSelection(packageName) val previous = selectionRepository.getSelection(packageName)
if (previous.values.sumOf { it.size } == 0) return@launch if (previous.values.sumOf { it.size } == 0) return@launch
selection.value = SelectionState.Customized(previous) selectionState = SelectionState.Customized(previous)
} }
selection selection
@ -305,7 +305,7 @@ class SelectedAppInfoViewModel(
if (!persistConfiguration) return@launch if (!persistConfiguration) return@launch
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
selection?.let { selectionRepository.updateSelection(packageName, it) } selection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.clearSelection(packageName) ?: selectionRepository.resetSelectionForPackage(packageName)
optionsRepository.saveOptions(packageName, filteredOptions) optionsRepository.saveOptions(packageName, filteredOptions)
} }

View File

@ -112,7 +112,7 @@ class PM(
app.packageManager.getPackageInfo(packageName, PackageInfoFlags.of(flags.toLong())) app.packageManager.getPackageInfo(packageName, PackageInfoFlags.of(flags.toLong()))
else else
app.packageManager.getPackageInfo(packageName, flags) app.packageManager.getPackageInfo(packageName, flags)
} catch (e: NameNotFoundException) { } catch (_: NameNotFoundException) {
null null
} }
@ -184,6 +184,8 @@ class PM(
get() = PackageInstaller.SessionParams( get() = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL PackageInstaller.SessionParams.MODE_FULL_INSTALL
).apply { ).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
setRequestUpdateOwnership(true)
setInstallReason(PackageManager.INSTALL_REASON_USER) setInstallReason(PackageManager.INSTALL_REASON_USER)
} }

View File

@ -118,6 +118,7 @@
<string name="export_keystore_success">Exported keystore</string> <string name="export_keystore_success">Exported keystore</string>
<string name="regenerate_keystore">Regenerate keystore</string> <string name="regenerate_keystore">Regenerate keystore</string>
<string name="regenerate_keystore_description">Generate a new keystore</string> <string name="regenerate_keystore_description">Generate a new keystore</string>
<string name="regenerate_keystore_dialog_description">You are about to regenerate your keystore the manager will use during the patching process.\n\nYou will not be able to update the previously installed apps from this source.</string>
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string> <string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
<string name="import_patch_selection">Import patch selection</string> <string name="import_patch_selection">Import patch selection</string>
<string name="import_patch_selection_description">Import patch selection from a JSON file</string> <string name="import_patch_selection_description">Import patch selection from a JSON file</string>
@ -129,12 +130,26 @@
<string name="export_patch_selection_success">Exported patch selection</string> <string name="export_patch_selection_success">Exported patch selection</string>
<string name="reset_patch_selection">Reset patch selection</string> <string name="reset_patch_selection">Reset patch selection</string>
<string name="reset_patch_selection_description">Reset the stored patch selection</string> <string name="reset_patch_selection_description">Reset the stored patch selection</string>
<string name="reset_patch_options">Reset patch options</string>
<string name="reset_patch_options_description">Reset the stored patch options</string>
<string name="reset_patch_selection_success">Patch selection has been reset</string> <string name="reset_patch_selection_success">Patch selection has been reset</string>
<string name="patch_selection_reset_all">Reset all patch selection</string>
<string name="patch_selection_reset_all_dialog_description">You are about to reset all the patch selections. You will need to manually select each patch again.</string>
<string name="patch_selection_reset_all_description">Reset all the patch selections</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_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_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_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_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_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_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_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>
<string name="downloader_plugin_state_trusted">Trusted</string> <string name="downloader_plugin_state_trusted">Trusted</string>
@ -142,9 +157,7 @@
<string name="downloader_plugin_state_untrusted">Untrusted</string> <string name="downloader_plugin_state_untrusted">Untrusted</string>
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string> <string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string> <string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
<string name="downloader_plugin_trust_dialog_body">By continuing you\'ve agreed to running external plugins.\n\nDo not allow any suspicious plugin(s) to be installed as they can run arbitrary code.</string> <string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string>
<string name="downloader_plugin_trust_dialog_signature">Signature:\n\n%s</string>
<string name="downloader_plugin_trust_dialog_plugin">Plugin:\n%s</string>
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string> <string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string> <string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
<string name="downloader_settings_no_apps">No downloaded apps found</string> <string name="downloader_settings_no_apps">No downloaded apps found</string>

View File

@ -9,7 +9,7 @@ appcompat = "1.7.0"
preferences-datastore = "1.1.2" preferences-datastore = "1.1.2"
work-runtime = "2.10.1" work-runtime = "2.10.1"
compose-bom = "2025.05.00" compose-bom = "2025.05.00"
navigation = "2.9.0" navigation = "2.8.6"
accompanist = "0.37.0" accompanist = "0.37.0"
placeholder = "1.1.2" placeholder = "1.1.2"
reorderable = "2.4.3" reorderable = "2.4.3"

8365
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
{ {
"devDependencies": { "devDependencies": {
"@anolilab/multi-semantic-release": "^1.1.10", "@anolilab/multi-semantic-release": "^1.1.10",
"@revanced/gradle-semantic-release-plugin": "^1.10.1", "gradle-semantic-release-plugin": "^1.10.1",
"@saithodev/semantic-release-backmerge": "^4.0.1", "@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3", "@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1"
} }
} }