Compare commits

..

12 Commits

Author SHA1 Message Date
semantic-release-bot
7615453eec chore: Release v1.26.0-dev.20 [skip ci]
# app [1.26.0-dev.20](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.19...v1.26.0-dev.20) (2026-01-09)

### Bug Fixes

* Save FAB freaking out in select patches screen ([4c0b6b0](4c0b6b02e9))
2026-01-09 20:41:33 +00:00
Ax333l
4c0b6b02e9 fix: Save FAB freaking out in select patches screen 2026-01-09 21:33:08 +01:00
Ax333l
fe84b22b6f chore: update dependencies and fix deprecations 2026-01-09 19:36:04 +01:00
semantic-release-bot
1b21f5d4ab chore: Release v1.26.0-dev.19 [skip ci]
# app [1.26.0-dev.19](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.18...v1.26.0-dev.19) (2026-01-08)

### Bug Fixes

* **locales:** use buildconfig instead of generating kt file ([72b1db9](72b1db9a2f))
2026-01-08 22:35:21 +00:00
Ax333l
72b1db9a2f fix(locales): use buildconfig instead of generating kt file 2026-01-08 23:27:02 +01:00
semantic-release-bot
2805ac6540 chore: Release v1.26.0-dev.18 [skip ci]
# app [1.26.0-dev.18](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.17...v1.26.0-dev.18) (2026-01-08)

### Bug Fixes

* Prevent trailing comma when no locales are generated ([b16931c](b16931ca79))

### Features

