Compare commits

..

4 Commits

Author SHA1 Message Date
1851a35e10 Revert api_source to api_url 2025-02-05 19:04:36 +03:00
f4a2b06276 Remove unnecessary changes and deprecated code 2025-02-05 19:03:25 +03:00
bbfd625ac1 fix: Compilation 2025-02-02 13:03:11 +07:00
c012a3e9c0 feat: Settings v2
Signed-off-by: validcube <pun.butrach@gmail.com>
2025-02-02 00:41:37 +07:00
69 changed files with 1970 additions and 726 deletions

View File

@ -1,27 +0,0 @@
name: Open a PR to main
on:
push:
branches:
- dev
workflow_dispatch:
env:
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
jobs:
pull-request:
name: Open pull request
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Open pull request
uses: repo-sync/pull-request@v2
with:
destination_branch: main
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: |
This pull request will ${{ env.MESSAGE }}.
pr_draft: true

View File

@ -23,8 +23,8 @@ jobs:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
- name: Cache Gradle - name: Set up Gradle
uses: burrunan/gradle-cache-action@v1 uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle - name: Build with Gradle
env: env:

View File

@ -23,8 +23,8 @@ jobs:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
- name: Cache Gradle - name: Set up Gradle
uses: burrunan/gradle-cache-action@v1 uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle - name: Build with Gradle
env: env:
@ -44,10 +44,9 @@ jobs:
- name: Add version to APK - name: Add version to APK
run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
- name: Attest - name: Generate artifact attestation
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v1
with: with:
subject-name: 'ReVanced Manager ${{ env.RELEASE_VERSION }}'
subject-path: revanced-manager-${{ env.RELEASE_VERSION }}.apk subject-path: revanced-manager-${{ env.RELEASE_VERSION }}.apk
- name: Publish release APK - name: Publish release APK

View File

@ -1,18 +0,0 @@
name: Update Gradle wrapper
on:
schedule:
- cron: "0 0 1 * *"
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v1
with:
target-branch: dev

View File

@ -161,7 +161,7 @@ dependencies {
implementation(libs.revanced.library) implementation(libs.revanced.library)
// Downloader plugins // Downloader plugins
implementation(libs.plugin.api) implementation(project(":downloader-plugin"))
// Native processes // Native processes
implementation(libs.kotlin.process) implementation(libs.kotlin.process)

View File

@ -44,14 +44,13 @@ import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.screen.UpdateScreen import app.revanced.manager.ui.screen.UpdateScreen
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
import app.revanced.manager.ui.screen.settings.BackupRestoreSettingsScreen
import app.revanced.manager.ui.screen.settings.ContributorScreen import app.revanced.manager.ui.screen.settings.ContributorScreen
import app.revanced.manager.ui.screen.settings.DeveloperOptionsScreen import app.revanced.manager.ui.screen.settings.DeveloperOptionsScreen
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen
import app.revanced.manager.ui.screen.settings.LicensesScreen import app.revanced.manager.ui.screen.settings.LicensesScreen
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel import app.revanced.manager.ui.viewmodel.MainViewModel
@ -270,32 +269,28 @@ private fun ReVancedManager(vm: MainViewModel) {
} }
composable<Settings.General> { composable<Settings.General> {
GeneralSettingsScreen(onBackClick = navController::popBackStack) GeneralSettingsScreen(
onBackClick = navController::popBackStack,
onUpdateClick = { navController.navigate(Update()) }
)
} }
composable<Settings.Advanced> { composable<Settings.Advanced> {
AdvancedSettingsScreen(onBackClick = navController::popBackStack) AdvancedSettingsScreen(onBackClick = navController::popBackStack)
} }
composable<Settings.Updates> {
UpdatesSettingsScreen(
onBackClick = navController::popBackStack,
onChangelogClick = { navController.navigate(Settings.Changelogs) },
onUpdateClick = { navController.navigate(Update()) }
)
}
composable<Settings.Downloads> { composable<Settings.Downloads> {
DownloadsSettingsScreen(onBackClick = navController::popBackStack) DownloadsSettingsScreen(onBackClick = navController::popBackStack)
} }
composable<Settings.ImportExport> { composable<Settings.ImportExport> {
ImportExportSettingsScreen(onBackClick = navController::popBackStack) BackupRestoreSettingsScreen(onBackClick = navController::popBackStack)
} }
composable<Settings.About> { composable<Settings.About> {
AboutSettingsScreen( AboutSettingsScreen(
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
onChangelogClick = { navController.navigate(Settings.Changelogs) },
navigate = navController::navigate navigate = navController::navigate
) )
} }

View File

@ -81,41 +81,3 @@ fun AppTopBar(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTopBar(
title: @Composable () -> Unit,
onBackClick: (() -> Unit)? = null,
backIcon: @Composable (() -> Unit) = @Composable {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
R.string.back
)
)
},
actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null,
applyContainerColor: Boolean = false
) {
val containerColor = if (applyContainerColor) {
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
} else {
Color.Unspecified
}
TopAppBar(
title = title,
scrollBehavior = scrollBehavior,
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
backIcon()
}
}
},
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = containerColor
)
)
}

View File

@ -16,7 +16,6 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.bundles.LocalPatchBundle import app.revanced.manager.domain.bundles.LocalPatchBundle
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
@ -24,7 +23,6 @@ import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefaul
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import app.revanced.manager.ui.component.ExceptionViewerDialog import app.revanced.manager.ui.component.ExceptionViewerDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -34,8 +32,6 @@ fun BundleInformationDialog(
bundle: PatchBundleSource, bundle: PatchBundleSource,
onUpdate: () -> Unit, onUpdate: () -> Unit,
) { ) {
val networkInfo = koinInject<NetworkInfo>()
val hasNetwork = remember { networkInfo.isConnected() }
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
var viewCurrentBundlePatches by remember { mutableStateOf(false) } var viewCurrentBundlePatches by remember { mutableStateOf(false) }
val isLocal = bundle is LocalPatchBundle val isLocal = bundle is LocalPatchBundle
@ -85,7 +81,7 @@ fun BundleInformationDialog(
) )
} }
} }
if (!isLocal && hasNetwork) { if (!isLocal) {
IconButton(onClick = onUpdate) { IconButton(onClick = onUpdate) {
Icon( Icon(
Icons.Outlined.Update, Icons.Outlined.Update,

View File

@ -21,25 +21,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.*
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.*
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -59,11 +42,7 @@ import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.*
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.FloatInputDialog
import app.revanced.manager.ui.component.IntInputDialog
import app.revanced.manager.ui.component.LongInputDialog
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticRadioButton import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.haptics.HapticSwitch import app.revanced.manager.ui.component.haptics.HapticSwitch
@ -623,10 +602,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
interactionSource = interactionSource, interactionSource = interactionSource,
onLongClickLabel = stringResource(R.string.select), onLongClickLabel = stringResource(R.string.select),
onLongClick = { onLongClick = {
if (!deleteMode) { deletionTargets.add(item.key)
deletionTargets.add(item.key) deleteMode = true
deleteMode = true
}
}, },
onClick = { onClick = {
if (!deleteMode) { if (!deleteMode) {

View File

@ -7,7 +7,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.NewReleases import androidx.compose.material.icons.outlined.CalendarToday
import androidx.compose.material.icons.outlined.Campaign
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -34,18 +37,28 @@ fun Changelog(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.NewReleases, imageVector = Icons.Outlined.Campaign,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
) )
Text( Text(
"${version.removePrefix("v")} ($publishDate)", version.removePrefix("v"),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
} }
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
) {
Tag(
Icons.Outlined.CalendarToday,
publishDate
)
}
} }
Markdown( Markdown(
markdown, markdown,

View File

@ -12,34 +12,34 @@ import kotlinx.coroutines.flow.map
data class BundleInfo( data class BundleInfo(
val name: String, val name: String,
val uid: Int, val uid: Int,
val compatible: List<PatchInfo>, val supported: List<PatchInfo>,
val incompatible: List<PatchInfo>, val unsupported: List<PatchInfo>,
val universal: List<PatchInfo> val universal: List<PatchInfo>
) { ) {
val all = sequence { val all = sequence {
yieldAll(compatible) yieldAll(supported)
yieldAll(incompatible) yieldAll(unsupported)
yieldAll(universal) yieldAll(universal)
} }
val patchCount get() = compatible.size + incompatible.size + universal.size val patchCount get() = supported.size + unsupported.size + universal.size
fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) { fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) {
all all
} else { } else {
sequence { sequence {
yieldAll(compatible) yieldAll(supported)
yieldAll(universal) yieldAll(universal)
} }
} }
companion object Extensions { companion object Extensions {
inline fun Iterable<BundleInfo>.toPatchSelection( inline fun Iterable<BundleInfo>.toPatchSelection(
allowIncompatible: Boolean, allowUnsupported: Boolean,
condition: (Int, PatchInfo) -> Boolean condition: (Int, PatchInfo) -> Boolean
): PatchSelection = this.associate { bundle -> ): PatchSelection = this.associate { bundle ->
val patches = val patches =
bundle.patchSequence(allowIncompatible) bundle.patchSequence(allowUnsupported)
.mapNotNullTo(mutableSetOf()) { patch -> .mapNotNullTo(mutableSetOf()) { patch ->
patch.name.takeIf { patch.name.takeIf {
condition( condition(
@ -60,8 +60,8 @@ data class BundleInfo(
source.state.map { state -> source.state.map { state ->
val bundle = state.patchBundleOrNull() ?: return@map null val bundle = state.patchBundleOrNull() ?: return@map null
val compatible = mutableListOf<PatchInfo>() val supported = mutableListOf<PatchInfo>()
val incompatible = mutableListOf<PatchInfo>() val unsupported = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>() val universal = mutableListOf<PatchInfo>()
bundle.patches.filter { it.compatibleWith(packageName) }.forEach { bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
@ -70,15 +70,15 @@ data class BundleInfo(
it.supports( it.supports(
packageName, packageName,
version version
) -> compatible ) -> supported
else -> incompatible else -> unsupported
} }
targetList.add(it) targetList.add(it)
} }
BundleInfo(source.getName(), source.uid, compatible, incompatible, universal) BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
} }
} }

View File

@ -70,9 +70,6 @@ object Settings {
@Serializable @Serializable
data object Advanced : Destination data object Advanced : Destination
@Serializable
data object Updates : Destination
@Serializable @Serializable
data object Downloads : Destination data object Downloads : Destination

View File

@ -6,7 +6,6 @@ import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -265,6 +264,9 @@ fun DashboardScreen(
} }
} }
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(
false
)
Notifications( Notifications(
if (!Aapt.supportsDevice()) { if (!Aapt.supportsDevice()) {
{ {
@ -276,23 +278,16 @@ fun DashboardScreen(
) )
} }
} else null, } else null,
if (vm.showBatteryOptimizationsWarning) { if (showBatteryOptimizationsWarning) {
{ {
val batteryOptimizationsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
vm.updateBatteryOptimizationsWarning()
}
NotificationCard( NotificationCard(
isWarning = true, isWarning = true,
icon = Icons.Default.BatteryAlert, icon = Icons.Default.BatteryAlert,
text = stringResource(R.string.battery_optimization_notification), text = stringResource(R.string.battery_optimization_notification),
onClick = { onClick = {
batteryOptimizationsLauncher.launch( androidContext.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
Intent( data = Uri.parse("package:${androidContext.packageName}")
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, })
Uri.fromParts("package", androidContext.packageName, null)
)
)
} }
) )
} }

View File

@ -7,12 +7,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -20,14 +15,7 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.PostAdd import androidx.compose.material.icons.outlined.PostAdd
import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.AlertDialog import androidx.compose.material3.*
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
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -144,7 +132,7 @@ fun PatcherScreen(
BottomAppBar( BottomAppBar(
actions = { actions = {
IconButton( IconButton(
onClick = { exportApkLauncher.launch("${vm.packageName}_${vm.version}_revanced_patched.apk") }, onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
enabled = patcherSucceeded == true enabled = patcherSucceeded == true
) { ) {
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))

View File

@ -76,8 +76,8 @@ import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionBut
import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.isScrollingUp
@ -147,9 +147,9 @@ fun PatchesSelectorScreen(
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
CheckedFilterChip( CheckedFilterChip(
selected = vm.filter and SHOW_INCOMPATIBLE == 0, selected = vm.filter and SHOW_UNSUPPORTED == 0,
onClick = { vm.toggleFlag(SHOW_INCOMPATIBLE) }, onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
label = { Text(stringResource(R.string.this_version)) } label = { Text(stringResource(R.string.supported)) }
) )
CheckedFilterChip( CheckedFilterChip(
@ -163,18 +163,18 @@ fun PatchesSelectorScreen(
} }
if (vm.compatibleVersions.isNotEmpty()) if (vm.compatibleVersions.isNotEmpty())
IncompatiblePatchDialog( UnsupportedPatchDialog(
appVersion = vm.appVersion ?: stringResource(R.string.any_version), appVersion = vm.appVersion ?: stringResource(R.string.any_version),
compatibleVersions = vm.compatibleVersions, supportedVersions = vm.compatibleVersions,
onDismissRequest = vm::dismissDialogs onDismissRequest = vm::dismissDialogs
) )
var showIncompatiblePatchesDialog by rememberSaveable { var showUnsupportedPatchesDialog by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
if (showIncompatiblePatchesDialog) if (showUnsupportedPatchesDialog)
IncompatiblePatchesDialog( UnsupportedPatchesDialog(
appVersion = vm.appVersion ?: stringResource(R.string.any_version), appVersion = vm.appVersion ?: stringResource(R.string.any_version),
onDismissRequest = { showIncompatiblePatchesDialog = false } onDismissRequest = { showUnsupportedPatchesDialog = false }
) )
vm.optionsDialog?.let { (bundle, patch) -> vm.optionsDialog?.let { (bundle, patch) ->
@ -204,7 +204,7 @@ fun PatchesSelectorScreen(
uid: Int, uid: Int,
patches: List<PatchInfo>, patches: List<PatchInfo>,
visible: Boolean, visible: Boolean,
compatible: Boolean, supported: Boolean,
header: (@Composable () -> Unit)? = null header: (@Composable () -> Unit)? = null
) { ) {
if (patches.isNotEmpty() && visible) { if (patches.isNotEmpty() && visible) {
@ -224,14 +224,14 @@ fun PatchesSelectorScreen(
onOptionsDialog = { onOptionsDialog = {
vm.optionsDialog = uid to patch vm.optionsDialog = uid to patch
}, },
selected = compatible && vm.isSelected( selected = supported && vm.isSelected(
uid, uid,
patch patch
), ),
onToggle = { onToggle = {
when { when {
// Open incompatible dialog if the patch is not supported // Open unsupported dialog if the patch is not supported
!compatible -> vm.openIncompatibleDialog(patch) !supported -> vm.openUnsupportedDialog(patch)
// Show selection warning if enabled // Show selection warning if enabled
vm.selectionWarningEnabled -> showSelectionWarning = true vm.selectionWarningEnabled -> showSelectionWarning = true
@ -245,7 +245,7 @@ fun PatchesSelectorScreen(
else -> vm.togglePatch(uid, patch) else -> vm.togglePatch(uid, patch)
} }
}, },
compatible = compatible supported = supported
) )
} }
} }
@ -321,15 +321,15 @@ fun PatchesSelectorScreen(
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.compatible.searched(), patches = bundle.supported.searched(),
visible = true, visible = true,
compatible = true supported = true
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal.searched(), patches = bundle.universal.searched(),
visible = vm.filter and SHOW_UNIVERSAL != 0, visible = vm.filter and SHOW_UNIVERSAL != 0,
compatible = true supported = true
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.universal_patches), title = stringResource(R.string.universal_patches),
@ -338,13 +338,13 @@ fun PatchesSelectorScreen(
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.incompatible.searched(), patches = bundle.unsupported.searched(),
visible = vm.filter and SHOW_INCOMPATIBLE != 0, visible = vm.filter and SHOW_UNSUPPORTED != 0,
compatible = vm.allowIncompatiblePatches supported = vm.allowIncompatiblePatches
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.incompatible_patches), title = stringResource(R.string.unsupported_patches),
onHelpClick = { showIncompatiblePatchesDialog = true } onHelpClick = { showUnsupportedPatchesDialog = true }
) )
} }
} }
@ -427,15 +427,15 @@ fun PatchesSelectorScreen(
) { ) {
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.compatible, patches = bundle.supported,
visible = true, visible = true,
compatible = true supported = true
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal, patches = bundle.universal,
visible = vm.filter and SHOW_UNIVERSAL != 0, visible = vm.filter and SHOW_UNIVERSAL != 0,
compatible = true supported = true
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.universal_patches), title = stringResource(R.string.universal_patches),
@ -443,13 +443,13 @@ fun PatchesSelectorScreen(
} }
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.incompatible, patches = bundle.unsupported,
visible = vm.filter and SHOW_INCOMPATIBLE != 0, visible = vm.filter and SHOW_UNSUPPORTED != 0,
compatible = vm.allowIncompatiblePatches supported = vm.allowIncompatiblePatches
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.incompatible_patches), title = stringResource(R.string.unsupported_patches),
onHelpClick = { showIncompatiblePatchesDialog = true } onHelpClick = { showUnsupportedPatchesDialog = true }
) )
} }
} }
@ -506,24 +506,24 @@ private fun PatchItem(
onOptionsDialog: () -> Unit, onOptionsDialog: () -> Unit,
selected: Boolean, selected: Boolean,
onToggle: () -> Unit, onToggle: () -> Unit,
compatible: Boolean = true supported: Boolean = true
) = ListItem( ) = ListItem(
modifier = Modifier modifier = Modifier
.let { if (!compatible) it.alpha(0.5f) else it } .let { if (!supported) it.alpha(0.5f) else it }
.clickable(onClick = onToggle) .clickable(onClick = onToggle)
.fillMaxSize(), .fillMaxSize(),
leadingContent = { leadingContent = {
HapticCheckbox( HapticCheckbox(
checked = selected, checked = selected,
onCheckedChange = { onToggle() }, onCheckedChange = { onToggle() },
enabled = compatible enabled = supported
) )
}, },
headlineContent = { Text(patch.name) }, headlineContent = { Text(patch.name) },
supportingContent = patch.description?.let { { Text(it) } }, supportingContent = patch.description?.let { { Text(it) } },
trailingContent = { trailingContent = {
if (patch.options?.isNotEmpty() == true) { if (patch.options?.isNotEmpty() == true) {
IconButton(onClick = onOptionsDialog, enabled = compatible) { IconButton(onClick = onOptionsDialog, enabled = supported) {
Icon(Icons.Outlined.Settings, null) Icon(Icons.Outlined.Settings, null)
} }
} }
@ -559,7 +559,7 @@ fun ListHeader(
} }
@Composable @Composable
private fun IncompatiblePatchesDialog( private fun UnsupportedPatchesDialog(
appVersion: String, appVersion: String,
onDismissRequest: () -> Unit onDismissRequest: () -> Unit
) = AlertDialog( ) = AlertDialog(
@ -572,11 +572,11 @@ private fun IncompatiblePatchesDialog(
Text(stringResource(R.string.ok)) Text(stringResource(R.string.ok))
} }
}, },
title = { Text(stringResource(R.string.incompatible_patches)) }, title = { Text(stringResource(R.string.unsupported_patches)) },
text = { text = {
Text( Text(
stringResource( stringResource(
R.string.incompatible_patches_dialog, R.string.unsupported_patches_dialog,
appVersion appVersion
) )
) )
@ -584,9 +584,9 @@ private fun IncompatiblePatchesDialog(
) )
@Composable @Composable
private fun IncompatiblePatchDialog( private fun UnsupportedPatchDialog(
appVersion: String, appVersion: String,
compatibleVersions: List<String>, supportedVersions: List<String>,
onDismissRequest: () -> Unit onDismissRequest: () -> Unit
) = AlertDialog( ) = AlertDialog(
icon = { icon = {
@ -598,13 +598,13 @@ private fun IncompatiblePatchDialog(
Text(stringResource(R.string.ok)) Text(stringResource(R.string.ok))
} }
}, },
title = { Text(stringResource(R.string.incompatible_patch)) }, title = { Text(stringResource(R.string.unsupported_patch)) },
text = { text = {
Text( Text(
stringResource( stringResource(
R.string.app_version_not_compatible, R.string.app_not_supported,
appVersion, appVersion,
compatibleVersions.joinToString(", ") supportedVersions.joinToString(", ")
) )
) )
} }

View File

@ -4,8 +4,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
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.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -14,7 +12,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.filled.AutoFixHigh import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
@ -35,7 +32,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
@ -44,7 +40,6 @@ import app.revanced.manager.ui.component.AppInfo
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.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
@ -55,7 +50,6 @@ import app.revanced.manager.util.enabled
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -67,9 +61,6 @@ fun SelectedAppInfoScreen(
vm: SelectedAppInfoViewModel vm: SelectedAppInfoViewModel
) { ) {
val context = LocalContext.current val context = LocalContext.current
val networkInfo = koinInject<NetworkInfo>()
val networkConnected = remember { networkInfo.isConnected() }
val networkMetered = remember { !networkInfo.isUnmetered() }
val packageName = vm.selectedApp.packageName val packageName = vm.selectedApp.packageName
val version = vm.selectedApp.version val version = vm.selectedApp.version
@ -217,35 +208,6 @@ fun SelectedAppInfoScreen(
modifier = Modifier.padding(horizontal = 24.dp) modifier = Modifier.padding(horizontal = 24.dp)
) )
} }
Column(
modifier = Modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
val needsInternet =
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
when {
!needsInternet -> {}
!networkConnected -> {
NotificationCard(
isWarning = true,
icon = Icons.Outlined.WarningAmber,
text = stringResource(R.string.network_unavailable_warning),
onDismiss = null
)
}
networkMetered -> {
NotificationCard(
isWarning = true,
icon = Icons.Outlined.WarningAmber,
text = stringResource(R.string.network_metered_warning),
onDismiss = null
)
}
}
}
} }
} }
} }

View File

@ -4,8 +4,14 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.* import androidx.compose.material.icons.outlined.Download
import androidx.compose.material3.* import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SwapVert
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -22,18 +28,13 @@ private val settingsSections = listOf(
Icons.Outlined.Settings Icons.Outlined.Settings
) to Settings.General, ) to Settings.General,
Triple( Triple(
R.string.updates, R.string.extensions,
R.string.updates_description, R.string.extensions_description,
Icons.Outlined.Update
) to Settings.Updates,
Triple(
R.string.downloads,
R.string.downloads_description,
Icons.Outlined.Download Icons.Outlined.Download
) to Settings.Downloads, ) to Settings.Downloads,
Triple( Triple(
R.string.import_export, R.string.backup_restore,
R.string.import_export_description, R.string.backup_restore_description,
Icons.Outlined.SwapVert Icons.Outlined.SwapVert
) to Settings.ImportExport, ) to Settings.ImportExport,
Triple( Triple(

View File

@ -5,17 +5,19 @@ import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.InstallMobile
import androidx.compose.material.icons.outlined.Update import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -26,15 +28,16 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.BuildConfig
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.network.dto.ReVancedAsset import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.settings.Changelog import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel import app.revanced.manager.ui.viewmodel.UpdateViewModel
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
@ -57,81 +60,37 @@ fun UpdateScreen(
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = { title = stringResource(R.string.update),
Column {
Text(stringResource(vm.state.title))
if (vm.state == State.DOWNLOADING) {
Text(
text = "${vm.downloadedSize.div(1000000)} MB / ${
vm.totalSize.div(1000000)
} MB (${vm.downloadProgress.times(100).toInt()}%)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline
)
}
}
},
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
}, },
floatingActionButton = {
val buttonConfig = when (vm.state) {
State.CAN_DOWNLOAD -> Triple(
{ vm.downloadUpdate() },
R.string.download,
Icons.Outlined.InstallMobile
)
State.DOWNLOADING -> Triple(onBackClick, R.string.cancel, Icons.Outlined.Cancel)
State.CAN_INSTALL -> Triple(
{ vm.installUpdate() },
R.string.install_app,
Icons.Outlined.InstallMobile
)
else -> null
}
buttonConfig?.let { (onClick, textRes, icon) ->
HapticExtendedFloatingActionButton(
onClick = onClick::invoke,
icon = { Icon(icon, null) },
text = { Text(stringResource(textRes)) }
)
}
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
MeteredDownloadConfirmationDialog(
onDismiss = { vm.showInternetCheckDialog = false },
onDownloadAnyways = { vm.downloadUpdate(true) }
)
}
Column( Column(
modifier = Modifier modifier = Modifier
.padding(paddingValues), .fillMaxSize()
.padding(paddingValues)
.padding(vertical = 16.dp, horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
if (vm.state == State.DOWNLOADING) Header(
LinearProgressIndicator( vm.state,
progress = { vm.downloadProgress }, vm.releaseInfo,
modifier = Modifier.fillMaxWidth(), DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
) )
vm.releaseInfo?.let { changelog ->
AnimatedVisibility(visible = vm.showInternetCheckDialog) { HorizontalDivider()
MeteredDownloadConfirmationDialog( Changelog(changelog)
onDismiss = { vm.showInternetCheckDialog = false }, } ?: Spacer(modifier = Modifier.weight(1f))
onDownloadAnyways = { vm.downloadUpdate(true) } Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick)
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
vm.releaseInfo?.let { changelog ->
Changelog(changelog)
}
}
} }
} }
} }
@ -164,6 +123,58 @@ private fun MeteredDownloadConfirmationDialog(
) )
} }
@Composable
private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
text = stringResource(state.title),
style = MaterialTheme.typography.headlineMedium
)
if (state == State.CAN_DOWNLOAD) {
Column {
Text(
text = stringResource(
id = R.string.current_version,
BuildConfig.VERSION_NAME
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
releaseInfo?.version?.let {
Text(
text = stringResource(
R.string.new_version,
it.replace("v", "")
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else if (state == State.DOWNLOADING) {
LinearProgressIndicator(
progress = { downloadData.downloadProgress },
modifier = Modifier.fillMaxWidth(),
)
Text(
text =
"${downloadData.downloadedSize.div(1000000)} MB / ${
downloadData.totalSize.div(
1000000
)
} MB (${
downloadData.downloadProgress.times(
100
).toInt()
}%)",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
}
@Composable @Composable
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) { private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -194,4 +205,40 @@ private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current) publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
) )
} }
} }
@Composable
private fun Buttons(
state: State,
onDownloadClick: () -> Unit,
onInstallClick: () -> Unit,
onBackClick: () -> Unit
) {
Row(modifier = Modifier.fillMaxWidth()) {
if (state.showCancel) {
TextButton(
onClick = onBackClick,
) {
Text(text = stringResource(R.string.cancel))
}
}
Spacer(modifier = Modifier.weight(1f))
if (state == State.CAN_DOWNLOAD) {
Button(onClick = onDownloadClick) {
Text(text = stringResource(R.string.update))
}
} else if (state == State.CAN_INSTALL) {
Button(
onClick = onInstallClick
) {
Text(text = stringResource(R.string.install_app))
}
}
}
}
data class DownloadData(
val downloadProgress: Float,
val downloadedSize: Long,
val totalSize: Long
)

View File

@ -44,7 +44,6 @@ import app.revanced.manager.ui.model.navigation.Settings
import app.revanced.manager.ui.viewmodel.AboutViewModel import app.revanced.manager.ui.viewmodel.AboutViewModel
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
import app.revanced.manager.util.openUrl import app.revanced.manager.util.openUrl
import app.revanced.manager.util.toast
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@ -52,6 +51,7 @@ import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun AboutSettingsScreen( fun AboutSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onChangelogClick: () -> Unit,
navigate: (Settings.Destination) -> Unit, navigate: (Settings.Destination) -> Unit,
viewModel: AboutViewModel = koinViewModel() viewModel: AboutViewModel = koinViewModel()
) { ) {
@ -109,6 +109,11 @@ fun AboutSettingsScreen(
} }
val listItems = listOfNotNull( val listItems = listOfNotNull(
Triple(
stringResource(R.string.changelog),
stringResource(R.string.changelog_description),
third = { onChangelogClick }
),
Triple(stringResource(R.string.submit_feedback), Triple(stringResource(R.string.submit_feedback),
stringResource(R.string.submit_feedback_description), stringResource(R.string.submit_feedback_description),
third = { third = {
@ -117,14 +122,7 @@ fun AboutSettingsScreen(
Triple( Triple(
stringResource(R.string.contributors), stringResource(R.string.contributors),
stringResource(R.string.contributors_description), stringResource(R.string.contributors_description),
third = nav@{ third = { navigate(Settings.Contributors) }
if (!viewModel.isConnected) {
context.toast(context.getString(R.string.no_network_toast))
return@nav
}
navigate(Settings.Contributors)
}
), ),
Triple( Triple(
stringResource(R.string.developer_options), stringResource(R.string.developer_options),

View File

@ -17,6 +17,7 @@ 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.Icons
import androidx.compose.material.icons.outlined.Api import androidx.compose.material.icons.outlined.Api
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -100,17 +101,10 @@ fun AdvancedSettingsScreen(
defaultUrl = vm.prefs.api.default, defaultUrl = vm.prefs.api.default,
onSubmit = { onSubmit = {
showApiUrlDialog = false showApiUrlDialog = false
it?.let(vm::setApiUrl) it?.let(vm::setApiSource)
} }
) )
} }
SettingsListItem(
headlineContent = stringResource(R.string.api_url),
supportingContent = stringResource(R.string.api_url_description),
modifier = Modifier.clickable {
showApiUrlDialog = true
}
)
GroupHeader(stringResource(R.string.patcher)) GroupHeader(stringResource(R.string.patcher))
BooleanItem( BooleanItem(
@ -126,13 +120,28 @@ fun AdvancedSettingsScreen(
description = R.string.process_runtime_memory_limit_description, description = R.string.process_runtime_memory_limit_description,
) )
GroupHeader(stringResource(R.string.safeguards)) GroupHeader(stringResource(R.string.manager))
SettingsListItem(
headlineContent = stringResource(R.string.api_url),
supportingContent = apiUrl,
modifier = Modifier.clickable {
showApiUrlDialog = true
},
trailingContent = {
IconButton(onClick = { showApiUrlDialog = true }) {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.edit)
)
}
}
)
SafeguardBooleanItem( SafeguardBooleanItem(
preference = vm.prefs.disablePatchVersionCompatCheck, preference = vm.prefs.disablePatchVersionCompatCheck,
coroutineScope = vm.viewModelScope, coroutineScope = vm.viewModelScope,
headline = R.string.patch_compat_check, headline = R.string.allow_compatibility_mixing,
description = R.string.patch_compat_check_description, description = R.string.allow_compatibility_mixing_description,
confirmationText = R.string.patch_compat_check_confirmation confirmationText = R.string.allow_compatibility_mixing_confirmation
) )
SafeguardBooleanItem( SafeguardBooleanItem(
preference = vm.prefs.disableUniversalPatchWarning, preference = vm.prefs.disableUniversalPatchWarning,
@ -141,13 +150,6 @@ fun AdvancedSettingsScreen(
description = R.string.universal_patches_safeguard_description, description = R.string.universal_patches_safeguard_description,
confirmationText = R.string.universal_patches_safeguard_confirmation confirmationText = R.string.universal_patches_safeguard_confirmation
) )
SafeguardBooleanItem(
preference = vm.prefs.suggestedVersionSafeguard,
coroutineScope = vm.viewModelScope,
headline = R.string.suggested_version_safeguard,
description = R.string.suggested_version_safeguard_description,
confirmationText = R.string.suggested_version_safeguard_confirmation
)
SafeguardBooleanItem( SafeguardBooleanItem(
preference = vm.prefs.disableSelectionWarning, preference = vm.prefs.disableSelectionWarning,
coroutineScope = vm.viewModelScope, coroutineScope = vm.viewModelScope,
@ -156,6 +158,21 @@ fun AdvancedSettingsScreen(
confirmationText = R.string.patch_selection_safeguard_confirmation confirmationText = R.string.patch_selection_safeguard_confirmation
) )
GroupHeader(stringResource(R.string.update))
BooleanItem(
preference = vm.prefs.showManagerUpdateDialogOnLaunch,
headline = R.string.show_manager_update_dialog_on_launch,
description = R.string.check_for_update_auto_description
)
GroupHeader(stringResource(R.string.experimental_features))
BooleanItem(
preference = vm.prefs.useProcessRuntime,
coroutineScope = vm.viewModelScope,
headline = R.string.process_runtime,
description = R.string.process_runtime_description,
)
GroupHeader(stringResource(R.string.debugging)) GroupHeader(stringResource(R.string.debugging))
val exportDebugLogsLauncher = val exportDebugLogsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {

View File

@ -54,53 +54,53 @@ import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ImportExportSettingsScreen( fun BackupRestoreSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: ImportExportViewModel = koinViewModel() viewModel: ImportExportViewModel = koinViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
val importKeystoreLauncher = val importKeystoreLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
it?.let { uri -> vm.startKeystoreImport(uri) } it?.let { uri -> viewModel.startKeystoreImport(uri) }
} }
val exportKeystoreLauncher = val exportKeystoreLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) {
it?.let(vm::exportKeystore) it?.let(viewModel::exportKeystore)
} }
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList()) val patchBundles by viewModel.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet()) val packagesWithOptions by viewModel.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
vm.selectionAction?.let { action -> viewModel.selectionAction?.let { action ->
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri -> val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
if (uri == null) { if (uri == null) {
vm.clearSelectionAction() viewModel.clearSelectionAction()
} else { } else {
vm.executeSelectionAction(uri) viewModel.executeSelectionAction(uri)
} }
} }
if (vm.selectedBundle == null) { if (viewModel.selectedBundle == null) {
BundleSelector(patchBundles) { BundleSelector(patchBundles) {
if (it == null) { if (it == null) {
vm.clearSelectionAction() viewModel.clearSelectionAction()
} else { } else {
vm.selectBundle(it) viewModel.selectBundle(it)
launcher.launch(action.activityArg) launcher.launch(action.activityArg)
} }
} }
} }
} }
if (vm.showCredentialsDialog) { if (viewModel.showCredentialsDialog) {
KeystoreCredentialsDialog( KeystoreCredentialsDialog(
onDismissRequest = vm::cancelKeystoreImport, onDismissRequest = viewModel::cancelKeystoreImport,
onSubmit = { cn, pass -> onSubmit = { cn, pass ->
vm.viewModelScope.launch { viewModel.viewModelScope.launch {
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") { uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
val result = vm.tryKeystoreImport(cn, pass) val result = viewModel.tryKeystoreImport(cn, pass)
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials)) if (!result) context.toast(context.getString(R.string.restore_keystore_wrong_credentials))
} }
} }
} }
@ -112,7 +112,7 @@ fun ImportExportSettingsScreen(
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.import_export), title = stringResource(R.string.backup_restore),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
@ -133,7 +133,7 @@ fun ImportExportSettingsScreen(
if (showPackageSelector) { if (showPackageSelector) {
PackageSelector(packages = packagesWithOptions) { selected -> PackageSelector(packages = packagesWithOptions) { selected ->
selected?.let(vm::resetOptionsForPackage) selected?.let(viewModel::resetOptionsForPackage)
showPackageSelector = false showPackageSelector = false
} }
@ -141,69 +141,95 @@ fun ImportExportSettingsScreen(
if (showBundleSelector) { if (showBundleSelector) {
BundleSelector(bundles = patchBundles) { bundle -> BundleSelector(bundles = patchBundles) { bundle ->
bundle?.let(vm::clearOptionsForBundle) bundle?.let(viewModel::clearOptionsForBundle)
showBundleSelector = false showBundleSelector = false
} }
} }
GroupHeader(stringResource(R.string.import_)) GroupHeader(stringResource(R.string.keystore))
GroupItem( GroupItem(
onClick = { onClick = {
importKeystoreLauncher.launch("*/*") if (!viewModel.canExport()) {
}, context.toast(context.getString(R.string.backup_keystore_unavailable))
headline = R.string.import_keystore,
description = R.string.import_keystore_description
)
GroupItem(
onClick = vm::importSelection,
headline = R.string.import_patch_selection,
description = R.string.import_patch_selection_description
)
GroupHeader(stringResource(R.string.export))
GroupItem(
onClick = {
if (!vm.canExport()) {
context.toast(context.getString(R.string.export_keystore_unavailable))
return@GroupItem return@GroupItem
} }
exportKeystoreLauncher.launch("Manager.keystore") exportKeystoreLauncher.launch("Manager.keystore")
}, },
headline = R.string.export_keystore, headline = R.string.backup,
description = R.string.export_keystore_description description = R.string.backup_keystore_description
) )
GroupItem( GroupItem(
onClick = vm::exportSelection, onClick = {
headline = R.string.export_patch_selection, importKeystoreLauncher.launch("*/*")
description = R.string.export_patch_selection_description },
headline = R.string.restore,
description = R.string.restore_keystore_description
) )
GroupHeader(stringResource(R.string.reset))
GroupItem( GroupItem(
onClick = vm::regenerateKeystore, onClick = viewModel::regenerateKeystore,
headline = R.string.regenerate_keystore, headline = {
Text(
stringResource(R.string.regenerate_keystore),
color = MaterialTheme.colorScheme.error
)
},
description = R.string.regenerate_keystore_description description = R.string.regenerate_keystore_description
) )
GroupHeader(stringResource(R.string.patch_selection))
GroupItem( GroupItem(
onClick = vm::resetSelection, // TODO: allow resetting selection for specific bundle or package name. onClick = viewModel::exportSelection,
headline = R.string.reset_patch_selection, headline = R.string.backup,
description = R.string.reset_patch_selection_description description = R.string.restore_patch_selection_description
) )
GroupItem( GroupItem(
onClick = vm::resetOptions, // TODO: patch options import/export. onClick = viewModel::importSelection,
headline = R.string.patch_options_reset_all, headline = R.string.restore,
description = R.string.backup_patch_selection_description
)
GroupItem(
onClick = viewModel::resetSelection, // TODO: allow resetting selection for specific bundle or package name.
headline = {
Text(
stringResource(R.string.reset),
color = MaterialTheme.colorScheme.error
)
},
description = R.string.reset_patch_selection_description
)
GroupHeader(stringResource(R.string.patch_options))
// TODO: patch options import/export.
GroupItem(
onClick = viewModel::resetOptions,
headline = {
Text(
stringResource(R.string.reset),
color = MaterialTheme.colorScheme.error
)
},
description = R.string.patch_options_reset_all_description, description = R.string.patch_options_reset_all_description,
) )
GroupItem( GroupItem(
onClick = { showPackageSelector = true }, onClick = { showPackageSelector = true },
headline = R.string.patch_options_reset_package, headline = {
Text(
stringResource(R.string.patch_options_reset_package),
color = MaterialTheme.colorScheme.error
)
},
description = R.string.patch_options_reset_package_description description = R.string.patch_options_reset_package_description
) )
if (patchBundles.size > 1) { if (patchBundles.size > 1) {
GroupItem( GroupItem(
onClick = { showBundleSelector = true }, onClick = { showBundleSelector = true },
headline = R.string.patch_options_reset_bundle, headline = {
Text(
stringResource(R.string.patch_options_reset_bundle),
color = MaterialTheme.colorScheme.error
)
},
description = R.string.patch_options_reset_bundle_description, description = R.string.patch_options_reset_bundle_description,
) )
} }
@ -282,6 +308,19 @@ private fun GroupItem(
) )
} }
@Composable
private fun GroupItem(
onClick: () -> Unit,
headline: @Composable () -> Unit,
@StringRes description: Int? = null
) {
SettingsListItem(
modifier = Modifier.clickable { onClick() },
headlineContent = headline,
supportingContent = description?.let { stringResource(it) }
)
}
@Composable @Composable
fun KeystoreCredentialsDialog( fun KeystoreCredentialsDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
@ -298,7 +337,7 @@ fun KeystoreCredentialsDialog(
onSubmit(cn, pass) onSubmit(cn, pass)
} }
) { ) {
Text(stringResource(R.string.import_keystore_dialog_button)) Text(stringResource(R.string.restore_keystore_dialog_button))
} }
}, },
dismissButton = { dismissButton = {
@ -311,7 +350,7 @@ fun KeystoreCredentialsDialog(
}, },
title = { title = {
Text( Text(
text = stringResource(R.string.import_keystore_dialog_title), text = stringResource(R.string.restore_keystore_dialog_title),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@ -322,19 +361,19 @@ fun KeystoreCredentialsDialog(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text( Text(
text = stringResource(R.string.import_keystore_dialog_description), text = stringResource(R.string.restore_keystore_dialog_description),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
OutlinedTextField( OutlinedTextField(
value = cn, value = cn,
onValueChange = { cn = it }, onValueChange = { cn = it },
label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) } label = { Text(stringResource(R.string.restore_keystore_dialog_alias_field)) }
) )
PasswordField( PasswordField(
value = pass, value = pass,
onValueChange = { pass = it }, onValueChange = { pass = it },
label = { Text(stringResource(R.string.import_keystore_dialog_password_field)) } label = { Text(stringResource(R.string.restore_keystore_dialog_password_field)) }
) )
} }
} }

View File

@ -97,14 +97,7 @@ fun ContributorScreen(
) )
} }
} }
} ?: item { } ?: item { LoadingIndicator() }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
LoadingIndicator()
}
}
} }
} }
} }

View File

@ -44,7 +44,13 @@ fun DeveloperOptionsScreen(
) )
SettingsListItem( SettingsListItem(
headlineContent = stringResource(R.string.patch_bundles_reset), headlineContent = stringResource(R.string.patch_bundles_reset),
modifier = Modifier.clickable(onClick = vm::redownloadBundles) modifier = Modifier.clickable(onClick = vm::resetBundles)
)
GroupHeader(stringResource(R.string.testing))
SettingsListItem(
headlineContent = stringResource(R.string.disable_safeguard),
modifier = Modifier.clickable(onClick = vm::disableSafeguard)
) )
} }
} }

View File

@ -63,7 +63,7 @@ fun DownloadsSettingsScreen(
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.downloads), title = stringResource(R.string.extensions),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
onBackClick = onBackClick, onBackClick = onBackClick,
actions = { actions = {

View File

@ -8,6 +8,7 @@ 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
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -42,6 +43,7 @@ import org.koin.compose.koinInject
@Composable @Composable
fun GeneralSettingsScreen( fun GeneralSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onUpdateClick: () -> Unit,
viewModel: GeneralSettingsViewModel = koinViewModel() viewModel: GeneralSettingsViewModel = koinViewModel()
) { ) {
val prefs = viewModel.prefs val prefs = viewModel.prefs
@ -76,10 +78,10 @@ fun GeneralSettingsScreen(
val theme by prefs.theme.getAsState() val theme by prefs.theme.getAsState()
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { showThemePicker = true }, modifier = Modifier.clickable { showThemePicker = true },
headlineContent = stringResource(R.string.theme), headlineContent = stringResource(R.string.theme_mode),
supportingContent = stringResource(R.string.theme_description), supportingContent = stringResource(R.string.theme_mode_description),
trailingContent = { trailingContent = {
FilledTonalButton( Button(
onClick = { onClick = {
showThemePicker = true showThemePicker = true
} }
@ -92,11 +94,24 @@ fun GeneralSettingsScreen(
BooleanItem( BooleanItem(
preference = prefs.dynamicColor, preference = prefs.dynamicColor,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
headline = R.string.dynamic_color, headline = R.string.personalized_color,
description = R.string.dynamic_color_description description = R.string.personalized_color_description
) )
} }
} }
GroupHeader(stringResource(R.string.update))
BooleanItem(
preference = prefs.managerAutoUpdates,
headline = R.string.check_for_update,
description = R.string.check_for_update_auto_description
)
FilledTonalButton (
modifier = Modifier.padding(top = paddingValues.calculateTopPadding()),
onClick = onUpdateClick
) {
Text(stringResource(R.string.check_for_update))
}
} }
} }
@ -110,7 +125,7 @@ private fun ThemePicker(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.theme)) }, title = { Text(stringResource(R.string.theme_mode)) },
text = { text = {
Column { Column {
Theme.entries.forEach { Theme.entries.forEach {

View File

@ -1,94 +0,0 @@
package app.revanced.manager.ui.screen.settings.update
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
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.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
import app.revanced.manager.util.toast
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UpdatesSettingsScreen(
onBackClick: () -> Unit,
onChangelogClick: () -> Unit,
onUpdateClick: () -> Unit,
vm: UpdatesSettingsViewModel = koinViewModel(),
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.updates),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
ColumnWithScrollbar(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
SettingsListItem(
modifier = Modifier.clickable {
coroutineScope.launch {
if (!vm.isConnected) {
context.toast(context.getString(R.string.no_network_toast))
return@launch
}
if (vm.checkForUpdates()) onUpdateClick()
}
},
headlineContent = stringResource(R.string.manual_update_check),
supportingContent = stringResource(R.string.manual_update_check_description)
)
SettingsListItem(
modifier = Modifier.clickable {
if (!vm.isConnected) {
context.toast(context.getString(R.string.no_network_toast))
return@clickable
}
onChangelogClick()
},
headlineContent = stringResource(R.string.changelog),
supportingContent = stringResource(
R.string.changelog_description
)
)
BooleanItem(
preference = vm.managerAutoUpdates,
headline = R.string.update_checking_manager,
description = R.string.update_checking_manager_description
)
BooleanItem(
preference = vm.showManagerUpdateDialogOnLaunch,
headline = R.string.show_manager_update_dialog_on_launch,
description = R.string.update_checking_manager_description
)
}
}
}

View File

@ -7,7 +7,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedDonationLink import app.revanced.manager.network.dto.ReVancedDonationLink
import app.revanced.manager.network.dto.ReVancedSocial import app.revanced.manager.network.dto.ReVancedSocial
@ -24,24 +23,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class AboutViewModel( class AboutViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
private val reVancedAPI: ReVancedAPI,
private val network: NetworkInfo,
) : ViewModel() {
var socials by mutableStateOf(emptyList<ReVancedSocial>()) var socials by mutableStateOf(emptyList<ReVancedSocial>())
private set private set
var contact by mutableStateOf<String?>(null) var contact by mutableStateOf<String?>(null)
private set private set
var donate by mutableStateOf<String?>(null) var donate by mutableStateOf<String?>(null)
private set private set
val isConnected: Boolean
get() = network.isConnected()
init { init {
viewModelScope.launch { viewModelScope.launch {
if (!isConnected) {
return@launch
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
reVancedAPI.getInfo("https://api.revanced.app").getOrNull() reVancedAPI.getInfo("https://api.revanced.app").getOrNull()
}?.let { }?.let {

View File

@ -34,7 +34,7 @@ class AdvancedSettingsViewModel(
return "revanced-manager_logcat_$time" return "revanced-manager_logcat_$time"
} }
fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) { fun setApiSource(value: String) = viewModelScope.launch(Dispatchers.Default) {
if (value == prefs.api.get()) return@launch if (value == prefs.api.get()) return@launch
prefs.api.update(value) prefs.api.update(value)

View File

@ -24,7 +24,9 @@ import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
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.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -56,13 +58,19 @@ class DashboardViewModel(
var updatedManagerVersion: String? by mutableStateOf(null) var updatedManagerVersion: String? by mutableStateOf(null)
private set private set
var showBatteryOptimizationsWarning by mutableStateOf(false) val showBatteryOptimizationsWarningFlow = flow {
private set while (true) {
// There is no callback for this, so we have to poll it.
val result = !powerManager.isIgnoringBatteryOptimizations(app.packageName)
emit(result)
if (!result) return@flow
delay(500L)
}
}
init { init {
viewModelScope.launch { viewModelScope.launch {
checkForManagerUpdates() checkForManagerUpdates()
updateBatteryOptimizationsWarning()
} }
} }
@ -82,10 +90,6 @@ class DashboardViewModel(
} }
} }
fun updateBatteryOptimizationsWarning() {
showBatteryOptimizationsWarning = !powerManager.isIgnoringBatteryOptimizations(app.packageName)
}
fun setShowManagerUpdateDialogOnLaunch(value: Boolean) { fun setShowManagerUpdateDialogOnLaunch(value: Boolean) {
viewModelScope.launch { viewModelScope.launch {
prefs.showManagerUpdateDialogOnLaunch.update(value) prefs.showManagerUpdateDialogOnLaunch.update(value)

View File

@ -24,4 +24,10 @@ class DeveloperOptionsViewModel(
fun resetBundles() = viewModelScope.launch { fun resetBundles() = viewModelScope.launch {
patchBundleRepository.reset() patchBundleRepository.reset()
} }
fun disableSafeguard() = viewModelScope.launch {
prefs.disablePatchVersionCompatCheck.update(true)
prefs.disableSelectionWarning.update(true)
prefs.disableUniversalPatchWarning.update(true)
}
} }

View File

@ -103,7 +103,7 @@ class ImportExportViewModel(
private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean { private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
path.inputStream().use { stream -> path.inputStream().use { stream ->
if (keystoreManager.import(cn, pass, stream)) { if (keystoreManager.import(cn, pass, stream)) {
app.toast(app.getString(R.string.import_keystore_success)) app.toast(app.getString(R.string.restore_keystore_success))
cancelKeystoreImport() cancelKeystoreImport()
return true return true
} }
@ -122,7 +122,7 @@ class ImportExportViewModel(
fun exportKeystore(target: Uri) = viewModelScope.launch { fun exportKeystore(target: Uri) = viewModelScope.launch {
keystoreManager.export(contentResolver.openOutputStream(target)!!) keystoreManager.export(contentResolver.openOutputStream(target)!!)
app.toast(app.getString(R.string.export_keystore_success)) app.toast(app.getString(R.string.backup_keystore_success))
} }
fun regenerateKeystore() = viewModelScope.launch { fun regenerateKeystore() = viewModelScope.launch {
@ -171,7 +171,7 @@ class ImportExportViewModel(
override val activityArg = JSON_MIMETYPE override val activityArg = JSON_MIMETYPE
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe( override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
app, app,
R.string.import_patch_selection_fail, R.string.restore_patch_selection_fail,
"Failed to restore patch selection" "Failed to restore patch selection"
) { ) {
val selection = withContext(Dispatchers.IO) { val selection = withContext(Dispatchers.IO) {
@ -181,7 +181,7 @@ class ImportExportViewModel(
} }
selectionRepository.import(bundleUid, selection) selectionRepository.import(bundleUid, selection)
app.toast(app.getString(R.string.import_patch_selection_success)) app.toast(app.getString(R.string.restore_patch_selection_success))
} }
} }
@ -190,7 +190,7 @@ class ImportExportViewModel(
override val activityArg = "selection.json" override val activityArg = "selection.json"
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe( override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
app, app,
R.string.export_patch_selection_fail, R.string.backup_patch_selection_fail,
"Failed to backup patch selection" "Failed to backup patch selection"
) { ) {
val selection = selectionRepository.export(bundleUid) val selection = selectionRepository.export(bundleUid)
@ -200,7 +200,7 @@ class ImportExportViewModel(
Json.Default.encodeToStream(selection, it) Json.Default.encodeToStream(selection, it)
} }
} }
app.toast(app.getString(R.string.export_patch_selection_success)) app.toast(app.getString(R.string.backup_patch_selection_success))
} }
} }

View File

@ -137,8 +137,8 @@ class MainViewModel(
updateCheck() updateCheck()
} }
} }
settings.patchesChangeEnabled?.let { disableSelectionWarning -> settings.patchesChangeEnabled?.let { allowChangingPatchSelection ->
prefs.disableSelectionWarning.update(disableSelectionWarning) prefs.allowChangingPatchSelection.update(allowChangingPatchSelection)
} }
settings.keystore?.let { keystore -> settings.keystore?.let { keystore ->
val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT) val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)

View File

@ -83,9 +83,7 @@ class PatcherViewModel(
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private var installedApp: InstalledApp? = null private var installedApp: InstalledApp? = null
private val selectedApp = input.selectedApp val packageName = input.selectedApp.packageName
val packageName = selectedApp.packageName
val version = selectedApp.version
var installedPackageName by savedStateHandle.saveable( var installedPackageName by savedStateHandle.saveable(
key = "installedPackageName", key = "installedPackageName",

View File

@ -3,11 +3,11 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -30,20 +30,15 @@ import app.revanced.manager.util.saver.persistentMapSaver
import app.revanced.manager.util.saver.persistentSetSaver import app.revanced.manager.util.saver.persistentSetSaver
import app.revanced.manager.util.saver.snapshotStateMapSaver import app.revanced.manager.util.saver.snapshotStateMapSaver
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import kotlinx.collections.immutable.*
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) : class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
@ -71,7 +66,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
viewModelScope.launch { viewModelScope.launch {
universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get() universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get()
if (prefs.disableSelectionWarning.get()) { if (prefs.allowChangingPatchSelection.get()) {
selectionWarningEnabled = false selectionWarningEnabled = false
return@launch return@launch
} }
@ -213,8 +208,8 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
compatibleVersions.clear() compatibleVersions.clear()
} }
fun openIncompatibleDialog(incompatiblePatch: PatchInfo) { fun openUnsupportedDialog(unsupportedPatch: PatchInfo) {
compatibleVersions.addAll(incompatiblePatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty()) compatibleVersions.addAll(unsupportedPatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty())
} }
fun toggleFlag(flag: Int) { fun toggleFlag(flag: Int) {
@ -222,7 +217,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
} }
companion object { companion object {
const val SHOW_INCOMPATIBLE = 1 // 2^0 const val SHOW_UNSUPPORTED = 1 // 2^0
const val SHOW_UNIVERSAL = 2 // 2^1 const val SHOW_UNIVERSAL = 2 // 2^1
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver( private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(

View File

@ -30,13 +30,14 @@ import app.revanced.manager.domain.repository.PatchOptionsRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderData import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.BundleInfo import app.revanced.manager.ui.model.BundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
@ -151,7 +152,7 @@ class SelectedAppInfoViewModel(
// Try to get the previous selection if customization is enabled. // Try to get the previous selection if customization is enabled.
viewModelScope.launch { viewModelScope.launch {
if (!prefs.disableSelectionWarning.get()) return@launch if (!prefs.allowChangingPatchSelection.get()) return@launch
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
@ -274,25 +275,25 @@ class SelectedAppInfoViewModel(
) )
suspend fun getPatcherParams(): Patcher.ViewModelParams { suspend fun getPatcherParams(): Patcher.ViewModelParams {
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get() val allowUnsupported = prefs.disablePatchVersionCompatCheck.get()
val bundles = bundleInfoFlow.first() val bundles = bundleInfoFlow.first()
return Patcher.ViewModelParams( return Patcher.ViewModelParams(
selectedApp, selectedApp,
getPatches(bundles, allowIncompatible), getPatches(bundles, allowUnsupported),
getOptionsFiltered(bundles) getOptionsFiltered(bundles)
) )
} }
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles) fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
fun getPatches(bundles: List<BundleInfo>, allowIncompatible: Boolean) = fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
selectionState.patches(bundles, allowIncompatible) selectionState.patches(bundles, allowUnsupported)
fun getCustomPatches( fun getCustomPatches(
bundles: List<BundleInfo>, bundles: List<BundleInfo>,
allowIncompatible: Boolean allowUnsupported: Boolean
): PatchSelection? = ): PatchSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible) (selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch { fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
val bundles = bundleInfoFlow.first() val bundles = bundleInfoFlow.first()
@ -342,13 +343,13 @@ class SelectedAppInfoViewModel(
} }
private sealed interface SelectionState : Parcelable { private sealed interface SelectionState : Parcelable {
fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean): PatchSelection fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean): PatchSelection
@Parcelize @Parcelize
data class Customized(val patchSelection: PatchSelection) : SelectionState { data class Customized(val patchSelection: PatchSelection) : SelectionState {
override fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean) = override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
bundles.toPatchSelection( bundles.toPatchSelection(
allowIncompatible allowUnsupported
) { uid, patch -> ) { uid, patch ->
patchSelection[uid]?.contains(patch.name) ?: false patchSelection[uid]?.contains(patch.name) ?: false
} }
@ -356,8 +357,8 @@ private sealed interface SelectionState : Parcelable {
@Parcelize @Parcelize
data object Default : SelectionState { data object Default : SelectionState {
override fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean) = override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include } bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include }
} }
} }

View File

@ -9,7 +9,6 @@ import android.content.pm.PackageInstaller
import androidx.annotation.StringRes 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.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -43,9 +42,9 @@ class UpdateViewModel(
private val networkInfo: NetworkInfo by inject() private val networkInfo: NetworkInfo by inject()
private val fs: Filesystem by inject() private val fs: Filesystem by inject()
var downloadedSize by mutableLongStateOf(0L) var downloadedSize by mutableStateOf(0L)
private set private set
var totalSize by mutableLongStateOf(0L) var totalSize by mutableStateOf(0L)
private set private set
val downloadProgress by derivedStateOf { val downloadProgress by derivedStateOf {
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
@ -90,7 +89,7 @@ class UpdateViewModel(
totalSize = contentLength totalSize = contentLength
} }
} }
installUpdate() state = State.CAN_INSTALL
} }
} }
} }
@ -141,10 +140,10 @@ class UpdateViewModel(
location.delete() location.delete()
} }
enum class State(@StringRes val title: Int) { enum class State(@StringRes val title: Int, val showCancel: Boolean = false) {
CAN_DOWNLOAD(R.string.update_available), CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update), DOWNLOADING(R.string.downloading_manager_update, true),
CAN_INSTALL(R.string.ready_to_install_update), CAN_INSTALL(R.string.ready_to_install_update, true),
INSTALLING(R.string.installing_manager_update), INSTALLING(R.string.installing_manager_update),
FAILED(R.string.install_update_manager_failed), FAILED(R.string.install_update_manager_failed),
SUCCESS(R.string.update_completed) SUCCESS(R.string.update_completed)

View File

@ -3,7 +3,6 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@ -13,14 +12,10 @@ class UpdatesSettingsViewModel(
prefs: PreferencesManager, prefs: PreferencesManager,
private val app: Application, private val app: Application,
private val reVancedAPI: ReVancedAPI, private val reVancedAPI: ReVancedAPI,
private val network: NetworkInfo,
) : ViewModel() { ) : ViewModel() {
val managerAutoUpdates = prefs.managerAutoUpdates val managerAutoUpdates = prefs.managerAutoUpdates
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
val isConnected: Boolean
get() = network.isConnected()
suspend fun checkForUpdates(): Boolean { suspend fun checkForUpdates(): Boolean {
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") { uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
app.toast(app.getString(R.string.update_check)) app.toast(app.getString(R.string.update_check))

View File

@ -5,8 +5,11 @@
<string name="cli">CLI</string> <string name="cli">CLI</string>
<string name="manager">Manager</string> <string name="manager">Manager</string>
<string name="revanced_patcher">ReVanced Patcher</string>
<string name="plugin_host_permission_label">ReVanced Manager plugin host</string> <string name="plugin_host_permission_label">ReVanced Manager plugin host</string>
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string> <string name="plugin_host_permission_description">Used to control access to ReVanced Manager
plugins. Only ReVanced Manager has this.</string>
<string name="toast_copied_to_clipboard">Copied!</string> <string name="toast_copied_to_clipboard">Copied!</string>
<string name="copy_to_clipboard">Copy to clipboard</string> <string name="copy_to_clipboard">Copy to clipboard</string>
@ -16,8 +19,10 @@
<string name="select_app">Select an app</string> <string name="select_app">Select an app</string>
<string name="patches_selected">%1$d/%2$d selected</string> <string name="patches_selected">%1$d/%2$d selected</string>
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string> <string name="new_downloader_plugins_notification">New downloader plugins available. Click here
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string> 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_">Import</string>
<string name="import_bundle">Import patch bundle</string> <string name="import_bundle">Import patch bundle</string>
@ -36,26 +41,26 @@
<string name="bundle_name_fallback">Unnamed</string> <string name="bundle_name_fallback">Unnamed</string>
<string name="android_11_bug_dialog_title">Android 11 bug</string> <string name="android_11_bug_dialog_title">Android 11 bug</string>
<string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string> <string name="android_11_bug_dialog_description">The app installation permission must be granted
ahead of time to avoid a bug in the Android 11 system that will negatively affect the user
<string name="no_network_toast">No internet connection available</string> experience.</string>
<string name="selected_app_meta_any_version">Any available version</string> <string name="selected_app_meta_any_version">Any available version</string>
<string name="app_source_dialog_title">Select source</string> <string name="app_source_dialog_title">Select source</string>
<string name="app_source_dialog_option_auto">Auto</string> <string name="app_source_dialog_option_auto">Auto</string>
<string name="app_source_dialog_option_auto_description">Use all installed downloaders to find a suitable APK file</string> <string name="app_source_dialog_option_auto_description">Use all installed downloaders to find a
suitable APK file</string>
<string name="app_source_dialog_option_auto_unavailable">No plugins available</string> <string name="app_source_dialog_option_auto_unavailable">No plugins available</string>
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</string> <string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string> without root access</string>
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not
match the suggested version</string>
<string name="patch_item_description">Start patching the application</string> <string name="patch_item_description">Start patching the application</string>
<string name="patch_selector_item">Patch selection and options</string> <string name="patch_selector_item">Patch selection and options</string>
<string name="patch_selector_item_description">%d patches selected</string> <string name="patch_selector_item_description">%d patches selected</string>
<string name="no_patches_selected">No patches selected</string> <string name="no_patches_selected">No patches selected</string>
<string name="network_unavailable_warning">Your device is not connected to the internet. Downloading will fail later.</string>
<string name="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
<string name="apk_source_selector_item">Change source</string> <string name="apk_source_selector_item">Change source</string>
<string name="apk_source_auto">Current: All downloaders</string> <string name="apk_source_auto">Current: All downloaders</string>
<string name="apk_source_downloader">Current: %s</string> <string name="apk_source_downloader">Current: %s</string>
@ -65,77 +70,91 @@
<string name="legacy_import_failed">Could not import legacy settings</string> <string name="legacy_import_failed">Could not import legacy settings</string>
<string name="auto_updates_dialog_title">Configure updates</string> <string name="auto_updates_dialog_title">Configure updates</string>
<string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically check for updates for the following components?</string> <string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically
check for updates for the following components?</string>
<string name="auto_updates_dialog_manager">ReVanced Manager</string> <string name="auto_updates_dialog_manager">ReVanced Manager</string>
<string name="auto_updates_dialog_patches">ReVanced Patches</string> <string name="auto_updates_dialog_patches">ReVanced Patches</string>
<string name="auto_updates_dialog_note">These settings can be changed later.</string> <string name="auto_updates_dialog_note">These settings can be changed later.</string>
<string name="general">General</string> <string name="general">General</string>
<string name="general_description">Theme, dynamic color</string> <string name="general_description">Appearances, Updates</string>
<string name="updates">Updates</string> <string name="updates">Updates</string>
<string name="updates_description">Check for updates and view changelogs</string> <string name="updates_description">Check for updates and view changelogs</string>
<string name="downloads">Downloads</string>
<string name="downloads_description">Downloader plugins and downloaded apps</string> <string name="extensions">Extensions</string>
<string name="import_export">Import &amp; export</string> <string name="extensions_description">Downloader plugins, downloaded apps</string>
<string name="import_export_description">Keystore, patch options and selection</string> <string name="backup_restore">Backup &amp; Restore</string>
<string name="backup_restore_description">Keystore, Patch selections, Patch options</string>
<string name="advanced">Advanced</string> <string name="advanced">Advanced</string>
<string name="advanced_description">API URL, memory limit, debugging</string> <string name="advanced_description">API Source, memory limits, debug logs</string>
<string name="experimental_features">Experimental features</string>
<string name="about">About</string> <string name="about">About</string>
<string name="opensource_licenses">Open source licenses</string> <string name="opensource_licenses">Open source licenses</string>
<string name="opensource_licenses_description">View all the libraries used to make this application</string> <string name="opensource_licenses_description">View all the libraries used to make this
application</string>
<string name="contributors">Contributors</string> <string name="contributors">Contributors</string>
<string name="contributors_description">View the contributors of ReVanced</string> <string name="contributors_description">View the contributors of ReVanced</string>
<string name="dynamic_color">Dynamic color</string> <string name="personalized_color">Personalized color</string>
<string name="dynamic_color_description">Adapt colors to the wallpaper</string> <string name="personalized_color_description">Use color provided by your device\'s palette</string>
<string name="theme">Theme</string> <string name="theme_mode">Theme mode</string>
<string name="theme_description">Choose between light or dark theme</string> <string name="theme_mode_description">Choose between light, dark, and system provided mode</string>
<string name="safeguards">Safeguards</string> <string name="safeguards">Safeguards</string>
<string name="patch_compat_check">Disable version compatibility check</string> <string name="allow_compatibility_mixing">Allow unsupported compatibility</string>
<string name="patch_compat_check_description">The check restricts patches to compatible app versions</string> <string name="allow_compatibility_mixing_description">Permit apps and patches to be mixed in
<string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.\n\nDo you want to proceed anyways?</string> unsupported state</string>
<string name="suggested_version_safeguard">Require suggested app version</string> <string name="allow_compatibility_mixing_confirmation">Selecting incompatible patches can result
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string> in a broken app.\n\nAllow anyways?</string>
<string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?</string>
<string name="patch_selection_safeguard">Allow changing patch selection</string> <string name="patch_selection_safeguard">Allow changing patch selection</string>
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches</string> <string name="patch_selection_safeguard_description">Permit selecting or deselecting patches
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?</string> from default</string>
<string name="universal_patches_safeguard">Disable universal patch warning</string> <string name="patch_selection_safeguard_confirmation">Changing the selection of patches may
<string name="universal_patches_safeguard_description">Disables the warning that appears when you try to select universal patches</string> cause unexpected issues.\n\nAllow anyways?</string>
<string name="universal_patches_safeguard_confirmation">Universal patches are not as well tested as those that target specific apps.\n\nEnable anyways?</string> <string name="universal_patches_safeguard">Allow universal patch</string>
<string name="import_keystore">Import keystore</string> <string name="universal_patches_safeguard_description">Permit selecting app\'s generic patch</string>
<string name="import_keystore_description">Import a custom keystore</string> <string name="universal_patches_safeguard_confirmation">Universal patch are not as well tested
<string name="import_keystore_dialog_title">Enter keystore credentials</string> as those that target specific apps.\n\nAllow anyways?</string>
<string name="import_keystore_dialog_description">You\'ll need enter the keystores credentials to import it.</string> <string name="backup">Backup</string>
<string name="import_keystore_dialog_alias_field">Username (Alias)</string> <string name="restore">Restore</string>
<string name="import_keystore_dialog_password_field">Password</string> <string name="keystore">Keystore</string>
<string name="import_keystore_dialog_button">Import</string> <string name="patch_selection">Patch selection</string>
<string name="import_keystore_wrong_credentials">Wrong keystore credentials</string> <string name="patch_options">Patch options</string>
<string name="import_keystore_success">Imported keystore</string> <string name="restore_keystore_description">Restore keystore from external source</string>
<string name="export_keystore">Export keystore</string> <string name="restore_keystore_dialog_title">Enter keystore credentials</string>
<string name="export_keystore_description">Export the current keystore</string> <string name="restore_keystore_dialog_description">You\'ll need enter the keystores credentials
<string name="export_keystore_unavailable">No keystore to export</string> to restore it.</string>
<string name="export_keystore_success">Exported keystore</string> <string name="restore_keystore_dialog_alias_field">Username (Alias)</string>
<string name="regenerate_keystore">Regenerate keystore</string> <string name="restore_keystore_dialog_password_field">Password</string>
<string name="regenerate_keystore_description">Generate a new keystore</string> <string name="restore_keystore_dialog_button">Import</string>
<string name="restore_keystore_wrong_credentials">Wrong keystore credentials</string>
<string name="restore_keystore_success">Keystore successfully restored</string>
<string name="backup_keystore_description">Export apps keystore into usable file</string>
<string name="backup_keystore_unavailable">No keystore to backup</string>
<string name="backup_keystore_success">Keystore successfully backed up</string>
<string name="regenerate_keystore">Regeneration</string>
<string name="regenerate_keystore_description">Replace current keystore with new one</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="restore_patch_selection_description">Export apps patch selections into usable
<string name="import_patch_selection_description">Import patch selection from a JSON file</string> file</string>
<string name="import_patch_selection_fail">Could not import patch selection: %s</string> <string name="restore_patch_selection_fail">Could not import patch selection: %s</string>
<string name="import_patch_selection_success">Imported patch selection</string> <string name="restore_patch_selection_success">Patch selection successfully restored</string>
<string name="export_patch_selection">Export patch selection</string> <string name="backup_patch_selection_description">Import apps patch selections from external
<string name="export_patch_selection_description">Export patch selection to a JSON file</string> source</string>
<string name="export_patch_selection_fail">Could not export patch selection: %s</string> <string name="backup_patch_selection_fail">Could not backup patch selection: %s</string>
<string name="export_patch_selection_success">Exported patch selection</string> <string name="backup_patch_selection_success">Patch selection successfully restored</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_selection_success">Patch selection has been reset</string> <string name="reset_patch_selection_success">Patch selection successfully reset</string>
<string name="patch_options_reset_package">Reset patch options for app</string> <string name="patch_options_reset_package">Reset for app</string>
<string name="patch_options_reset_package_description">Resets patch options for a single app</string> <string name="patch_options_reset_package_description">Use the default patch options
<string name="patch_options_reset_bundle">Resets patch options for bundle</string> configuration for a single app</string>
<string name="patch_options_reset_bundle_description">Resets patch options for all patches in a bundle</string> <string name="patch_options_reset_bundle">Reset for bundle</string>
<string name="patch_options_reset_all">Reset patch options</string> <string name="patch_options_reset_bundle_description">Use the default patch options
<string name="patch_options_reset_all_description">Resets all patch options</string> configuration for all patches in a bundle</string>
<string name="patch_options_reset_all_description">Use the default patch options configuration</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>
<string name="downloader_plugin_state_failed">Failed to load. Click for more details</string> <string name="downloader_plugin_state_failed">Failed to load. Click for more details</string>
@ -175,21 +194,25 @@
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="appearance">Appearance</string> <string name="appearance">Appearance</string>
<string name="downloaded_apps">Downloaded apps</string> <string name="downloaded_apps">Downloaded apps</string>
<string name="process_runtime">Run Patcher in another process (experimental)</string> <string name="process_runtime">Run patcher in another process</string>
<string name="process_runtime_description">This is faster and allows Patcher to use more memory.</string> <string name="process_runtime_description">Faster and allows patcher to use more memory</string>
<string name="process_runtime_memory_limit">Patcher process memory limit</string> <string name="process_runtime_memory_limit">Patcher process memory limit</string>
<string name="process_runtime_memory_limit_description">The max amount of memory that the Patcher process can use (in megabytes)</string> <string name="process_runtime_memory_limit_description">The max amount of memory that the
patcher process can use (in megabytes)</string>
<string name="debug_logs_export">Export debug logs</string> <string name="debug_logs_export">Export debug logs</string>
<string name="debug_logs_export_read_failed">Failed to read logs (exit code %d)</string> <string name="debug_logs_export_read_failed">Failed to read logs (exit code %d)</string>
<string name="debug_logs_export_failed">Failed to export logs</string> <string name="debug_logs_export_failed">Failed to export logs</string>
<string name="debug_logs_export_success">Exported logs</string> <string name="debug_logs_export_success">Exported logs</string>
<string name="api_url">API URL</string> <string name="api_url">API Source</string>
<string name="api_url_description">The API used to download necessary files.</string> <string name="api_url_dialog_title">Set custom API Source</string>
<string name="api_url_dialog_title">Set custom API URL</string> <string name="api_url_dialog_description">Set the API Source of ReVanced Manager. ReVanced
<string name="api_url_dialog_description">Set the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string> Manager uses the API to download patches and updates.</string>
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string> <string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches
and updates. Make sure that you trust it.</string>
<string name="api_url_dialog_save">Set</string> <string name="api_url_dialog_save">Set</string>
<string name="api_url_dialog_reset">Reset API URL</string> <string name="api_url_dialog_reset">Reset API Source</string>
<string name="testing">Testing</string>
<string name="disable_safeguard">Disable all safeguards</string>
<string name="device">Device</string> <string name="device">Device</string>
<string name="device_android_version">Android version</string> <string name="device_android_version">Android version</string>
<string name="device_model">Model</string> <string name="device_model">Model</string>
@ -216,23 +239,34 @@
<string name="no_patched_apps_found">No patched apps found</string> <string name="no_patched_apps_found">No patched apps found</string>
<string name="tap_on_patches">Tap on the patches to get more information about them</string> <string name="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="bundles_selected">%s selected</string> <string name="bundles_selected">%s selected</string>
<string name="incompatible_patches">Incompatible patches</string> <string name="unsupported_patches">Incompatible patches</string>
<string name="universal_patches">Universal patches</string> <string name="universal_patches">Universal patches</string>
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string> <string name="patch_selection_reset_toast">Patch selection and options has been reset to
recommended defaults</string>
<string name="patch_options_reset_toast">Patch options have been reset</string> <string name="patch_options_reset_toast">Patch options have been reset</string>
<string name="non_suggested_version_warning_title">Non suggested version</string> <string name="non_suggested_version_warning_title">Non suggested version</string>
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo continue anyway, disable \"Require suggested app version\" in the advanced settings.</string> <string name="non_suggested_version_warning_description">The version of the app you have
selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo
continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
<string name="selection_warning_title">Stop using defaults?</string> <string name="selection_warning_title">Stop using defaults?</string>
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string> <string name="selection_warning_description">It is recommended to use the default patch
<string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nThis warning can be disabled in the advanced settings.</string> selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on
<string name="this_version">This version</string> \"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
<string name="universal_patch_warning_description">Universal patches have a more generalized use
and do not work as reliably as patches that target specific apps. You may encounter issues
while using them.\n\nThis warning can be disabled in the advanced settings.</string>
<string name="supported">This version</string>
<string name="universal">Any app</string> <string name="universal">Any app</string>
<string name="unsupported">Unsupported</string>
<string name="search_patches">Search patches</string> <string name="search_patches">Search patches</string>
<string name="app_version_not_compatible">This patch is not compatible with the selected app version (%1$s).\n\nIt is only compatible with the following version(s): %2$s.</string> <string name="app_not_supported">This patch is not compatible with the selected app version
(%1$s).\n\nIt only supports the following version(s): %2$s.</string>
<string name="continue_with_version">Continue with this version?</string> <string name="continue_with_version">Continue with this version?</string>
<string name="version_not_compatible">Not all patches are compatible with this version (%s). Do you want to continue anyway?</string> <string name="version_not_supported">Not all patches support this version (%s). Do you want to
continue anyway?</string>
<string name="download_application">Download application?</string> <string name="download_application">Download application?</string>
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string> <string name="app_not_installed">The app you selected isn\'t installed. Do you want to download
it?</string>
<string name="failed_to_load_apk">Failed to load APK</string> <string name="failed_to_load_apk">Failed to load APK</string>
<string name="loading">Loading…</string> <string name="loading">Loading…</string>
<string name="not_installed">Not installed</string> <string name="not_installed">Not installed</string>
@ -262,7 +296,8 @@
<string name="downloader_app_not_found">Downloader did not find the app</string> <string name="downloader_app_not_found">Downloader did not find the app</string>
<string name="downloader_error">Downloader error: %s</string> <string name="downloader_error">Downloader error: %s</string>
<string name="downloader_no_plugins_installed">No plugins installed.</string> <string name="downloader_no_plugins_installed">No plugins installed.</string>
<string name="downloader_no_plugins_available">No trusted plugins available for use. Check your settings.</string> <string name="downloader_no_plugins_available">No trusted plugins available for use. Check your
settings.</string>
<string name="already_patched">Already patched</string> <string name="already_patched">Already patched</string>
<string name="patch_selector_sheet_filter_title">Filter</string> <string name="patch_selector_sheet_filter_title">Filter</string>
@ -290,7 +325,8 @@
<string name="save_apk_success">APK Saved</string> <string name="save_apk_success">APK Saved</string>
<string name="sign_fail">Failed to sign APK: %s</string> <string name="sign_fail">Failed to sign APK: %s</string>
<string name="save_logs">Save logs</string> <string name="save_logs">Save logs</string>
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with this plugin.</string> <string name="plugin_activity_dialog_body">User interaction is required in order to proceed with
this plugin.</string>
<string name="select_install_type">Select installation type</string> <string name="select_install_type">Select installation type</string>
<string name="patcher_step_group_preparing">Preparing</string> <string name="patcher_step_group_preparing">Preparing</string>
@ -332,28 +368,33 @@
<string name="bundle_update_success">Successfully updated %s</string> <string name="bundle_update_success">Successfully updated %s</string>
<string name="bundle_update_unavailable">No update available for %s</string> <string name="bundle_update_unavailable">No update available for %s</string>
<string name="bundle_auto_update">Auto update</string> <string name="bundle_auto_update">Auto update</string>
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string> <string name="bundle_auto_update_description">Automatically update this bundle when ReVanced
starts</string>
<string name="bundle_view_patches">View patches</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_version">Any version</string>
<string name="bundle_view_patches_any_package">Any package</string> <string name="bundle_view_patches_any_package">Any package</string>
<string name="about_revanced_manager">About ReVanced Manager</string> <string name="about_revanced_manager">About ReVanced Manager</string>
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string> <string name="revanced_manager_description">ReVanced Manager is an application designed to work
with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps.
The patching system is designed to automatically work with new versions of apps with minimal
maintenance.</string>
<string name="update_available">An update is available</string> <string name="update_available">An update is available</string>
<string name="current_version">Current version: %s</string> <string name="current_version">Current version: %s</string>
<string name="new_version">New version: %s</string> <string name="new_version">New version: %s</string>
<string name="ready_to_install_update">Ready to install update</string> <string name="ready_to_install_update">Ready to install update</string>
<string name="update_completed">Update installed</string> <string name="update_completed">Update installed</string>
<string name="install_update_manager_failed">Failed to install update</string> <string name="install_update_manager_failed">Failed to install update</string>
<string name="manual_update_check">Check for updates</string> <string name="check_for_update">Check for update</string>
<string name="manual_update_check_description">Manually check for updates</string> <string name="check_for_update_auto_description">Automatically check for new version when the
<string name="update_checking_manager">Auto check for updates</string> app launched</string>
<string name="update_checking_manager_description">Check for new versions of ReVanced Manager when the application starts</string>
<string name="changelog">View changelogs</string> <string name="changelog">View changelogs</string>
<string name="changelog_loading">Loading changelog</string> <string name="changelog_loading">Loading changelog</string>
<string name="changelog_download_fail">Failed to download changelog: %s</string> <string name="changelog_download_fail">Failed to download changelog: %s</string>
<string name="changelog_description">Check out the latest changes in this update</string> <string name="changelog_description">Check out the latest changes in this update</string>
<string name="battery_optimization_notification">Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off optimizations.</string> <string name="battery_optimization_notification">Battery optimizations must be turned off in
order for ReVanced Manager to work correctly in the background. Click here to turn off
optimizations.</string>
<string name="installing_manager_update">Installing update…</string> <string name="installing_manager_update">Installing update…</string>
<string name="downloading_manager_update">Downloading update…</string> <string name="downloading_manager_update">Downloading update…</string>
<string name="download_manager_failed">Failed to download update: %s</string> <string name="download_manager_failed">Failed to download update: %s</string>
@ -362,7 +403,8 @@
<string name="save_with_count">Save (%1$s)</string> <string name="save_with_count">Save (%1$s)</string>
<string name="update">Update</string> <string name="update">Update</string>
<string name="empty">Empty</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. \n ReVanced Manager will
close when updating.</string>
<string name="no_changelogs_found">No changelogs found</string> <string name="no_changelogs_found">No changelogs found</string>
<string name="just_now">Just now</string> <string name="just_now">Just now</string>
<string name="minutes_ago">%sm ago</string> <string name="minutes_ago">%sm ago</string>
@ -378,18 +420,22 @@
<string name="no_update_available">No update available</string> <string name="no_update_available">No update available</string>
<string name="update_check">Checking for updates…</string> <string name="update_check">Checking for updates…</string>
<string name="dismiss_temporary">Not now</string> <string name="dismiss_temporary">Not now</string>
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string> <string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is
available.</string>
<string name="failed_to_download_update">Failed to download update: %s</string> <string name="failed_to_download_update">Failed to download update: %s</string>
<string name="download">Download</string> <string name="download">Download</string>
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string> <string name="download_confirmation_metered">You are currently on a metered connection, and data
charges from your service provider may apply.\n\nDo you still want to continue?</string>
<string name="download_update_confirmation">Download update?</string> <string name="download_update_confirmation">Download update?</string>
<string name="no_contributors_found">No contributors found</string> <string name="no_contributors_found">No contributors found</string>
<string name="select">Select</string> <string name="select">Select</string>
<string name="select_deselect_all">Select or deselect all</string> <string name="select_deselect_all">Select or deselect all</string>
<string name="select_bundle_type_dialog_title">Add new bundle</string> <string name="select_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="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="local_bundle_description">Import local files from your storage, does not
<string name="remote_bundle_description">Import remote files from a URL, can automatically update</string> automatically update</string>
<string name="remote_bundle_description">Import remote files from a URL, can automatically
update</string>
<string name="recommended">Recommended</string> <string name="recommended">Recommended</string>
<string name="installation_failed_dialog_title">Installation failed</string> <string name="installation_failed_dialog_title">Installation failed</string>
@ -400,13 +446,20 @@
<string name="installation_invalid_dialog_title">Installation invalid</string> <string name="installation_invalid_dialog_title">Installation invalid</string>
<string name="installation_storage_issue_dialog_title">Not enough storage</string> <string name="installation_storage_issue_dialog_title">Not enough storage</string>
<string name="installation_timeout_dialog_title">Installation timed out</string> <string name="installation_timeout_dialog_title">Installation timed out</string>
<string name="installation_failed_description">The installation failed due to an unknown reason. Try again?</string> <string name="installation_failed_description">The installation failed due to an unknown reason.
<string name="installation_aborted_description">The installation was cancelled manually. Try again?</string> Try again?</string>
<string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string> <string name="installation_aborted_description">The installation was cancelled manually. Try
<string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string> again?</string>
<string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is compatible by this device and try again.</string> <string name="installation_blocked_description">The installation was blocked. Review your device
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string> security settings and try again.</string>
<string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string> <string name="installation_conflict_description">The installation was prevented by an existing
installation of the app. Uninstall the installed app and try again?</string>
<string name="installation_incompatible_description">The app is incompatible with this device.
Use an APK that is supported by this device and try again.</string>
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try
again?</string>
<string name="installation_storage_issue_description">The app could not be installed due to
insufficient storage. Free up some space and try again.</string>
<string name="installation_timeout_description">The installation took too long. Try again?</string> <string name="installation_timeout_description">The installation took too long. Try again?</string>
<string name="reinstall">Reinstall</string> <string name="reinstall">Reinstall</string>
<string name="show">Show</string> <string name="show">Show</string>
@ -417,12 +470,14 @@
<string name="add_patch_bundle">Add patch bundle</string> <string name="add_patch_bundle">Add patch bundle</string>
<string name="bundle_url">Bundle URL</string> <string name="bundle_url">Bundle URL</string>
<string name="auto_update">Auto update</string> <string name="auto_update">Auto update</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="unsupported_patches_dialog">These patches are not compatible with the selected app
<string name="incompatible_patch">Incompatible patch</string> version (%1$s).\n\nClick on the patches to see more details.</string>
<string name="unsupported_patch">Unsupported patch</string>
<string name="any_version">Any</string> <string name="any_version">Any</string>
<string name="never_show_again">Never show again</string> <string name="never_show_again">Never show again</string>
<string name="show_manager_update_dialog_on_launch">Show update message on launch</string> <string name="show_manager_update_dialog_on_launch">Show update message on launch</string>
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string> <string name="show_manager_update_dialog_on_launch_description">Shows a popup notification
whenever there is a new update available on launch.</string>
<string name="failed_to_import_keystore">Failed to import keystore</string> <string name="failed_to_import_keystore">Failed to import keystore</string>
<string name="export">Export</string> <string name="export">Export</string>
</resources> </resources>

View File

@ -7,4 +7,10 @@ plugins {
alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.about.libraries) apply false alias(libs.plugins.about.libraries) apply false
alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.binary.compatibility.validator)
} }
apiValidation {
ignoredProjects.addAll(listOf("app", "example-downloader-plugin"))
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
}

1
downloader-plugin/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,171 @@
public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
}
public final class app/revanced/manager/plugin/downloader/ConstantsKt {
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
}
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getHeaders ()Ljava/util/Map;
public final fun getUrl ()Ljava/lang/String;
public fun hashCode ()I
public final fun toDownloadResult ()Lkotlin/Pair;
public fun toString ()Ljava/lang/String;
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Downloader {
}
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
}
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
}
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
public final fun download (Lkotlin/jvm/functions/Function3;)V
public final fun get (Lkotlin/jvm/functions/Function4;)V
public fun getHostPackageName ()Ljava/lang/String;
public fun getPluginPackageName ()Ljava/lang/String;
public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
}
public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
}
public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getName ()Ljava/lang/String;
public final fun getVersion ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
}
public abstract interface class app/revanced/manager/plugin/downloader/Scope {
public abstract fun getHostPackageName ()Ljava/lang/String;
public abstract fun getPluginPackageName ()Ljava/lang/String;
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
public final fun getIntent ()Landroid/content/Intent;
public final fun getResultCode ()I
}
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
}
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public fun finish ()V
public fun load (Ljava/lang/String;)V
}
public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
}
public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public fun pageLoad (Ljava/lang/String;)V
public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
}
public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
public fun <init> ()V
public fun asBinder ()Landroid/os/IBinder;
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
}
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
public final fun download (Lkotlin/jvm/functions/Function5;)V
public fun getHostPackageName ()Ljava/lang/String;
public fun getPluginPackageName ()Ljava/lang/String;
public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
}

View File

@ -0,0 +1,61 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
`maven-publish`
}
android {
namespace = "app.revanced.manager.plugin.downloader"
compileSdk = 35
defaultConfig {
minSdk = 26
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
aidl = true
}
}
dependencies {
implementation(libs.androidx.ktx)
implementation(libs.activity.ktx)
implementation(libs.runtime.ktx)
implementation(libs.appcompat)
}
publishing {
repositories {
mavenLocal()
}
publications {
create<MavenPublication>("release") {
groupId = "app.revanced"
artifactId = "manager-downloader-plugin"
version = "1.0"
afterEvaluate {
from(components["release"])
}
}
}
}

View File

21
downloader-plugin/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,8 @@
// IWebView.aidl
package app.revanced.manager.plugin.downloader.webview;
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
oneway interface IWebView {
void load(String url);
void finish();
}

View File

@ -0,0 +1,11 @@
// IWebViewEvents.aidl
package app.revanced.manager.plugin.downloader.webview;
import app.revanced.manager.plugin.downloader.webview.IWebView;
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
oneway interface IWebViewEvents {
void ready(IWebView iface);
void pageLoad(String url);
void download(String url, String mimetype, String userAgent);
}

View File

@ -0,0 +1,7 @@
package app.revanced.manager.plugin.downloader
/**
* The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission.
* Plugin UI activities and internal services can be protected using this permission.
*/
const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST"

View File

@ -0,0 +1,165 @@
package app.revanced.manager.plugin.downloader
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.app.Activity
import android.os.Parcelable
import kotlinx.coroutines.withTimeout
import java.io.InputStream
import java.io.OutputStream
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
)
@Retention(AnnotationRetention.BINARY)
annotation class PluginHostApi
/**
* The base interface for all DSL scopes.
*/
interface Scope {
/**
* The package name of ReVanced Manager.
*/
val hostPackageName: String
/**
* The package name of the plugin.
*/
val pluginPackageName: String
}
/**
* The scope of [DownloaderScope.get].
*/
interface GetScope : Scope {
/**
* Ask the user to perform some required interaction in the activity specified by the provided [Intent].
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
*
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
*/
suspend fun requestStartActivity(intent: Intent): Intent?
}
interface BaseDownloadScope : Scope
/**
* The scope for [DownloaderScope.download].
*/
interface InputDownloadScope : BaseDownloadScope
typealias Size = Long
typealias DownloadResult = Pair<InputStream, Size?>
typealias Version = String
typealias GetResult<T> = Pair<T, Version?>
class DownloaderScope<T : Parcelable> internal constructor(
private val scopeImpl: Scope,
internal val context: Context
) : Scope by scopeImpl {
// Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
/**
* Define the download block of the plugin.
*/
fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
download = { app, outputStream ->
val (inputStream, size) = inputDownloadScopeImpl.block(app)
inputStream.use {
if (size != null) reportSize(size)
it.copyTo(outputStream)
}
}
}
/**
* Define the get block of the plugin.
* The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
*/
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
get = block
}
/**
* Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends.
*/
suspend fun <R : Any?> useService(intent: Intent, block: suspend (IBinder) -> R): R {
var onBind: ((IBinder) -> Unit)? = null
val serviceConn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) =
onBind!!(service!!)
override fun onServiceDisconnected(name: ComponentName?) {}
}
return try {
val binder = withTimeout(10000L) {
suspendCoroutine { continuation ->
onBind = continuation::resume
context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE)
}
}
block(binder)
} finally {
onBind = null
context.unbindService(serviceConn)
}
}
}
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
@PluginHostApi
fun build(scopeImpl: Scope, context: Context) =
with(DownloaderScope<T>(scopeImpl, context)) {
block()
Downloader(
download = download!!,
get = get!!
)
}
}
class Downloader<T : Parcelable> internal constructor(
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
@property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
)
/**
* Define a downloader plugin.
*/
fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
/**
* @see GetScope.requestStartActivity
*/
sealed class UserInteractionException(message: String) : Exception(message) {
class RequestDenied @PluginHostApi constructor() :
UserInteractionException("Request denied by user")
sealed class Activity(message: String) : UserInteractionException(message) {
class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
/**
* @param resultCode The result code of the activity.
* @param intent The [Intent] of the activity.
*/
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
Activity("Unexpected activity result code: $resultCode")
}
}

View File

@ -0,0 +1,42 @@
package app.revanced.manager.plugin.downloader
import android.app.Activity
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.Parcelable
import java.io.OutputStream
/**
* The scope of the [OutputStream] version of [DownloaderScope.download].
*/
interface OutputDownloadScope : BaseDownloadScope {
suspend fun reportSize(size: Long)
}
/**
* A replacement for [DownloaderScope.download] that uses [OutputStream].
* The provided [OutputStream] does not need to be closed manually.
*/
fun <T : Parcelable> DownloaderScope<T>.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) {
download = block
}
/**
* Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY].
* @see [GetScope.requestStartActivity]
*/
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
requestStartActivity(
Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
)
/**
* Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
* @see [DownloaderScope.useService]
*/
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService(
noinline block: suspend (IBinder) -> R
) = useService(
Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
)

View File

@ -0,0 +1,39 @@
package app.revanced.manager.plugin.downloader
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.net.HttpURLConnection
import java.net.URI
/**
* A simple parcelable data class for storing a package name and version.
* This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
*
* @param name The package name.
* @param version The version.
*/
@Parcelize
data class Package(val name: String, val version: String) : Parcelable
/**
* A data class for storing a download URL.
*
* @param url The download URL.
* @param headers The headers to use for the request.
*/
@Parcelize
data class DownloadUrl(val url: String, val headers: Map<String, String> = emptyMap()) : Parcelable {
/**
* Converts this into a [DownloadResult].
*/
fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) {
useCaches = false
allowUserInteraction = false
headers.forEach(::setRequestProperty)
connectTimeout = 10_000
connect()
inputStream to getHeaderField("Content-Length").toLong()
}
}

View File

@ -0,0 +1,176 @@
package app.revanced.manager.plugin.downloader.webview
import android.content.Intent
import app.revanced.manager.plugin.downloader.DownloadUrl
import app.revanced.manager.plugin.downloader.DownloaderScope
import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.Scope
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.PluginHostApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import kotlin.properties.Delegates
typealias InitialUrl = String
typealias PageLoadCallback<T> = suspend WebViewCallbackScope<T>.(url: String) -> Unit
typealias DownloadCallback<T> = suspend WebViewCallbackScope<T>.(url: String, mimeType: String, userAgent: String) -> Unit
interface WebViewCallbackScope<T> : Scope {
/**
* Finishes the activity and returns the [result].
*/
suspend fun finish(result: T)
/**
* Tells the WebView to load the specified [url].
*/
suspend fun load(url: String)
}
@OptIn(PluginHostApi::class)
class WebViewScope<T> internal constructor(
coroutineScope: CoroutineScope,
private val scopeImpl: Scope,
setResult: (T) -> Unit
) : Scope by scopeImpl {
private var onPageLoadCallback: PageLoadCallback<T> = {}
private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> }
@OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = Dispatchers.Default.limitedParallelism(1)
private lateinit var webView: IWebView
internal lateinit var initialUrl: String
internal val binder = object : IWebViewEvents.Stub() {
override fun ready(iface: IWebView?) {
coroutineScope.launch(dispatcher) {
webView = iface!!.also {
it.load(initialUrl)
}
}
}
override fun pageLoad(url: String?) {
coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) }
}
override fun download(url: String?, mimetype: String?, userAgent: String?) {
coroutineScope.launch(dispatcher) {
onDownloadCallback(
callbackScope,
url!!,
mimetype!!,
userAgent!!
)
}
}
}
private val callbackScope = object : WebViewCallbackScope<T>, Scope by scopeImpl {
override suspend fun finish(result: T) {
setResult(result)
// Tell the WebViewActivity to finish
webView.let { withContext(Dispatchers.IO) { it.finish() } }
}
override suspend fun load(url: String) {
webView.let { withContext(Dispatchers.IO) { it.load(url) } }
}
}
/**
* Called when the WebView attempts to download a file to disk.
*/
fun download(block: DownloadCallback<T>) {
onDownloadCallback = block
}
/**
* Called when the WebView finishes loading a page.
*/
fun pageLoad(block: PageLoadCallback<T>) {
onPageLoadCallback = block
}
}
@JvmInline
private value class Container<U>(val value: U)
/**
* Run a [android.webkit.WebView] Activity controlled by the provided code block.
* The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish].
* The [block] defines the event handlers and returns the initial URL.
*
* @param title The string displayed in the action bar.
* @param block The control block.
*/
@OptIn(PluginHostApi::class)
suspend fun <T> GetScope.runWebView(
title: String,
block: suspend WebViewScope<T>.() -> InitialUrl
) = supervisorScope {
var result by Delegates.notNull<Container<T>>()
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
scope.initialUrl = scope.block()
// Start the webview activity and wait until it finishes.
requestStartActivity(Intent().apply {
putExtra(
WebViewActivity.KEY,
WebViewActivity.Parameters(title, scope.binder)
)
setClassName(
hostPackageName,
WebViewActivity::class.qualifiedName!!
)
})
// Return the result and cancel any leftover coroutines.
coroutineContext.cancelChildren()
result.value
}
/**
* Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView].
* Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get].
*
* @see runWebView
*/
fun WebViewDownloader(block: suspend WebViewScope<DownloadUrl>.(packageName: String, version: String?) -> InitialUrl?) =
Downloader<DownloadUrl> {
val label = context.applicationInfo.loadLabel(
context.packageManager
).toString()
get { packageName, version ->
class ReturnNull : Exception()
try {
runWebView(label) {
download { url, _, userAgent ->
finish(
DownloadUrl(
url,
mapOf("User-Agent" to userAgent)
)
)
}
block(this@runWebView, packageName, version) ?: throw ReturnNull()
} to version
} catch (_: ReturnNull) {
null
}
}
download {
it.toDownloadResult()
}
}

View File

@ -0,0 +1,161 @@
package app.revanced.manager.plugin.downloader.webview
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.IBinder
import android.os.Parcelable
import android.view.MenuItem
import android.webkit.CookieManager
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.addCallback
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.R
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@OptIn(PluginHostApi::class)
@PluginHostApi
class WebViewActivity : ComponentActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm by viewModels<WebViewModel>()
enableEdgeToEdge()
setContentView(R.layout.activity_webview)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val webView = findViewById<WebView>(R.id.webview)
onBackPressedDispatcher.addCallback {
if (webView.canGoBack()) webView.goBack()
else cancelActivity()
}
val params = intent.getParcelableExtra<Parameters>(KEY)!!
actionBar?.apply {
title = params.title
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
setDisplayHomeAsUpEnabled(true)
}
val events = IWebViewEvents.Stub.asInterface(params.events)!!
vm.setup(events)
webView.apply {
settings.apply {
cacheMode = WebSettings.LOAD_NO_CACHE
allowContentAccess = false
domStorageEnabled = true
javaScriptEnabled = true
}
webViewClient = vm.webViewClient
setDownloadListener { url, userAgent, _, mimetype, _ ->
vm.onDownload(url, mimetype, userAgent)
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.commands.collect {
when (it) {
is WebViewModel.Command.Finish -> {
setResult(RESULT_OK)
finish()
}
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
}
}
}
}
}
private fun cancelActivity() {
setResult(RESULT_CANCELED)
finish()
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
cancelActivity()
true
} else super.onOptionsItemSelected(item)
@Parcelize
internal class Parameters(
val title: String, val events: IBinder
) : Parcelable
internal companion object {
const val KEY = "params"
}
}
@OptIn(PluginHostApi::class)
internal class WebViewModel : ViewModel() {
init {
CookieManager.getInstance().apply {
removeAllCookies(null)
setAcceptCookie(true)
}
}
private val commandChannel = Channel<Command>()
val commands = commandChannel.receiveAsFlow()
private var eventBinder: IWebViewEvents? = null
private val ctrlBinder = object : IWebView.Stub() {
override fun load(url: String?) {
viewModelScope.launch {
commandChannel.send(Command.Load(url!!))
}
}
override fun finish() {
viewModelScope.launch {
commandChannel.send(Command.Finish)
}
}
}
val webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
eventBinder!!.pageLoad(url)
}
}
fun onDownload(url: String, mimeType: String, userAgent: String) {
eventBinder!!.download(url, mimeType, userAgent)
}
fun setup(binder: IWebViewEvents) {
if (eventBinder != null) return
eventBinder = binder
binder.ready(ctrlBinder)
}
sealed interface Command {
data class Load(val url: String) : Command
data object Finish : Command
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1 @@
<resources></resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.WebViewActivity" parent="Theme.AppCompat.DayNight">
<item name="android:windowActionBar">true</item>
<item name="android:windowNoTitle">false</item>
</style>
</resources>

1
example-downloader-plugin/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,53 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
}
android {
val packageName = "app.revanced.manager.plugin.downloader.example"
namespace = packageName
compileSdk = 35
defaultConfig {
applicationId = packageName
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
if (project.hasProperty("signAsDebug")) {
signingConfig = signingConfigs.getByName("debug")
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures.compose = true
}
dependencies {
implementation(libs.activity.compose)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling)
implementation(libs.compose.material3)
compileOnly(project(":downloader-plugin"))
}

View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="app.revanced.manager.plugin.downloader" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
tools:targetApi="34">
<activity
android:name=".InteractionActivity"
android:exported="true"
android:permission="app.revanced.manager.permission.PLUGIN_HOST"
android:theme="@android:style/Theme.DeviceDefault" />
<meta-data
android:name="app.revanced.manager.plugin.downloader.class"
android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginKt" />
</application>
</manifest>

View File

@ -0,0 +1,69 @@
@file:Suppress("Unused")
package app.revanced.manager.plugin.downloader.example
import android.app.Application
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Parcelable
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.requestStartActivity
import app.revanced.manager.plugin.downloader.webview.WebViewDownloader
import kotlinx.parcelize.Parcelize
import kotlin.io.path.*
val apkMirrorDownloader = WebViewDownloader { packageName, version ->
with(Uri.Builder()) {
scheme("https")
authority("www.apkmirror.com")
mapOf(
"post_type" to "app_release",
"searchtype" to "apk",
"s" to (version?.let { "$packageName $it" } ?: packageName),
"bundles%5B%5D" to "apk_files" // bundles[]
).forEach { (key, value) ->
appendQueryParameter(key, value)
}
build().toString()
}
}
@Parcelize
class InstalledApp(val path: String) : Parcelable
private val application by lazy {
// Don't do this in a real plugin.
val clazz = Class.forName("android.app.ActivityThread")
val activityThread = clazz.getMethod("currentActivityThread")(null)
clazz.getMethod("getApplication")(activityThread) as Application
}
val installedAppDownloader = Downloader<InstalledApp> {
val pm = application.packageManager
get { packageName, version ->
val packageInfo = try {
pm.getPackageInfo(packageName, 0)
} catch (_: PackageManager.NameNotFoundException) {
return@get null
}
if (version != null && packageInfo.versionName != version) return@get null
requestStartActivity<InteractionActivity>()
InstalledApp(packageInfo.applicationInfo!!.sourceDir) to packageInfo.versionName
}
download { app ->
with(Path(app.path)) { inputStream() to fileSize() }
}
/*
download { app, outputStream ->
val path = Path(app.path)
reportSize(path.fileSize())
Files.copy(path, outputStream)
}*/
}

View File

@ -0,0 +1,65 @@
package app.revanced.manager.plugin.downloader.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.Modifier
class InteractionActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val isDarkTheme = isSystemInDarkTheme()
val colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme()
MaterialTheme(colorScheme) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("User interaction example") }
)
}
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
Text("This is an example interaction.")
Row {
TextButton(
onClick = {
setResult(RESULT_CANCELED)
finish()
}
) {
Text("Cancel")
}
TextButton(
onClick = {
setResult(RESULT_OK)
finish()
}
) {
Text("Continue")
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Example Downloader Plugin</string>
</resources>

View File

@ -19,7 +19,6 @@ datetime = "0.6.1"
room-version = "2.6.1" room-version = "2.6.1"
revanced-patcher = "21.0.0" revanced-patcher = "21.0.0"
revanced-library = "3.0.2" revanced-library = "3.0.2"
plugin-api = "1.0.0"
koin = "3.5.3" koin = "3.5.3"
ktor = "2.3.9" ktor = "2.3.9"
markdown-renderer = "0.30.0" markdown-renderer = "0.30.0"
@ -28,6 +27,7 @@ kotlin = "2.1.10"
android-gradle-plugin = "8.8.0" android-gradle-plugin = "8.8.0"
dev-tools-gradle-plugin = "2.1.10-1.0.29" dev-tools-gradle-plugin = "2.1.10-1.0.29"
about-libraries-gradle-plugin = "11.5.0" about-libraries-gradle-plugin = "11.5.0"
binary-compatibility-validator = "0.17.0"
coil = "2.7.0" coil = "2.7.0"
app-icon-loader-coil = "1.5.0" app-icon-loader-coil = "1.5.0"
skrapeit = "1.2.2" skrapeit = "1.2.2"
@ -45,6 +45,7 @@ runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", ve
runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "viewmodel-lifecycle" } runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "viewmodel-lifecycle" }
splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" } splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" }
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" } activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" } work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" }
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" } preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
@ -83,9 +84,6 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref =
revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" } revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" }
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" } revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
# Plugin API
plugin-api = { group = "app.revanced", name = "revanced-manager-downloader-api", version.ref = "plugin-api" }
# Koin # Koin
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
@ -145,4 +143,5 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" } devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" } about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }

View File

@ -26,3 +26,5 @@ dependencyResolutionManagement {
} }
rootProject.name = "ReVanced Manager" rootProject.name = "ReVanced Manager"
include(":app") include(":app")
include(":downloader-plugin")
include(":example-downloader-plugin")