* Add language settings ([#2913](https://github.com/ReVanced/revanced-manager/issues/2913)) ([df31b39](df31b39cc8))
2026-01-08 21:12:25 +00:00
Robert
b16931ca79 fix: Prevent trailing comma when no locales are generated 2026-01-08 22:04:03 +01:00
Ushie
dfeca09d00 ci: Switch to using crowdin.yml to specify filename 2026-01-07 23:50:57 +03:00
Ushie
44c06e2197 ci: Use a clearer file name for source file to display in Crowdin 2026-01-07 23:43:40 +03:00
Ushie
df31b39cc8 feat: Add language settings (#2913) 2026-01-07 22:54:48 +03:00
semantic-release-bot
25d82e869c chore: Release v1.26.0-dev.17 [skip ci]
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)

### Bug Fixes

* allow updating patches on metered networks ([9d9a0e8](9d9a0e81db))
2026-01-06 21:45:09 +00:00
Ax333l
9d9a0e81db fix: allow updating patches on metered networks 2026-01-06 22:37:25 +01:00
42 changed files with 525 additions and 193 deletions

43
.github/workflows/pull_strings.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Pull strings
on:
schedule:
- cron: "0 0 * * 0"
workflow_dispatch:
jobs:
pull:
name: Pull strings
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: dev
clean: true
- name: Pull strings
uses: crowdin/github-action@v2
with:
config: crowdin.yml
upload_sources: false
download_translations: true
skip_ref_checkout: true
localization_branch_name: feat/translations
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Open pull request
if: github.event_name == 'workflow_dispatch'
uses: repo-sync/pull-request@v2
with:
source_branch: feat/translations
destination_branch: dev
pr_title: "chore: Sync translations"
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"

26
.github/workflows/push_strings.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Push strings
on:
workflow_dispatch:
push:
branches:
- dev
paths:
- app/src/main/res/values/strings.xml
jobs:
push:
name: Push strings
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Push strings
uses: crowdin/github-action@v2
with:
config: crowdin.yml
upload_sources: true
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -24,14 +24,6 @@ public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/
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 static final field $stable I
}
@@ -85,14 +77,6 @@ public final class app/revanced/manager/plugin/downloader/Package : android/os/P
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 {
}
@@ -159,14 +143,6 @@ public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEve
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;

View File

@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
@@ -17,9 +19,16 @@ dependencies {
implementation(libs.appcompat)
}
kotlin {
jvmToolchain(17)
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
android {
namespace = "app.revanced.manager.plugin.downloader"
compileSdk = 35
compileSdk = 36
defaultConfig {
minSdk = 26
@@ -42,10 +51,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
aidl = true
}

View File

@@ -1,3 +1,36 @@
# app [1.26.0-dev.20](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.19...v1.26.0-dev.20) (2026-01-09)
### Bug Fixes
* Save FAB freaking out in select patches screen ([4c0b6b0](https://github.com/ReVanced/revanced-manager/commit/4c0b6b02e95a8d6f655bcf5c25493b1f9a4a4dcd))
# app [1.26.0-dev.19](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.18...v1.26.0-dev.19) (2026-01-08)
### Bug Fixes
* **locales:** use buildconfig instead of generating kt file ([72b1db9](https://github.com/ReVanced/revanced-manager/commit/72b1db9a2f33ab5d5fffd8ba83c05901eff19bea))
# app [1.26.0-dev.18](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.17...v1.26.0-dev.18) (2026-01-08)
### Bug Fixes
* Prevent trailing comma when no locales are generated ([b16931c](https://github.com/ReVanced/revanced-manager/commit/b16931ca79d5ce4d17c75f6dd3bf6f976b8ff7be))
### Features
* Add language settings ([#2913](https://github.com/ReVanced/revanced-manager/issues/2913)) ([df31b39](https://github.com/ReVanced/revanced-manager/commit/df31b39cc8c1fbf00bc3301468e8e7e4b283caf2))
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
### Bug Fixes
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)

View File

@@ -1,4 +1,7 @@
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
import com.mikepenz.aboutlibraries.plugin.DuplicateRule
import io.github.z4kn4fein.semver.toVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import kotlin.random.Random
plugins {
@@ -9,6 +12,7 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.devtools)
alias(libs.plugins.about.libraries)
alias(libs.plugins.about.libraries.android)
signing
}
@@ -81,7 +85,8 @@ dependencies {
implementation(libs.koin.workmanager)
// Licenses
implementation(libs.about.libraries)
implementation(libs.about.libraries.core)
implementation(libs.about.libraries.m3)
// Ktor
implementation(libs.ktor.core)
@@ -126,7 +131,7 @@ buildscript {
android {
namespace = "app.revanced.manager"
compileSdk = 35
compileSdk = 36
buildToolsVersion = "35.0.1"
defaultConfig {
@@ -143,13 +148,25 @@ android {
(preRelease?.substringAfterLast('.')?.toInt() ?: 99)
}
vectorDrawables.useSupportLibrary = true
val resDir = file("src/main/res")
val locales = resDir.listFiles()
.orEmpty()
//noinspection WrongGradleMethod
.filter { it.isDirectory && it.name.matches(Regex("values-[a-z]{2}(-r[A-Z]{2})?")) }
//noinspection WrongGradleMethod
.map { it.name.removePrefix("values-").replace("-r", "-") }
.sorted()
//noinspection WrongGradleMethod
.joinToString(prefix = "{", separator = ",", postfix = "}") { "\"$it\"" }
buildConfigField("String[]", "SUPPORTED_LOCALES", locales)
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager (Debug)")
isPseudoLocalesEnabled = true
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
}
@@ -221,20 +238,14 @@ android {
arg("room.schemaLocation", "$projectDir/schemas")
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
aidl = true
buildConfig = true
}
android {
androidResources {
generateLocaleConfig = true
}
androidResources {
generateLocaleConfig = true
}
externalNativeBuild {
@@ -247,6 +258,18 @@ android {
kotlin {
jvmToolchain(17)
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
aboutLibraries {
library {
// Enable the duplication mode, allows to merge, or link dependencies which relate
duplicationMode = DuplicateMode.MERGE
// Configure the duplication rule, to match "duplicates" with
duplicationRule = DuplicateRule.EXACT
}
}
tasks {

View File

@@ -1 +1 @@
version = 1.26.0-dev.16
version = 1.26.0-dev.20

View File

@@ -3,11 +3,11 @@ package app.revanced.manager
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
@@ -59,11 +59,10 @@ import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.compose.navigation.koinNavViewModel
import org.koin.core.parameter.parametersOf
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() {
class MainActivity : AppCompatActivity() {
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -185,7 +184,7 @@ private fun ReVancedManager(vm: MainViewModel) {
val data =
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
val viewModel =
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
koinViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
parametersOf(data)
}
@@ -226,7 +225,7 @@ private fun ReVancedManager(vm: MainViewModel) {
composable<SelectedApplicationInfo.PatchesSelector> {
val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
@@ -243,7 +242,7 @@ private fun ReVancedManager(vm: MainViewModel) {
composable<SelectedApplicationInfo.RequiredOptions> {
val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)

View File

@@ -15,5 +15,5 @@ class NetworkInfo(app: Application) {
/**
* Returns true if it is safe to download large files.
*/
fun isSafe() = isConnected() && isUnmetered()
fun isSafe(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered())
}

View File

@@ -1,7 +1,7 @@
package app.revanced.manager.di
import app.revanced.manager.ui.viewmodel.*
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.*
import org.koin.dsl.module
val viewModelModule = module {

View File

@@ -34,4 +34,6 @@ class PreferencesManager(
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
}

View File

@@ -286,28 +286,29 @@ class PatchBundleRepository(
State(sources.toPersistentMap(), info.toPersistentMap())
}
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
try {
createStream().use { patches -> replace(patches) }
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e(tag, "Got exception while importing bundle", e)
withContext(Dispatchers.Main) {
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
suspend fun createLocal(createStream: suspend () -> InputStream) =
dispatchAction("Add bundle") {
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
try {
createStream().use { patches -> replace(patches) }
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e(tag, "Got exception while importing bundle", e)
withContext(Dispatchers.Main) {
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
}
deleteLocalFile()
}
deleteLocalFile()
}
}
doReload()
}
doReload()
}
suspend fun createRemote(url: String, autoUpdate: Boolean) =
dispatchAction("Add bundle ($url)") { state ->
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
update(src)
update(src, force = true)
state.copy(sources = state.sources.put(src.uid, src))
}
@@ -329,32 +330,38 @@ class PatchBundleRepository(
state.copy(sources = state.sources.put(uid, newSrc))
}
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
suspend fun update(
vararg sources: RemotePatchBundle,
showToast: Boolean = false,
force: Boolean = false
) {
val uids = sources.map { it.uid }.toSet()
store.dispatch(Update(showToast = showToast) { it.uid in uids })
store.dispatch(Update(showToast = showToast, force = force) { it.uid in uids })
}
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true))
/**
* Updates all bundles that should be automatically updated.
*/
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
suspend fun updateCheck() =
store.dispatch(Update(force = prefs.allowMeteredNetworks.get()) { it.autoUpdate })
private inner class Update(
private val force: Boolean = false,
private val redownload: Boolean = false,
private val showToast: Boolean = false,
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
) : Action<State> {
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
override fun toString() = if (redownload) "Redownload remote bundles" else "Update check"
override suspend fun ActionContext.execute(
current: State
) = coroutineScope {
if (!networkInfo.isSafe()) {
if (!networkInfo.isSafe(force)) {
Log.d(tag, "Skipping update check because the network is down or metered.")
return@coroutineScope current
}
@@ -367,7 +374,7 @@ class PatchBundleRepository(
Log.d(tag, "Updating patch bundle: ${it.name}")
val newVersion = with(it) {
if (force) downloadLatest() else update()
if (redownload) downloadLatest() else update()
} ?: return@async null
it to newVersion

View File

@@ -16,8 +16,11 @@ import io.ktor.http.isSuccess
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.core.isNotEmpty
import io.ktor.utils.io.core.readBytes
import io.ktor.utils.io.exhausted
import io.ktor.utils.io.readRemaining
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.io.asSink
import kotlinx.serialization.json.Json
import java.io.File
import java.io.OutputStream
@@ -69,14 +72,12 @@ class HttpService(
) {
http.prepareGet(builder).execute { httpResponse ->
if (httpResponse.status.isSuccess()) {
val channel: ByteReadChannel = httpResponse.body()
withContext(Dispatchers.IO) {
while (!channel.isClosedForRead) {
val channel: ByteReadChannel = httpResponse.body()
val sink = outputStream.asSink()
while (!channel.exhausted()) {
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (packet.isNotEmpty) {
val bytes = packet.readBytes()
outputStream.write(bytes)
}
packet.transferTo(sink)
}
}

View File

@@ -16,7 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import coil.compose.AsyncImage
import io.github.fornewid.placeholder.material3.placeholder
import com.eygraber.compose.placeholder.material3.placeholder
@Composable
fun AppIcon(

View File

@@ -16,7 +16,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import app.revanced.manager.R
import io.github.fornewid.placeholder.material3.placeholder
import com.eygraber.compose.placeholder.material3.placeholder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View File

@@ -14,7 +14,7 @@ fun LoadingIndicator(
progress: () -> Float? = { null },
color: Color = ProgressIndicatorDefaults.circularColor,
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
trackColor: Color = ProgressIndicatorDefaults.circularTrackColor,
trackColor: Color = ProgressIndicatorDefaults.circularIndeterminateTrackColor,
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
) {
progress()?.let {

View File

@@ -18,8 +18,6 @@ fun Markdown(
colors = markdownColor(
text = MaterialTheme.colorScheme.onSurfaceVariant,
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
codeText = MaterialTheme.colorScheme.onSecondaryContainer,
linkText = MaterialTheme.colorScheme.primary
),
typography = markdownTypography(
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),

View File

@@ -29,7 +29,8 @@ fun SearchBar(
) {
val colors = SearchBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
dividerColor = MaterialTheme.colorScheme.outline
dividerColor = MaterialTheme.colorScheme.outline,
inputFieldColors = SearchBarDefaults.inputFieldColors()
)
val keyboardController = LocalSoftwareKeyboardController.current

View File

@@ -31,7 +31,8 @@ fun SearchView(
) {
val colors = SearchBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
dividerColor = MaterialTheme.colorScheme.outline
dividerColor = MaterialTheme.colorScheme.outline,
inputFieldColors = SearchBarDefaults.inputFieldColors()
)
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current

View File

@@ -6,7 +6,7 @@ import app.revanced.manager.R
import app.revanced.manager.patcher.StepId
import kotlinx.parcelize.Parcelize
enum class StepCategory(@StringRes val displayName: Int) {
enum class StepCategory(@param:StringRes val displayName: Int) {
PREPARING(R.string.patcher_step_group_preparing),
PATCHING(R.string.patcher_step_group_patching),
SAVING(R.string.patcher_step_group_saving)

View File

@@ -36,7 +36,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TabRow
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
@@ -53,6 +53,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -100,6 +101,7 @@ fun DashboardScreen(
false
)
val androidContext = LocalContext.current
val resources = LocalResources.current
val composableScope = rememberCoroutineScope()
val pagerState = rememberPagerState(
initialPage = DashboardPage.DASHBOARD.ordinal,
@@ -234,7 +236,7 @@ fun DashboardScreen(
when (pagerState.currentPage) {
DashboardPage.DASHBOARD.ordinal -> {
if (availablePatches < 1) {
androidContext.toast(androidContext.getString(R.string.no_patch_found))
androidContext.toast(resources.getString(R.string.no_patch_found))
composableScope.launch {
pagerState.animateScrollToPage(
DashboardPage.BUNDLES.ordinal
@@ -259,7 +261,7 @@ fun DashboardScreen(
}
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
TabRow(
SecondaryTabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) {

View File

@@ -40,6 +40,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
@@ -69,6 +70,7 @@ fun PatcherScreen(
}
val context = LocalContext.current
val resources = LocalResources.current
val exportApkLauncher =
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
@@ -79,7 +81,7 @@ fun PatcherScreen(
fun onPageBack() = when {
patcherSucceeded == null -> showDismissConfirmationDialog = true
viewModel.isInstalling -> context.toast(context.getString(R.string.patcher_install_in_progress))
viewModel.isInstalling -> context.toast(resources.getString(R.string.patcher_install_in_progress))
else -> onLeave()
}

View File

@@ -40,7 +40,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SecondaryScrollableTabRow
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -49,10 +49,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -81,9 +83,11 @@ import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, FlowPreview::class)
@Composable
fun PatchesSelectorScreen(
onSave: (PatchSelection?, Options) -> Unit,
@@ -231,7 +235,8 @@ fun PatchesSelectorScreen(
viewModel.selectionWarningEnabled -> showSelectionWarning = true
// Show universal warning if universal patch is selected and the toggle is off
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning =
true
// Toggle the patch otherwise
else -> viewModel.togglePatch(uid, patch)
@@ -360,6 +365,21 @@ fun PatchesSelectorScreen(
) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
}
val isScrollingUp =
patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp()
val expanded by produceState(true, isScrollingUp) {
val state = isScrollingUp ?: return@produceState
value = state.value
// Use snapshotFlow and sample to prevent the value from changing too often.
snapshotFlow { state.value }
.sample(333L)
.collect {
value = it
}
}
HapticExtendedFloatingActionButton(
text = {
Text(
@@ -375,8 +395,7 @@ fun PatchesSelectorScreen(
contentDescription = stringResource(R.string.save)
)
},
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true,
expanded = expanded,
onClick = {
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
}
@@ -392,7 +411,7 @@ fun PatchesSelectorScreen(
.padding(top = 16.dp)
) {
if (bundles.size > 1) {
ScrollableTabRow(
SecondaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) {

View File

@@ -13,7 +13,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SecondaryScrollableTabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@@ -106,7 +106,7 @@ fun RequiredOptionsScreen(
.padding(paddingValues)
) {
if (list.isEmpty()) return@Column
else if (list.size > 1) ScrollableTabRow(
else if (list.size > 1) SecondaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) {

View File

@@ -32,6 +32,7 @@ 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.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -68,6 +69,7 @@ fun SelectedAppInfoScreen(
vm: SelectedAppInfoViewModel
) {
val context = LocalContext.current
val resources = LocalResources.current
val networkInfo = koinInject<NetworkInfo>()
val networkConnected = remember { networkInfo.isConnected() }
val networkMetered = remember { !networkInfo.isUnmetered() }
@@ -118,7 +120,7 @@ fun SelectedAppInfoScreen(
},
onClick = patchClick@{
if (selectedPatchCount == 0) {
context.toast(context.getString(R.string.no_patches_selected))
context.toast(resources.getString(R.string.no_patches_selected))
return@patchClick
}

View File

@@ -22,8 +22,8 @@ import app.revanced.manager.ui.model.navigation.Settings
import org.koin.compose.koinInject
private data class Section(
@StringRes val name: Int,
@StringRes val description: Int,
@param:StringRes val name: Int,
@param:StringRes val description: Int,
val image: ImageVector,
val destination: Settings.Destination,
)

View File

@@ -1,5 +1,6 @@
package app.revanced.manager.ui.screen.settings
import android.annotation.SuppressLint
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
@@ -40,6 +41,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.semantics
@@ -67,8 +69,9 @@ fun AboutSettingsScreen(
viewModel: AboutViewModel = koinViewModel()
) {
val context = LocalContext.current
val resources = LocalResources.current
// painterResource() is broken on release builds for some reason.
val icon = rememberDrawablePainter(drawable = remember {
val icon = rememberDrawablePainter(drawable = remember(resources) {
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
})
@@ -76,7 +79,7 @@ fun AboutSettingsScreen(
viewModel.socials.partition(ReVancedSocial::preferred)
}
val preferredSocialButtons = remember(preferredSocials, viewModel.donate, viewModel.contact) {
val preferredSocialButtons = remember(resources, preferredSocials, viewModel.donate, viewModel.contact) {
preferredSocials.map {
Triple(
getSocialIcon(it.name),
@@ -89,7 +92,7 @@ fun AboutSettingsScreen(
viewModel.donate?.let {
Triple(
Icons.Outlined.FavoriteBorder,
context.getString(R.string.donate),
resources.getString(R.string.donate),
third = {
context.openUrl(it)
}
@@ -98,7 +101,7 @@ fun AboutSettingsScreen(
viewModel.contact?.let {
Triple(
Icons.Outlined.MailOutline,
context.getString(R.string.contact),
resources.getString(R.string.contact),
third = {
context.openUrl("mailto:$it")
}
@@ -131,7 +134,7 @@ fun AboutSettingsScreen(
stringResource(R.string.contributors_description),
third = nav@{
if (!viewModel.isConnected) {
context.toast(context.getString(R.string.no_network_toast))
context.toast(resources.getString(R.string.no_network_toast))
return@nav
}
@@ -153,7 +156,7 @@ fun AboutSettingsScreen(
LaunchedEffect(developerTaps) {
if (developerTaps == 0) return@LaunchedEffect
if (showDeveloperSettings) {
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_already_enabled))
developerTaps = 0
return@LaunchedEffect
}
@@ -161,7 +164,7 @@ fun AboutSettingsScreen(
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
if (remaining > 0) {
snackbarHostState.showSnackbar(
context.getString(
resources.getString(
R.string.developer_options_taps,
remaining
),
@@ -169,7 +172,7 @@ fun AboutSettingsScreen(
)
} else if (remaining == 0) {
viewModel.showDeveloperSettings.update(true)
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_enabled))
}
// Reset the counter

View File

@@ -38,6 +38,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -64,9 +65,10 @@ fun AdvancedSettingsScreen(
viewModel: AdvancedSettingsViewModel = koinViewModel()
) {
val context = LocalContext.current
val memoryLimit = remember {
val resources = LocalResources.current
val memoryLimit = remember(resources) {
val activityManager = context.getSystemService<ActivityManager>()!!
context.getString(
resources.getString(
R.string.device_memory_limit_format,
activityManager.memoryClass,
activityManager.largeMemoryClass
@@ -183,7 +185,7 @@ fun AdvancedSettingsScreen(
ClipData.newPlainText("Device Information", deviceContent)
)
context.toast(context.getString(R.string.toast_copied_to_clipboard))
context.toast(resources.getString(R.string.toast_copied_to_clipboard))
}.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
),
headlineContent = stringResource(R.string.about_device),

View File

@@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
@@ -19,6 +21,7 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -38,6 +41,7 @@ import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -48,6 +52,7 @@ fun GeneralSettingsScreen(
val prefs = viewModel.prefs
val coroutineScope = viewModel.viewModelScope
var showThemePicker by rememberSaveable { mutableStateOf(false) }
var showLanguagePicker by rememberSaveable { mutableStateOf(false) }
if (showThemePicker) {
ThemePicker(
@@ -55,6 +60,17 @@ fun GeneralSettingsScreen(
onConfirm = { viewModel.setTheme(it) }
)
}
if (showLanguagePicker) {
LanguagePicker(
supportedLocales = viewModel.getSupportedLocales(),
currentLocale = viewModel.getCurrentLocale(),
onDismiss = { showLanguagePicker = false },
onConfirm = { viewModel.setLocale(it) },
getDisplayName = { viewModel.getLocaleDisplayName(it) }
)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
@@ -74,6 +90,24 @@ fun GeneralSettingsScreen(
) {
GroupHeader(stringResource(R.string.appearance))
val currentLocale = viewModel.getCurrentLocale()
val currentLanguageDisplay = remember(currentLocale) {
currentLocale?.let { viewModel.getLocaleDisplayName(it) }
}
SettingsListItem(
modifier = Modifier.clickable { showLanguagePicker = true },
headlineContent = stringResource(R.string.language),
supportingContent = stringResource(R.string.language_description),
trailingContent = {
FilledTonalButton(onClick = { showLanguagePicker = true }) {
Text(
currentLanguageDisplay
?: stringResource(R.string.language_system_default)
)
}
}
)
val theme by prefs.theme.getAsState()
SettingsListItem(
modifier = Modifier.clickable { showThemePicker = true },
@@ -105,6 +139,14 @@ fun GeneralSettingsScreen(
description = R.string.pure_black_theme_description
)
}
GroupHeader(stringResource(R.string.networking))
BooleanItem(
preference = prefs.allowMeteredNetworks,
coroutineScope = coroutineScope,
headline = R.string.allow_metered_networks,
description = R.string.allow_metered_networks_description
)
}
}
}
@@ -148,4 +190,64 @@ private fun ThemePicker(
}
}
)
}
@Composable
private fun LanguagePicker(
supportedLocales: List<Locale>,
currentLocale: Locale?,
onDismiss: () -> Unit,
onConfirm: (Locale?) -> Unit,
getDisplayName: (Locale) -> String
) {
var selectedLocale by remember { mutableStateOf(currentLocale) }
val systemDefaultString = stringResource(R.string.language_system_default)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.language)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedLocale = null },
verticalAlignment = Alignment.CenterVertically
) {
HapticRadioButton(
selected = selectedLocale == null,
onClick = { selectedLocale = null }
)
Text(systemDefaultString)
}
supportedLocales.forEach { locale ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedLocale = locale },
verticalAlignment = Alignment.CenterVertically
) {
HapticRadioButton(
selected = selectedLocale == locale,
onClick = { selectedLocale = locale }
)
Text(getDisplayName(locale))
}
}
}
},
confirmButton = {
TextButton(
onClick = {
onConfirm(selectedLocale)
onDismiss()
}
) {
Text(stringResource(R.string.apply))
}
}
)
}

View File

@@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -64,6 +65,7 @@ fun ImportExportSettingsScreen(
vm: ImportExportViewModel = koinViewModel()
) {
val context = LocalContext.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope()
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
@@ -108,7 +110,7 @@ fun ImportExportSettingsScreen(
vm.viewModelScope.launch {
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
val result = vm.tryKeystoreImport(alias, pass)
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
if (!result) context.toast(resources.getString(R.string.import_keystore_wrong_credentials))
}
}
}
@@ -166,7 +168,7 @@ fun ImportExportSettingsScreen(
GroupItem(
onClick = {
if (!vm.canExport()) {
context.toast(context.getString(R.string.export_keystore_unavailable))
context.toast(resources.getString(R.string.export_keystore_unavailable))
return@GroupItem
}
exportKeystoreLauncher.launch("Manager.keystore")

View File

@@ -7,15 +7,18 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.Scrollbar
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import com.mikepenz.aboutlibraries.ui.compose.libraryColors
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.m3.chipColors
import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -33,16 +36,23 @@ fun LicensesSettingsScreen(
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
val lazyListState = rememberLazyListState()
val libraries by produceLibraries(R.raw.aboutlibraries)
val chipColors = LibraryDefaults.chipColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
)
LibrariesContainer(
modifier = Modifier
.fillMaxSize(),
libraries = libraries,
lazyListState = lazyListState,
colors = LibraryDefaults.libraryColors(
backgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
libraryBackgroundColor = MaterialTheme.colorScheme.background,
libraryContentColor = MaterialTheme.colorScheme.onBackground,
versionChipColors = chipColors,
licenseChipColors = chipColors,
fundingChipColors = chipColors,
)
)
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))

View File

@@ -12,6 +12,7 @@ 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.platform.LocalResources
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
@@ -33,6 +34,7 @@ fun UpdatesSettingsScreen(
vm: UpdatesSettingsViewModel = koinViewModel(),
) {
val context = LocalContext.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -57,7 +59,7 @@ fun UpdatesSettingsScreen(
modifier = Modifier.clickable {
coroutineScope.launch {
if (!vm.isConnected) {
context.toast(context.getString(R.string.no_network_toast))
context.toast(resources.getString(R.string.no_network_toast))
return@launch
}
if (vm.checkForUpdates()) onUpdateClick()
@@ -70,7 +72,7 @@ fun UpdatesSettingsScreen(
SettingsListItem(
modifier = Modifier.clickable {
if (!vm.isConnected) {
context.toast(context.getString(R.string.no_network_toast))
context.toast(resources.getString(R.string.no_network_toast))
return@clickable
}
onChangelogClick()

View File

@@ -54,6 +54,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
patchBundleRepository.update(
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
showToast = true,
force = true
)
}
}
@@ -65,7 +66,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
fun update(src: PatchBundleSource) = viewModelScope.launch {
if (src !is RemotePatchBundle) return@launch
patchBundleRepository.update(src, showToast = true)
patchBundleRepository.update(src, showToast = true, force = true)
}
enum class Event {

View File

@@ -1,17 +1,26 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.SupportedLocales
import app.revanced.manager.util.resetListItemColorsCached
import kotlinx.coroutines.launch
import java.util.Locale
class GeneralSettingsViewModel(
private val app: Application,
val prefs: PreferencesManager
) : ViewModel() {
fun setTheme(theme: Theme) = viewModelScope.launch {
prefs.theme.update(theme)
resetListItemColorsCached()
}
fun getSupportedLocales() = SupportedLocales.getSupportedLocales(app)
fun getCurrentLocale() = SupportedLocales.getCurrentLocale()
fun setLocale(locale: Locale?) = SupportedLocales.setLocale(locale)
fun getLocaleDisplayName(locale: Locale) = SupportedLocales.getDisplayName(locale)
}

View File

@@ -36,8 +36,8 @@ import kotlin.io.path.deleteExisting
import kotlin.io.path.inputStream
sealed class ResetDialogState(
@StringRes val titleResId: Int,
@StringRes val descriptionResId: Int,
@param:StringRes val titleResId: Int,
@param:StringRes val descriptionResId: Int,
val onConfirm: () -> Unit,
val dialogOptionName: String? = null
) {

View File

@@ -82,7 +82,7 @@ class UpdateViewModel(
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
val release = releaseInfo!!
withContext(Dispatchers.IO) {
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
if (!networkInfo.isSafe(false) && !ignoreInternetCheck) {
showInternetCheckDialog = true
} else {
state = State.DOWNLOADING
@@ -90,8 +90,10 @@ class UpdateViewModel(
http.download(location) {
url(release.downloadUrl)
onDownload { bytesSentTotal, contentLength ->
downloadedSize = bytesSentTotal
totalSize = contentLength
withContext(Dispatchers.Main) {
downloadedSize = bytesSentTotal
contentLength?.let { totalSize = it }
}
}
}
installUpdate()

View File

@@ -0,0 +1,40 @@
package app.revanced.manager.util
import android.app.LocaleConfig
import android.content.Context
import android.os.Build
import android.os.LocaleList
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import app.revanced.manager.BuildConfig
import java.util.Locale
object SupportedLocales {
fun getSupportedLocales(context: Context): List<Locale> {
var result: List<Locale>? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) result = runCatching {
LocaleConfig(context).supportedLocales?.toList()
}.getOrNull()
return result ?: generated
}
fun getCurrentLocale(): Locale? =
AppCompatDelegate.getApplicationLocales().takeIf { !it.isEmpty }?.get(0)
fun setLocale(locale: Locale?) = AppCompatDelegate.setApplicationLocales(
locale?.let { LocaleListCompat.create(it) } ?: LocaleListCompat.getEmptyLocaleList()
)
fun getDisplayName(locale: Locale) =
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
private fun LocaleList.toList() = (0 until size()).map { get(it) }
private val generated by lazy {
listOf(
Locale.ENGLISH,
*BuildConfig.SUPPORTED_LOCALES.map(Locale::forLanguageTag).toTypedArray()
)
}
}

View File

@@ -15,12 +15,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.produceState
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
@@ -43,7 +41,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.format.MonthNames
@@ -51,9 +48,11 @@ import kotlinx.datetime.format.char
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import java.util.Locale
import kotlin.math.abs
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.time.Clock
typealias PatchSelection = Map<Int, Set<String>>
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
@@ -169,7 +168,7 @@ fun LocalDateTime.relativeTime(context: Context): String {
else -> LocalDateTime.Format {
monthName(MonthNames.ENGLISH_ABBREVIATED)
char(' ')
dayOfMonth()
day()
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
chars(", ")
year()
@@ -196,7 +195,12 @@ val transparentListItemColors
.also { transparentListItemColorsCached = it }
@Composable
fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = Lifecycle.State.STARTED, block: suspend (T) -> Unit) {
fun <T> EventEffect(
flow: Flow<T>,
vararg keys: Any?,
state: Lifecycle.State = Lifecycle.State.STARTED,
block: suspend (T) -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current
val currentBlock by rememberUpdatedState(block)
@@ -212,40 +216,36 @@ fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = L
const val isScrollingUpSensitivity = 10
@Composable
fun LazyListState.isScrollingUp(): State<Boolean> {
return remember(this) {
var previousIndex by mutableIntStateOf(firstVisibleItemIndex)
var previousScrollOffset by mutableIntStateOf(firstVisibleItemScrollOffset)
fun LazyListState.isScrollingUp() = produceState(true, this) {
var previousIndex = firstVisibleItemIndex
var previousScrollOffset = firstVisibleItemScrollOffset
derivedStateOf {
val indexChanged = previousIndex != firstVisibleItemIndex
val offsetChanged =
kotlin.math.abs(previousScrollOffset - firstVisibleItemScrollOffset) > isScrollingUpSensitivity
snapshotFlow {
firstVisibleItemIndex to firstVisibleItemScrollOffset
}.collect { (index, scrollOffset) ->
val indexChanged = previousIndex != index
val offsetChanged = abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity
if (indexChanged) {
previousIndex > firstVisibleItemIndex
} else if (offsetChanged) {
previousScrollOffset > firstVisibleItemScrollOffset
} else {
true
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
value = when {
indexChanged -> previousIndex > index
offsetChanged -> previousScrollOffset > scrollOffset
else -> value
}
previousIndex = index
previousScrollOffset = scrollOffset
}
}
// TODO: support sensitivity
@Composable
fun ScrollState.isScrollingUp(): State<Boolean> {
return remember(this) {
var previousScrollOffset by mutableIntStateOf(value)
derivedStateOf {
(previousScrollOffset >= value).also {
previousScrollOffset = value
}
fun ScrollState.isScrollingUp() = produceState(true, this) {
var previousScrollOffset = this@isScrollingUp.value
snapshotFlow { this@isScrollingUp.value }.collect { scrollOffset ->
if (abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity) {
value = previousScrollOffset >= scrollOffset
}
previousScrollOffset = scrollOffset
}
}

View File

@@ -14,7 +14,7 @@ Second \"item\" text"</string>
-->
<resources>
<string name="app_name">ReVanced Manager</string>
<string name="patcher">Patcher</string>
<string name="patcher">Patcher test</string>
<string name="patches">Patches</string>
<string name="cli">CLI</string>
<string name="manager">Manager</string>
@@ -83,7 +83,7 @@ Second \"item\" text"</string>
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
<string name="general">General</string>
<string name="general_description">Theme, dynamic color</string>
<string name="general_description">Language, theme, dynamic color</string>
<string name="updates">Updates</string>
<string name="updates_description">Check for updates and view changelogs</string>
<string name="downloads">Downloads</string>
@@ -104,6 +104,9 @@ Second \"item\" text"</string>
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
<string name="theme">Theme</string>
<string name="theme_description">Choose between light or dark theme</string>
<string name="language">Language</string>
<string name="language_description">Choose the app display language</string>
<string name="language_system_default">System default</string>
<string name="safeguards">Safeguards</string>
<string name="patch_compat_check">Disable version compatibility check</string>
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
@@ -217,6 +220,10 @@ You will not be able to update the previously installed apps from this source."<
<string name="light">Light</string>
<string name="dark">Dark</string>
<string name="appearance">Appearance</string>
<string name="networking">Networking</string>
<string name="allow_metered_networks">Allow metered networks</string>
<string name="allow_metered_networks_description">Permits automatic updates on metered networks.
The application might still warn about metered networks for manual operations.</string>
<string name="downloaded_apps">Downloaded apps</string>
<string name="process_runtime">Run Patcher in another process (experimental)</string>
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>

View File

@@ -6,5 +6,6 @@ plugins {
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.about.libraries) apply false
alias(libs.plugins.about.libraries.android) apply false
alias(libs.plugins.compose.compiler) apply false
}

9
crowdin.yml Normal file
View File

@@ -0,0 +1,9 @@
project_id_env: "CROWDIN_PROJECT_ID"
api_token_env: "CROWDIN_PERSONAL_TOKEN"
preserve_hierarchy: true
files:
- source: app/src/main/res/values/strings.xml
dest: manager.xml
translation: app/src/main/res/values-%android_code%/strings.xml
skip_untranslated_strings: true

View File

@@ -1,32 +1,32 @@
[versions]
ktx = "1.16.0"
material3 = "1.3.2"
ui-tooling = "1.8.1"
viewmodel-lifecycle = "2.9.0"
splash-screen = "1.0.1"
activity = "1.10.1"
appcompat = "1.7.0"
preferences-datastore = "1.1.2"
work-runtime = "2.10.1"
compose-bom = "2025.05.00"
navigation = "2.8.6"
accompanist = "0.37.0"
placeholder = "1.1.2"
reorderable = "2.4.3"
serialization = "1.8.0"
collection = "0.3.8"
datetime = "0.6.1"
room-version = "2.7.1"
ktx = "1.17.0"
material3 = "1.4.0"
ui-tooling = "1.10.0"
viewmodel-lifecycle = "2.10.0"
splash-screen = "1.2.0"
activity = "1.12.2"
appcompat = "1.7.1"
preferences-datastore = "1.2.0"
work-runtime = "2.11.0"
compose-bom = "2025.12.01"
navigation = "2.9.6"
accompanist = "0.37.3"
placeholder = "1.0.12"
reorderable = "3.0.0"
serialization = "1.9.0"
collection = "0.4.0"
datetime = "0.7.1"
room-version = "2.8.4"
revanced-patcher = "21.0.0"
revanced-library = "3.0.2"
koin = "3.5.3"
ktor = "2.3.9"
markdown-renderer = "0.30.0"
koin = "4.1.1"
ktor = "3.3.3"
markdown-renderer = "0.39.0"
fading-edges = "1.0.4"
kotlin = "2.1.10"
android-gradle-plugin = "8.9.1"
dev-tools-gradle-plugin = "2.1.10-1.0.29"
about-libraries-gradle-plugin = "12.1.2"
kotlin = "2.3.0"
android-gradle-plugin = "8.13.2"
dev-tools-gradle-plugin = "2.3.4"
about-libraries = "13.2.1"
coil = "2.7.0"
app-icon-loader-coil = "1.5.0"
libsu = "6.0.0"
@@ -34,10 +34,10 @@ scrollbars = "1.0.4"
enumutil = "1.1.1"
compose-icons = "1.2.4"
kotlin-process = "1.5.1"
hidden-api-stub = "4.3.3"
binary-compatibility-validator = "0.17.0"
hidden-api-stub = "4.4.0"
binary-compatibility-validator = "0.18.1"
semver-parser = "3.0.0"
ackpine = "0.18.5"
ackpine = "0.19.1"
[libraries]
# AndroidX Core
@@ -68,7 +68,7 @@ coil-appiconloader = { group = "me.zhanghai.android.appiconloader", name = "appi
accompanist-drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "accompanist" }
# Placeholder
placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-material3", version.ref = "placeholder"}
placeholder-material3 = { group = "com.eygraber", name = "compose-placeholder-material3", version.ref = "placeholder" }
# Kotlinx
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
@@ -91,7 +91,8 @@ koin-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-comp
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
# About Libraries
about-libraries = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" }
about-libraries-core = { group = "com.mikepenz", name = "aboutlibraries-compose-core", version.ref = "about-libraries" }
about-libraries-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "about-libraries" }
# Ktor
ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
@@ -146,5 +147,6 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", 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" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries" }
about-libraries-android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "about-libraries" }
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }