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
61 changed files with 1117 additions and 1253 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 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/Downloader {
public static final field $stable I 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 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 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 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 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 finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun load (Ljava/lang/String;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 { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
@@ -17,9 +19,16 @@ dependencies {
implementation(libs.appcompat) implementation(libs.appcompat)
} }
kotlin {
jvmToolchain(17)
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
android { android {
namespace = "app.revanced.manager.plugin.downloader" namespace = "app.revanced.manager.plugin.downloader"
compileSdk = 35 compileSdk = 36
defaultConfig { defaultConfig {
minSdk = 26 minSdk = 26
@@ -42,10 +51,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions {
jvmTarget = "17"
}
buildFeatures { buildFeatures {
aidl = true 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) # 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 io.github.z4kn4fein.semver.toVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import kotlin.random.Random import kotlin.random.Random
plugins { plugins {
@@ -9,6 +12,7 @@ plugins {
alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.compiler)
alias(libs.plugins.devtools) alias(libs.plugins.devtools)
alias(libs.plugins.about.libraries) alias(libs.plugins.about.libraries)
alias(libs.plugins.about.libraries.android)
signing signing
} }
@@ -81,7 +85,8 @@ dependencies {
implementation(libs.koin.workmanager) implementation(libs.koin.workmanager)
// Licenses // Licenses
implementation(libs.about.libraries) implementation(libs.about.libraries.core)
implementation(libs.about.libraries.m3)
// Ktor // Ktor
implementation(libs.ktor.core) implementation(libs.ktor.core)
@@ -126,7 +131,7 @@ buildscript {
android { android {
namespace = "app.revanced.manager" namespace = "app.revanced.manager"
compileSdk = 35 compileSdk = 36
buildToolsVersion = "35.0.1" buildToolsVersion = "35.0.1"
defaultConfig { defaultConfig {
@@ -143,13 +148,25 @@ android {
(preRelease?.substringAfterLast('.')?.toInt() ?: 99) (preRelease?.substringAfterLast('.')?.toInt() ?: 99)
} }
vectorDrawables.useSupportLibrary = true 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 { buildTypes {
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager (Debug)") resValue("string", "app_name", "ReVanced Manager (Debug)")
isPseudoLocalesEnabled = true
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L") buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
} }
@@ -221,20 +238,14 @@ android {
arg("room.schemaLocation", "$projectDir/schemas") arg("room.schemaLocation", "$projectDir/schemas")
} }
kotlinOptions {
jvmTarget = "17"
}
buildFeatures { buildFeatures {
compose = true compose = true
aidl = true aidl = true
buildConfig = true buildConfig = true
} }
android { androidResources {
androidResources { generateLocaleConfig = true
generateLocaleConfig = true
}
} }
externalNativeBuild { externalNativeBuild {
@@ -247,6 +258,18 @@ android {
kotlin { kotlin {
jvmToolchain(17) 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 { 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.content.ActivityNotFoundException
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
@@ -30,7 +30,7 @@ import app.revanced.manager.ui.model.navigation.ComplexParameter
import app.revanced.manager.ui.model.navigation.Dashboard import app.revanced.manager.ui.model.navigation.Dashboard
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedAppInfo import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.ui.model.navigation.Settings import app.revanced.manager.ui.model.navigation.Settings
import app.revanced.manager.ui.model.navigation.Update import app.revanced.manager.ui.model.navigation.Update
import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.AppSelectorScreen
@@ -41,9 +41,7 @@ import app.revanced.manager.ui.screen.PatchesSelectorScreen
import app.revanced.manager.ui.screen.RequiredOptionsScreen import app.revanced.manager.ui.screen.RequiredOptionsScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.screen.SourceSelectorScreen
import app.revanced.manager.ui.screen.UpdateScreen import app.revanced.manager.ui.screen.UpdateScreen
import app.revanced.manager.ui.screen.VersionSelectorScreen
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.ContributorSettingsScreen import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
@@ -61,11 +59,10 @@ import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect import app.revanced.manager.util.EventEffect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.compose.navigation.koinNavViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() { class MainActivity : AppCompatActivity() {
@ExperimentalAnimationApi @ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -97,16 +94,23 @@ class MainActivity : ComponentActivity() {
dynamicColor = dynamicColor, dynamicColor = dynamicColor,
pureBlackTheme = pureBlackTheme pureBlackTheme = pureBlackTheme
) { ) {
ReVancedManager() ReVancedManager(vm)
} }
} }
} }
} }
@Composable @Composable
private fun ReVancedManager() { private fun ReVancedManager(vm: MainViewModel) {
val navController = rememberNavController() val navController = rememberNavController()
EventEffect(vm.appSelectFlow) { app ->
navController.navigateComplex(
SelectedApplicationInfo,
SelectedApplicationInfo.ViewModelParams(app)
)
}
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Dashboard, startDestination = Dashboard,
@@ -137,12 +141,7 @@ private fun ReVancedManager() {
val data = it.toRoute<InstalledApplicationInfo>() val data = it.toRoute<InstalledApplicationInfo>()
InstalledAppInfoScreen( InstalledAppInfoScreen(
onPatchClick = { packageName -> onPatchClick = vm::selectApp,
navController.navigateComplex(
SelectedAppInfo,
SelectedAppInfo.ViewModelParams(packageName)
)
},
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
viewModel = koinViewModel { parametersOf(data.packageName) } viewModel = koinViewModel { parametersOf(data.packageName) }
) )
@@ -150,20 +149,8 @@ private fun ReVancedManager() {
composable<AppSelector> { composable<AppSelector> {
AppSelectorScreen( AppSelectorScreen(
onSelect = { packageName -> onSelect = vm::selectApp,
navController.navigateComplex( onStorageSelect = vm::selectApp,
SelectedAppInfo,
SelectedAppInfo.ViewModelParams(packageName)
)
},
onStorageSelect = { packageName, localPath ->
navController.navigateComplex(
SelectedAppInfo,
SelectedAppInfo.ViewModelParams(
packageName, localPath
)
)
},
onBackClick = navController::popBackStack onBackClick = navController::popBackStack
) )
} }
@@ -191,13 +178,13 @@ private fun ReVancedManager() {
) )
} }
navigation<SelectedAppInfo>(startDestination = SelectedAppInfo.Main) { navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
composable<SelectedAppInfo.Main> { composable<SelectedApplicationInfo.Main> {
val parentBackStackEntry = navController.navGraphEntry(it) val parentBackStackEntry = navController.navGraphEntry(it)
val data = val data =
parentBackStackEntry.getComplexArg<SelectedAppInfo.ViewModelParams>() parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
val viewModel = val viewModel =
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) { koinViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
parametersOf(data) parametersOf(data)
} }
@@ -211,47 +198,23 @@ private fun ReVancedManager() {
) )
} }
}, },
onPatchSelectorClick = { packageName, version, patchSelection, options -> onPatchSelectorClick = { app, patches, options ->
navController.navigateComplex( navController.navigateComplex(
SelectedAppInfo.PatchesSelector, SelectedApplicationInfo.PatchesSelector,
SelectedAppInfo.PatchesSelector.ViewModelParams( SelectedApplicationInfo.PatchesSelector.ViewModelParams(
packageName, app,
version, patches,
patchSelection, options
options,
) )
) )
}, },
onRequiredOptions = { packageName, version, patchSelection, options -> onRequiredOptions = { app, patches, options ->
navController.navigateComplex( navController.navigateComplex(
SelectedAppInfo.RequiredOptions, SelectedApplicationInfo.RequiredOptions,
SelectedAppInfo.PatchesSelector.ViewModelParams( SelectedApplicationInfo.PatchesSelector.ViewModelParams(
packageName, app,
version, patches,
patchSelection, options
options,
)
)
},
onVersionClick = { packageName, patchSelection, selectedVersion, local ->
navController.navigateComplex(
SelectedAppInfo.VersionSelector,
SelectedAppInfo.VersionSelector.ViewModelParams(
packageName,
patchSelection,
selectedVersion,
local,
)
)
},
onSourceClick = { packageName, version, selectedSource, local ->
navController.navigateComplex(
SelectedAppInfo.SourceSelector,
SelectedAppInfo.SourceSelector.ViewModelParams(
packageName,
version,
selectedSource,
local,
) )
) )
}, },
@@ -259,10 +222,10 @@ private fun ReVancedManager() {
) )
} }
composable<SelectedAppInfo.PatchesSelector> { composable<SelectedApplicationInfo.PatchesSelector> {
val data = val data =
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>() it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>( val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it) viewModelStoreOwner = navController.navGraphEntry(it)
) )
@@ -276,44 +239,10 @@ private fun ReVancedManager() {
) )
} }
composable<SelectedAppInfo.VersionSelector> { composable<SelectedApplicationInfo.RequiredOptions> {
val data = val data =
it.getComplexArg<SelectedAppInfo.VersionSelector.ViewModelParams>() it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>( val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
VersionSelectorScreen(
onBackClick = navController::popBackStack,
onSave = { version ->
selectedAppInfoVm.updateVersion(version)
navController.popBackStack()
},
viewModel = koinViewModel { parametersOf(data) }
)
}
composable<SelectedAppInfo.SourceSelector> {
val data =
it.getComplexArg<SelectedAppInfo.SourceSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
SourceSelectorScreen(
onBackClick = navController::popBackStack,
onSave = { source ->
selectedAppInfoVm.updateSource(source)
navController.popBackStack()
},
viewModel = koinViewModel { parametersOf(data) }
)
}
composable<SelectedAppInfo.RequiredOptions> {
val data =
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it) viewModelStoreOwner = navController.navGraphEntry(it)
) )

View File

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

View File

@@ -2,6 +2,7 @@ package app.revanced.manager.data.room.apps.downloaded
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -11,9 +12,6 @@ interface DownloadedAppDao {
@Query("SELECT * FROM downloaded_app") @Query("SELECT * FROM downloaded_app")
fun getAllApps(): Flow<List<DownloadedApp>> fun getAllApps(): Flow<List<DownloadedApp>>
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName")
fun get(packageName: String): Flow<List<DownloadedApp>>
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version") @Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
suspend fun get(packageName: String, version: String): DownloadedApp? suspend fun get(packageName: String, version: String): DownloadedApp?

View File

@@ -1,7 +1,7 @@
package app.revanced.manager.di package app.revanced.manager.di
import app.revanced.manager.ui.viewmodel.* import app.revanced.manager.ui.viewmodel.*
import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.*
import org.koin.dsl.module import org.koin.dsl.module
val viewModelModule = module { val viewModelModule = module {
@@ -24,6 +24,4 @@ val viewModelModule = module {
viewModelOf(::InstalledAppInfoViewModel) viewModelOf(::InstalledAppInfoViewModel)
viewModelOf(::UpdatesSettingsViewModel) viewModelOf(::UpdatesSettingsViewModel)
viewModelOf(::BundleListViewModel) viewModelOf(::BundleListViewModel)
viewModelOf(::VersionSelectorViewModel)
viewModelOf(::SourceSelectorViewModel)
} }

View File

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

View File

@@ -30,8 +30,6 @@ class DownloadedAppRepository(
fun getAll() = dao.getAllApps().distinctUntilChanged() fun getAll() = dao.getAllApps().distinctUntilChanged()
fun get(packageName: String) = dao.get(packageName)
fun getApkFileForApp(app: DownloadedApp): File = fun getApkFileForApp(app: DownloadedApp): File =
getApkFileForDir(dir.resolve(app.directory)) getApkFileForDir(dir.resolve(app.directory))

View File

@@ -26,7 +26,6 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@@ -75,17 +74,6 @@ class PatchBundleRepository(
val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } } val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } }
fun suggestedVersions(packageName: String, patchSelection: PatchSelection) =
bundleInfoFlow.map {
val allPatches = patchSelection.flatMap { (uid, patches) ->
val bundle = it[uid] ?: return@flatMap emptyList()
bundle.patches.filter { patch -> patches.contains(patch.name) }
.map(PatchInfo::toPatcherPatch)
}.toSet()
allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)[packageName]
}
val suggestedVersions = bundleInfoFlow.map { val suggestedVersions = bundleInfoFlow.map {
val allPatches = val allPatches =
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet() it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
@@ -298,28 +286,29 @@ class PatchBundleRepository(
State(sources.toPersistentMap(), info.toPersistentMap()) State(sources.toPersistentMap(), info.toPersistentMap())
} }
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") { suspend fun createLocal(createStream: suspend () -> InputStream) =
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) { dispatchAction("Add bundle") {
try { with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
createStream().use { patches -> replace(patches) } try {
} catch (e: Exception) { createStream().use { patches -> replace(patches) }
if (e is CancellationException) throw e } catch (e: Exception) {
Log.e(tag, "Got exception while importing bundle", e) if (e is CancellationException) throw e
withContext(Dispatchers.Main) { Log.e(tag, "Got exception while importing bundle", e)
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage())) 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) = suspend fun createRemote(url: String, autoUpdate: Boolean) =
dispatchAction("Add bundle ($url)") { state -> dispatchAction("Add bundle ($url)") { state ->
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle 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)) state.copy(sources = state.sources.put(src.uid, src))
} }
@@ -341,32 +330,38 @@ class PatchBundleRepository(
state.copy(sources = state.sources.put(uid, newSrc)) 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() 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. * 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 inner class Update(
private val force: Boolean = false, private val force: Boolean = false,
private val redownload: Boolean = false,
private val showToast: Boolean = false, private val showToast: Boolean = false,
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true }, private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
) : Action<State> { ) : Action<State> {
private suspend fun toast(@StringRes id: Int, vararg args: Any?) = private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) } 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( override suspend fun ActionContext.execute(
current: State current: State
) = coroutineScope { ) = coroutineScope {
if (!networkInfo.isSafe()) { if (!networkInfo.isSafe(force)) {
Log.d(tag, "Skipping update check because the network is down or metered.") Log.d(tag, "Skipping update check because the network is down or metered.")
return@coroutineScope current return@coroutineScope current
} }
@@ -379,7 +374,7 @@ class PatchBundleRepository(
Log.d(tag, "Updating patch bundle: ${it.name}") Log.d(tag, "Updating patch bundle: ${it.name}")
val newVersion = with(it) { val newVersion = with(it) {
if (force) downloadLatest() else update() if (redownload) downloadLatest() else update()
} ?: return@async null } ?: return@async null
it to newVersion it to newVersion

View File

@@ -16,7 +16,7 @@ class PatchSelectionRepository(db: AppDatabase) {
packageName = packageName packageName = packageName
).also { dao.createSelection(it) }.uid ).also { dao.createSelection(it) }.uid
suspend fun getSelection(packageName: String): app.revanced.manager.util.PatchSelection = suspend fun getSelection(packageName: String): Map<Int, Set<String>> =
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() } dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) = suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =

View File

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

View File

@@ -40,7 +40,7 @@ data class PatchInfo(
if (pkg.packageName != packageName) return@any false if (pkg.packageName != packageName) return@any false
if (pkg.versions == null) return@any true if (pkg.versions == null) return@any true
versionName == null || versionName in pkg.versions versionName != null && versionName in pkg.versions
} }
} }

View File

@@ -39,7 +39,7 @@ import app.revanced.manager.patcher.toRemoteError
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.SelectedSource import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@@ -67,9 +67,7 @@ class PatcherWorker(
private val rootInstaller: RootInstaller by inject() private val rootInstaller: RootInstaller by inject()
class Args( class Args(
val packageName: String, val input: SelectedApp,
val version: String?,
val source: SelectedSource,
val output: String, val output: String,
val selectedPatches: PatchSelection, val selectedPatches: PatchSelection,
val options: Options, val options: Options,
@@ -77,7 +75,9 @@ class PatcherWorker(
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult, val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
val setInputFile: suspend (File) -> Unit, val setInputFile: suspend (File) -> Unit,
val onEvent: (ProgressEvent) -> Unit, val onEvent: (ProgressEvent) -> Unit,
) ) {
val packageName get() = input.packageName
}
override suspend fun getForegroundInfo() = override suspend fun getForegroundInfo() =
ForegroundInfo( ForegroundInfo(
@@ -142,7 +142,7 @@ class PatcherWorker(
val patchedApk = fs.tempDir.resolve("patched.apk") val patchedApk = fs.tempDir.resolve("patched.apk")
return try { return try {
if (args.source is SelectedSource.Installed) { if (args.input is SelectedApp.Installed) {
installedAppRepository.get(args.packageName)?.let { installedAppRepository.get(args.packageName)?.let {
if (it.installType == InstallType.MOUNT) { if (it.installType == InstallType.MOUNT) {
rootInstaller.unmount(args.packageName) rootInstaller.unmount(args.packageName)
@@ -155,23 +155,32 @@ class PatcherWorker(
plugin, plugin,
data, data,
args.packageName, args.packageName,
args.version, args.input.version,
prefs.suggestedVersionSafeguard.get(), prefs.suggestedVersionSafeguard.get(),
!prefs.disablePatchVersionCompatCheck.get(), !prefs.disablePatchVersionCompatCheck.get(),
) { progress -> onDownload = { progress ->
args.onEvent( args.onEvent(
ProgressEvent.Progress( ProgressEvent.Progress(
stepId = StepId.DownloadAPK, stepId = StepId.DownloadAPK,
current = progress.first, current = progress.first,
total = progress.second total = progress.second
)
) )
) }
}.also { args.setInputFile(it) } ).also { args.setInputFile(it) }
val inputFile = when (val source = args.source) { val inputFile = when (val selectedApp = args.input) {
is SelectedSource.Auto -> throw Exception("Auto source is not supported in worker.") is SelectedApp.Download -> {
runStep(StepId.DownloadAPK, args.onEvent) {
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
selectedApp.data
)
is SelectedSource.Plugin -> { download(plugin, data)
}
}
is SelectedApp.Search -> {
runStep(StepId.DownloadAPK, args.onEvent) { runStep(StepId.DownloadAPK, args.onEvent) {
downloaderPluginRepository.loadedPluginsFlow.first() downloaderPluginRepository.loadedPluginsFlow.first()
.firstNotNullOfOrNull { plugin -> .firstNotNullOfOrNull { plugin ->
@@ -197,10 +206,10 @@ class PatcherWorker(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
plugin.get( plugin.get(
getScope, getScope,
args.packageName, selectedApp.packageName,
args.version selectedApp.version
) )
}?.takeIf { (_, version) -> args.version == null || version == args.version } }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
} catch (e: UserInteractionException.Activity.NotCompleted) { } catch (e: UserInteractionException.Activity.NotCompleted) {
throw e throw e
} catch (_: UserInteractionException) { } catch (_: UserInteractionException) {
@@ -210,10 +219,8 @@ class PatcherWorker(
} }
} }
is SelectedSource.Downloaded -> File(source.path) is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
is SelectedSource.Local -> File(source.path) is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
is SelectedSource.Installed -> File(pm.getPackageInfo(args.packageName)!!.applicationInfo!!.sourceDir)
} }
val runtime = if (prefs.useProcessRuntime.get()) { val runtime = if (prefs.useProcessRuntime.get()) {
@@ -251,7 +258,9 @@ class PatcherWorker(
Result.failure() Result.failure()
} finally { } finally {
patchedApk.delete() patchedApk.delete()
if (args.source is SelectedSource.Local) File(args.source.path).delete() if (args.input is SelectedApp.Local && args.input.temporary) {
args.input.file.delete()
}
} }
} }

View File

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

View File

@@ -15,9 +15,8 @@ import androidx.compose.ui.Modifier
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.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import app.revanced.manager.R 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -48,8 +47,6 @@ fun AppLabel(
shape = RoundedCornerShape(100) shape = RoundedCornerShape(100)
) )
.then(modifier), .then(modifier),
style = style, style = style
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import app.revanced.manager.R
import app.revanced.manager.patcher.StepId import app.revanced.manager.patcher.StepId
import kotlinx.parcelize.Parcelize 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), PREPARING(R.string.patcher_step_group_preparing),
PATCHING(R.string.patcher_step_group_patching), PATCHING(R.string.patcher_step_group_patching),
SAVING(R.string.patcher_step_group_saving) SAVING(R.string.patcher_step_group_saving)

View File

@@ -0,0 +1,35 @@
package app.revanced.manager.ui.model
import android.os.Parcelable
import app.revanced.manager.network.downloader.ParceledDownloaderData
import kotlinx.parcelize.Parcelize
import java.io.File
sealed interface SelectedApp : Parcelable {
val packageName: String
val version: String?
@Parcelize
data class Download(
override val packageName: String,
override val version: String?,
val data: ParceledDownloaderData
) : SelectedApp
@Parcelize
data class Search(override val packageName: String, override val version: String?) : SelectedApp
@Parcelize
data class Local(
override val packageName: String,
override val version: String,
val file: File,
val temporary: Boolean
) : SelectedApp
@Parcelize
data class Installed(
override val packageName: String,
override val version: String
) : SelectedApp
}

View File

@@ -1,13 +0,0 @@
package app.revanced.manager.ui.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
sealed class SelectedSource : Parcelable {
data object Auto : SelectedSource()
data object Installed : SelectedSource()
data class Downloaded(val path: String, val version: String) : SelectedSource()
data class Local(val path: String) : SelectedSource()
data class Plugin(val packageName: String?) : SelectedSource()
}

View File

@@ -1,11 +0,0 @@
package app.revanced.manager.ui.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
sealed class SelectedVersion : Parcelable {
data object Auto : SelectedVersion()
data object Any : SelectedVersion()
data class Specific(val version: String) : SelectedVersion()
}

View File

@@ -1,8 +1,7 @@
package app.revanced.manager.ui.model.navigation package app.revanced.manager.ui.model.navigation
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.ui.model.SelectedSource import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.SelectedVersion
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 kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -24,11 +23,10 @@ data class InstalledApplicationInfo(val packageName: String)
data class Update(val downloadOnScreenEntry: Boolean = false) data class Update(val downloadOnScreenEntry: Boolean = false)
@Serializable @Serializable
data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams> { data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
@Parcelize @Parcelize
data class ViewModelParams( data class ViewModelParams(
val packageName: String, val app: SelectedApp,
val localPath: String? = null,
val patches: PatchSelection? = null val patches: PatchSelection? = null
) : Parcelable ) : Parcelable
@@ -39,35 +37,12 @@ data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams>
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> { data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
@Parcelize @Parcelize
data class ViewModelParams( data class ViewModelParams(
val packageName: String, val app: SelectedApp,
val version: String?, val currentSelection: PatchSelection?,
val patchSelection: PatchSelection?,
val options: @RawValue Options, val options: @RawValue Options,
) : Parcelable ) : Parcelable
} }
@Serializable
data object VersionSelector : ComplexParameter<VersionSelector.ViewModelParams> {
@Parcelize
data class ViewModelParams(
val packageName: String,
val patchSelection: PatchSelection,
val selectedVersion: SelectedVersion,
val localPath: String? = null,
) : Parcelable
}
@Serializable
data object SourceSelector : ComplexParameter<SourceSelector.ViewModelParams> {
@Parcelize
data class ViewModelParams(
val packageName: String,
val version: String?,
val selectedSource: SelectedSource,
val localPath: String? = null,
) : Parcelable
}
@Serializable @Serializable
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams> data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
} }
@@ -76,9 +51,7 @@ data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams>
data object Patcher : ComplexParameter<Patcher.ViewModelParams> { data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
@Parcelize @Parcelize
data class ViewModelParams( data class ViewModelParams(
val packageName: String, val selectedApp: SelectedApp,
val version: String?,
val selectedSource: SelectedSource,
val selectedPatches: PatchSelection, val selectedPatches: PatchSelection,
val options: @RawValue Options val options: @RawValue Options
) : Parcelable ) : Parcelable

View File

@@ -44,6 +44,7 @@ import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.NonSuggestedVersionDialog import app.revanced.manager.ui.component.NonSuggestedVersionDialog
import app.revanced.manager.ui.component.SearchView import app.revanced.manager.ui.component.SearchView
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.EventEffect import app.revanced.manager.util.EventEffect
@@ -53,13 +54,13 @@ import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppSelectorScreen( fun AppSelectorScreen(
onSelect: (packageName: String) -> Unit, onSelect: (String) -> Unit,
onStorageSelect: (packageName: String, path: String) -> Unit, onStorageSelect: (SelectedApp.Local) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: AppSelectorViewModel = koinViewModel() vm: AppSelectorViewModel = koinViewModel()
) { ) {
EventEffect(flow = vm.storageSelectionFlow) { EventEffect(flow = vm.storageSelectionFlow) {
onStorageSelect(it.first, it.second) onStorageSelect(it)
} }
val pickApkLauncher = val pickApkLauncher =
@@ -82,12 +83,12 @@ fun AppSelectorScreen(
} }
} }
// vm.nonSuggestedVersionDialogSubject?.let { vm.nonSuggestedVersionDialogSubject?.let {
// NonSuggestedVersionDialog( NonSuggestedVersionDialog(
// suggestedVersion = suggestedVersions[it.packageName].orEmpty(), suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
// onDismiss = vm::dismissNonSuggestedVersionDialog onDismiss = vm::dismissNonSuggestedVersionDialog
// ) )
// } }
if (search) if (search)
SearchView( SearchView(
@@ -114,7 +115,8 @@ fun AppSelectorScreen(
) )
}, },
headlineContent = { AppLabel(app.packageInfo) }, headlineContent = { AppLabel(app.packageInfo) },
supportingContent = app.patches?.let { supportingContent = { Text(app.packageName) },
trailingContent = app.patches?.let {
{ {
Text( Text(
pluralStringResource( pluralStringResource(
@@ -212,7 +214,12 @@ fun AppSelectorScreen(
defaultText = app.packageName defaultText = app.packageName
) )
}, },
supportingContent = app.patches?.let { supportingContent = {
suggestedVersions[app.packageName]?.let {
Text(stringResource(R.string.suggested_version_info, it))
}
},
trailingContent = app.patches?.let {
{ {
Text( Text(
pluralStringResource( pluralStringResource(

View File

@@ -36,7 +36,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TabRow import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
@@ -53,6 +53,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
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 androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -100,6 +101,7 @@ fun DashboardScreen(
false false
) )
val androidContext = LocalContext.current val androidContext = LocalContext.current
val resources = LocalResources.current
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = DashboardPage.DASHBOARD.ordinal, initialPage = DashboardPage.DASHBOARD.ordinal,
@@ -234,7 +236,7 @@ fun DashboardScreen(
when (pagerState.currentPage) { when (pagerState.currentPage) {
DashboardPage.DASHBOARD.ordinal -> { DashboardPage.DASHBOARD.ordinal -> {
if (availablePatches < 1) { if (availablePatches < 1) {
androidContext.toast(androidContext.getString(R.string.no_patch_found)) androidContext.toast(resources.getString(R.string.no_patch_found))
composableScope.launch { composableScope.launch {
pagerState.animateScrollToPage( pagerState.animateScrollToPage(
DashboardPage.BUNDLES.ordinal DashboardPage.BUNDLES.ordinal
@@ -259,7 +261,7 @@ fun DashboardScreen(
} }
) { paddingValues -> ) { paddingValues ->
Column(Modifier.padding(paddingValues)) { Column(Modifier.padding(paddingValues)) {
TabRow( SecondaryTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) 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.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
@@ -69,6 +70,7 @@ fun PatcherScreen(
} }
val context = LocalContext.current val context = LocalContext.current
val resources = LocalResources.current
val exportApkLauncher = val exportApkLauncher =
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export) rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
@@ -79,7 +81,7 @@ fun PatcherScreen(
fun onPageBack() = when { fun onPageBack() = when {
patcherSucceeded == null -> showDismissConfirmationDialog = true 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() else -> onLeave()
} }

View File

@@ -40,7 +40,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.SecondaryScrollableTabRow
import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -49,10 +49,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha 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.PatchSelection
import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, FlowPreview::class)
@Composable @Composable
fun PatchesSelectorScreen( fun PatchesSelectorScreen(
onSave: (PatchSelection?, Options) -> Unit, onSave: (PatchSelection?, Options) -> Unit,
@@ -231,7 +235,8 @@ fun PatchesSelectorScreen(
viewModel.selectionWarningEnabled -> showSelectionWarning = true viewModel.selectionWarningEnabled -> showSelectionWarning = true
// Show universal warning if universal patch is selected and the toggle is off // 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 // Toggle the patch otherwise
else -> viewModel.togglePatch(uid, patch) else -> viewModel.togglePatch(uid, patch)
@@ -360,6 +365,21 @@ fun PatchesSelectorScreen(
) { ) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) 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( HapticExtendedFloatingActionButton(
text = { text = {
Text( Text(
@@ -375,8 +395,7 @@ fun PatchesSelectorScreen(
contentDescription = stringResource(R.string.save) contentDescription = stringResource(R.string.save)
) )
}, },
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp expanded = expanded,
?: true,
onClick = { onClick = {
onSave(viewModel.getCustomSelection(), viewModel.getOptions()) onSave(viewModel.getCustomSelection(), viewModel.getOptions())
} }
@@ -392,7 +411,7 @@ fun PatchesSelectorScreen(
.padding(top = 16.dp) .padding(top = 16.dp)
) { ) {
if (bundles.size > 1) { if (bundles.size > 1) {
ScrollableTabRow( SecondaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) { ) {
@@ -498,7 +517,7 @@ private fun PatchItem(
leadingContent = { leadingContent = {
HapticCheckbox( HapticCheckbox(
checked = selected, checked = selected,
onCheckedChange = null, onCheckedChange = { onToggle() },
enabled = compatible enabled = compatible
) )
}, },

View File

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

View File

@@ -1,12 +1,16 @@
package app.revanced.manager.ui.screen package app.revanced.manager.ui.screen
import androidx.activity.compose.rememberLauncherForActivityResult
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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope 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
import androidx.compose.foundation.lazy.LazyColumn
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
@@ -17,69 +21,82 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults 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.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
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.platform.LocalResources
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 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.platform.NetworkInfo
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.AppInfo 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.NotificationCard 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.SelectedSource import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.SelectedVersion
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect
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.enabled import app.revanced.manager.util.enabled
import app.revanced.manager.util.patchCount
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SelectedAppInfoScreen( fun SelectedAppInfoScreen(
onPatchSelectorClick: (packageName: String, version: String?, PatchSelection?, Options) -> Unit, onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
onRequiredOptions: (packageName: String, version: String?, PatchSelection?, Options) -> Unit, onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
onPatchClick: () -> Unit, onPatchClick: () -> Unit,
onVersionClick: (packageName: String, patchSelection: PatchSelection, selectedVersion: SelectedVersion, localPath: String?) -> Unit,
onSourceClick: (packageName: String, version: String?, SelectedSource, localPath: String?) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: SelectedAppInfoViewModel vm: SelectedAppInfoViewModel
) { ) {
val context = LocalContext.current val context = LocalContext.current
val resources = LocalResources.current
val networkInfo = koinInject<NetworkInfo>() val networkInfo = koinInject<NetworkInfo>()
val networkConnected = remember { networkInfo.isConnected() } val networkConnected = remember { networkInfo.isConnected() }
val networkMetered = remember { !networkInfo.isUnmetered() } val networkMetered = remember { !networkInfo.isUnmetered() }
val packageName = vm.packageName val packageName = vm.selectedApp.packageName
val version = vm.selectedApp.version
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
val patches by remember {
derivedStateOf {
vm.getPatches(bundles, allowIncompatiblePatches)
}
}
val selectedPatchCount = patches.values.sumOf { it.size }
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
onResult = vm::handlePluginActivityResult
)
EventEffect(flow = vm.launchActivityFlow) { intent ->
launcher.launch(intent)
}
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
val error by vm.errorFlow.collectAsStateWithLifecycle(null) val error by vm.errorFlow.collectAsStateWithLifecycle(null)
val selectedVersion by vm.selectedVersion.collectAsStateWithLifecycle()
val resolvedVersion by vm.resolvedVersion.collectAsStateWithLifecycle(null)
val selectedSource by vm.selectedSource.collectAsStateWithLifecycle()
val resolvedSource by vm.resolvedSource.collectAsStateWithLifecycle(null)
val customSelection by vm.customSelection.collectAsStateWithLifecycle(null)
val fullPatchSelection by vm.patchSelection.collectAsStateWithLifecycle(emptyMap())
val patchCount = fullPatchSelection.patchCount
val incompatibleCount by vm.incompatiblePatchCount.collectAsStateWithLifecycle(0)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
@@ -102,18 +119,18 @@ fun SelectedAppInfoScreen(
) )
}, },
onClick = patchClick@{ onClick = patchClick@{
if (patchCount == 0) { if (selectedPatchCount == 0) {
context.toast(context.getString(R.string.no_patches_selected)) context.toast(resources.getString(R.string.no_patches_selected))
return@patchClick return@patchClick
} }
composableScope.launch { composableScope.launch {
if (!vm.hasSetRequiredOptions(fullPatchSelection)) { if (!vm.hasSetRequiredOptions(patches)) {
onRequiredOptions( onRequiredOptions(
vm.packageName, vm.selectedApp,
resolvedVersion, vm.getCustomPatches(bundles, allowIncompatiblePatches),
customSelection, vm.options
vm.options,
) )
return@launch return@launch
} }
@@ -125,96 +142,94 @@ fun SelectedAppInfoScreen(
}, },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
if (vm.showSourceSelector) {
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
AppSourceSelectorDialog(
plugins = plugins,
installedApp = vm.installedAppData,
searchApp = SelectedApp.Search(
vm.packageName,
vm.desiredVersion
),
activeSearchJob = vm.activePluginAction,
hasRoot = vm.hasRoot,
onDismissRequest = vm::dismissSourceSelector,
onSelectPlugin = vm::searchUsingPlugin,
requiredVersion = requiredVersion,
onSelect = {
vm.selectedApp = it
vm.dismissSourceSelector()
}
)
}
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) { AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
vm.selectedAppInfo?.let { Text(
Text( version ?: stringResource(R.string.selected_app_meta_any_version),
it.packageName, color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium,
style = MaterialTheme.typography.bodyMedium, )
)
}
} }
PageItem( PageItem(
R.string.patch_selector_item, R.string.patch_selector_item,
stringResource(R.string.patch_selector_item_description, patchCount), stringResource(
R.string.patch_selector_item_description,
selectedPatchCount
),
onClick = { onClick = {
onPatchSelectorClick( onPatchSelectorClick(
vm.packageName, vm.selectedApp,
resolvedVersion, vm.getCustomPatches(
customSelection, bundles,
allowIncompatiblePatches
),
vm.options vm.options
) )
},
extraDescription = if (incompatibleCount > 0) { {
Text(
"$incompatibleCount incompatible",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium,
)
} } else null,
)
val versionText = resolvedVersion ?: "Any available version"
val versionDescription = if (selectedVersion is SelectedVersion.Auto)
"Auto ($versionText)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion)
else versionText
PageItem(
R.string.version_selector_item,
versionDescription,
onClick = {
onVersionClick(
packageName,
fullPatchSelection,
selectedVersion,
vm.localPath,
)
},
)
val sourceText = when (val source = resolvedSource) {
is SelectedSource.Installed -> "Installed APK"
is SelectedSource.Downloaded -> "Downloaded APK"
is SelectedSource.Local -> "Local APK"
is SelectedSource.Plugin -> {
source.packageName ?: "Any available downloader"
} }
else -> "Auto" )
}
val sourceDescription = if (selectedSource is SelectedSource.Auto)
"Auto ($sourceText)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion)
else sourceText
PageItem( PageItem(
R.string.apk_source_selector_item, R.string.apk_source_selector_item,
sourceDescription, when (val app = vm.selectedApp) {
onClick = { onSourceClick( is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
packageName, is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
resolvedVersion, is SelectedApp.Download -> stringResource(
selectedSource, R.string.apk_source_downloader,
vm.localPath, plugins.find { it.packageName == app.data.pluginPackageName }?.name
) }, ?: app.data.pluginPackageName
) )
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
},
onClick = {
vm.showSourceSelector()
}
)
error?.let { error?.let {
Text( Text(
stringResource(it.resourceId), stringResource(it.resourceId),
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 24.dp)
) )
} }
if (resolvedSource is SelectedSource.Plugin) Column( Column(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), modifier = Modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
val needsInternet =
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
when { when {
!needsInternet -> {}
!networkConnected -> { !networkConnected -> {
NotificationCard( NotificationCard(
isWarning = true, isWarning = true,
@@ -223,6 +238,7 @@ fun SelectedAppInfoScreen(
onDismiss = null onDismiss = null
) )
} }
networkMetered -> { networkMetered -> {
NotificationCard( NotificationCard(
isWarning = true, isWarning = true,
@@ -238,17 +254,11 @@ fun SelectedAppInfoScreen(
} }
@Composable @Composable
private fun PageItem( private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
@StringRes title: Int,
description: String,
onClick: () -> Unit,
enabled: Boolean = true,
extraDescription: @Composable (ColumnScope.() -> Unit)? = null,
) {
ListItem( ListItem(
modifier = Modifier modifier = Modifier
.clickable(enabled, onClick = onClick) .clickable(onClick = onClick)
.enabled(enabled), .padding(start = 8.dp),
headlineContent = { headlineContent = {
Text( Text(
stringResource(title), stringResource(title),
@@ -257,17 +267,99 @@ private fun PageItem(
) )
}, },
supportingContent = { supportingContent = {
Column { Text(
Text( description,
description, color = MaterialTheme.colorScheme.outline,
color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.bodyMedium
style = MaterialTheme.typography.bodyMedium )
)
extraDescription?.invoke(this)
}
}, },
trailingContent = { trailingContent = {
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null) Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
} }
) )
}
@Composable
private fun AppSourceSelectorDialog(
plugins: List<LoadedDownloaderPlugin>,
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
searchApp: SelectedApp.Search,
activeSearchJob: String?,
hasRoot: Boolean,
requiredVersion: String?,
onDismissRequest: () -> Unit,
onSelectPlugin: (LoadedDownloaderPlugin) -> Unit,
onSelect: (SelectedApp) -> Unit,
) {
val canSelect = activeSearchJob == null
AlertDialogExtended(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
},
title = { Text(stringResource(R.string.app_source_dialog_title)) },
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
text = {
LazyColumn {
item(key = "auto") {
val hasPlugins = plugins.isNotEmpty()
ListItem(
modifier = Modifier
.clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
.enabled(hasPlugins),
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
supportingContent = {
Text(
if (hasPlugins)
stringResource(R.string.app_source_dialog_option_auto_description)
else
stringResource(R.string.app_source_dialog_option_auto_unavailable)
)
},
colors = transparentListItemColors
)
}
installedApp?.let { (app, meta) ->
item(key = "installed") {
val (usable, text) = when {
// Mounted apps must be unpatched before patching, which cannot be done without root access.
meta?.installType == InstallType.MOUNT && !hasRoot -> false to stringResource(
R.string.app_source_dialog_option_installed_no_root
)
// Patching already patched apps is not allowed because patches expect unpatched apps.
meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched)
// Version does not match suggested version.
requiredVersion != null && app.version != requiredVersion -> false to stringResource(
R.string.app_source_dialog_option_installed_version_not_suggested,
app.version
)
else -> true to app.version
}
ListItem(
modifier = Modifier
.clickable(enabled = canSelect && usable) { onSelect(app) }
.enabled(usable),
headlineContent = { Text(stringResource(R.string.installed)) },
supportingContent = { Text(text) },
colors = transparentListItemColors
)
}
}
items(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
ListItem(
modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
headlineContent = { Text(plugin.name) },
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
colors = transparentListItemColors
)
}
}
}
)
} }

View File

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

View File

@@ -1,154 +0,0 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.model.SelectedSource
import app.revanced.manager.ui.viewmodel.SourceSelectorViewModel
import app.revanced.manager.util.enabled
import app.revanced.manager.util.transparentListItemColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SourceSelectorScreen(
onBackClick: () -> Unit,
onSave: (source: SelectedSource) -> Unit,
viewModel: SourceSelectorViewModel,
) {
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
val plugins by viewModel.plugins.collectAsStateWithLifecycle(emptyList())
Scaffold(
topBar = {
AppTopBar(
title = { Text("Select source") },
onBackClick = onBackClick,
)
},
floatingActionButton = {
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save)) },
icon = { Icon(Icons.Outlined.Save, null) },
onClick = { onSave(viewModel.selectedSource) },
)
}
) { paddingValues ->
LazyColumnWithScrollbar (
contentPadding = paddingValues,
) {
item {
SourceOption(
isSelected = viewModel.selectedSource == SelectedSource.Auto,
onSelect = { viewModel.selectSource(SelectedSource.Auto) },
headlineContent = { Text("Auto (Recommended)") },
supportingContent = { Text("Automatically select the best available source") }
)
}
item {
SourceOption(
isSelected = viewModel.selectedSource == SelectedSource.Plugin(null),
onSelect = { viewModel.selectSource(SelectedSource.Plugin(null)) },
headlineContent = { Text("Any available downloader") },
)
}
viewModel.localApp?.let { option ->
item {
HorizontalDivider()
SourceOption(
sourceOption = option,
isSelected = viewModel.selectedSource == option.source,
onSelect = viewModel::selectSource,
)
}
}
viewModel.installedSource?.let { option ->
item {
HorizontalDivider()
SourceOption(
sourceOption = option,
isSelected = viewModel.selectedSource == option.source,
onSelect = viewModel::selectSource,
)
}
}
if (downloadedApps.isNotEmpty()) item { HorizontalDivider() }
items(downloadedApps, key = { it.key }) { option ->
SourceOption(
sourceOption = option,
isSelected = viewModel.selectedSource == option.source,
onSelect = viewModel::selectSource,
)
}
if (plugins.isNotEmpty()) item { HorizontalDivider() }
items(plugins, key = { it.key }) { option ->
SourceOption(
sourceOption = option,
isSelected = viewModel.selectedSource == option.source,
onSelect = viewModel::selectSource,
)
}
}
}
}
@Composable
private fun SourceOption(
sourceOption: SourceSelectorViewModel.SourceOption,
isSelected: Boolean,
onSelect: (SelectedSource) -> Unit,
) = SourceOption(
isSelected = isSelected,
onSelect = { onSelect(sourceOption.source) },
overlineContent = sourceOption.category?.let {{ Text(it) }},
headlineContent = { Text(sourceOption.title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
supportingContent = sourceOption.disableReason?.let {{ Text(it.message) }},
enabled = sourceOption.disableReason == null,
)
@Composable
private fun SourceOption(
isSelected: Boolean,
onSelect: () -> Unit,
headlineContent: @Composable (() -> Unit),
supportingContent: @Composable (() -> Unit)? = null,
overlineContent: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
) {
ListItem(
modifier = Modifier
.clickable(enabled) { onSelect() }
.enabled(enabled),
leadingContent = {
RadioButton(
selected = isSelected,
onClick = null
)
},
headlineContent = headlineContent,
supportingContent = supportingContent,
overlineContent = overlineContent,
colors = transparentListItemColors
)
}

View File

@@ -1,139 +0,0 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.model.SelectedVersion
import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
import app.revanced.manager.util.transparentListItemColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VersionSelectorScreen(
onBackClick: () -> Unit,
onSave: (version: SelectedVersion) -> Unit,
viewModel: VersionSelectorViewModel,
) {
val versions by viewModel.availableVersions.collectAsStateWithLifecycle(emptyList())
val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList())
val localVersion by viewModel.localVersion.collectAsStateWithLifecycle(null)
Scaffold(
topBar = {
AppTopBar(
title = { Text("Select version") },
onBackClick = onBackClick,
actions = {
IconButton({}) {
Icon(Icons.Outlined.MoreVert, contentDescription = null)
}
}
)
},
floatingActionButton = {
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save)) },
icon = { Icon(Icons.Outlined.Save, contentDescription = null) },
onClick = { onSave(viewModel.selectedVersion) }
)
}
) { paddingValues ->
Column(
modifier = Modifier.padding(paddingValues)
) {
VersionOption(
version = SelectedVersion.Auto,
isSelected = viewModel.selectedVersion is SelectedVersion.Auto,
onSelect = viewModel::selectVersion,
headlineContent = { Text("Auto (Recommended)") },
supportingContent = { Text("Automatically select the best available version") }
)
HorizontalDivider()
if (versions.isNotEmpty()) {
LazyColumn {
items(versions, key = { it.first.version }) { version ->
val isDownloaded = downloadedVersions.contains(version.first.version)
val isInstalled = viewModel.installedAppVersion == version.first.version
val isLocal = localVersion == version.first.version
val overlineText = when {
isLocal -> "Local"
isDownloaded && isInstalled -> "Downloaded, Installed"
isDownloaded -> "Downloaded"
isInstalled -> "Installed"
else -> null
}
VersionOption(
version = version.first,
isSelected = viewModel.selectedVersion == version.first,
onSelect = viewModel::selectVersion,
headlineContent = { Text(version.first.version) },
supportingContent = {
Text(
"${version.second.let { if (it == 0) "No" else it }} incompatible patches"
)
},
overlineContent = overlineText?.let { { Text(it) } }
)
}
}
} else {
VersionOption(
version = SelectedVersion.Any,
isSelected = viewModel.selectedVersion is SelectedVersion.Any,
onSelect = viewModel::selectVersion,
headlineContent = { Text("Any available version") },
supportingContent = { Text("Use any available version regardless of compatibility") }
)
}
}
}
}
@Composable
private fun VersionOption(
version: SelectedVersion,
isSelected: Boolean,
onSelect: (SelectedVersion) -> Unit,
headlineContent: @Composable (() -> Unit),
supportingContent: @Composable (() -> Unit)? = null,
overlineContent: @Composable (() -> Unit)? = null,
) {
ListItem(
modifier = Modifier
.clickable { onSelect(version) },
leadingContent = {
RadioButton(
selected = isSelected,
onClick = null
)
},
headlineContent = headlineContent,
supportingContent = supportingContent,
trailingContent = overlineContent,
colors = transparentListItemColors
)
}

View File

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

View File

@@ -38,6 +38,7 @@ import androidx.compose.runtime.setValue
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.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -64,9 +65,10 @@ fun AdvancedSettingsScreen(
viewModel: AdvancedSettingsViewModel = koinViewModel() viewModel: AdvancedSettingsViewModel = koinViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
val memoryLimit = remember { val resources = LocalResources.current
val memoryLimit = remember(resources) {
val activityManager = context.getSystemService<ActivityManager>()!! val activityManager = context.getSystemService<ActivityManager>()!!
context.getString( resources.getString(
R.string.device_memory_limit_format, R.string.device_memory_limit_format,
activityManager.memoryClass, activityManager.memoryClass,
activityManager.largeMemoryClass activityManager.largeMemoryClass
@@ -183,7 +185,7 @@ fun AdvancedSettingsScreen(
ClipData.newPlainText("Device Information", deviceContent) 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) }.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
), ),
headlineContent = stringResource(R.string.about_device), 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.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.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
@@ -19,6 +21,7 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -38,6 +41,7 @@ import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject import org.koin.compose.koinInject
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -48,6 +52,7 @@ fun GeneralSettingsScreen(
val prefs = viewModel.prefs val prefs = viewModel.prefs
val coroutineScope = viewModel.viewModelScope val coroutineScope = viewModel.viewModelScope
var showThemePicker by rememberSaveable { mutableStateOf(false) } var showThemePicker by rememberSaveable { mutableStateOf(false) }
var showLanguagePicker by rememberSaveable { mutableStateOf(false) }
if (showThemePicker) { if (showThemePicker) {
ThemePicker( ThemePicker(
@@ -55,6 +60,17 @@ fun GeneralSettingsScreen(
onConfirm = { viewModel.setTheme(it) } 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()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
@@ -74,6 +90,24 @@ fun GeneralSettingsScreen(
) { ) {
GroupHeader(stringResource(R.string.appearance)) 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() val theme by prefs.theme.getAsState()
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { showThemePicker = true }, modifier = Modifier.clickable { showThemePicker = true },
@@ -105,6 +139,14 @@ fun GeneralSettingsScreen(
description = R.string.pure_black_theme_description 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.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.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -64,6 +65,7 @@ fun ImportExportSettingsScreen(
vm: ImportExportViewModel = koinViewModel() vm: ImportExportViewModel = koinViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) } var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
@@ -108,7 +110,7 @@ fun ImportExportSettingsScreen(
vm.viewModelScope.launch { vm.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(alias, pass) 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( GroupItem(
onClick = { onClick = {
if (!vm.canExport()) { if (!vm.canExport()) {
context.toast(context.getString(R.string.export_keystore_unavailable)) context.toast(resources.getString(R.string.export_keystore_unavailable))
return@GroupItem return@GroupItem
} }
exportKeystoreLauncher.launch("Manager.keystore") 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.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.Scrollbar 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.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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -33,16 +36,23 @@ fun LicensesSettingsScreen(
) { paddingValues -> ) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) { Column(modifier = Modifier.padding(paddingValues)) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val libraries by produceLibraries(R.raw.aboutlibraries)
val chipColors = LibraryDefaults.chipColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
)
LibrariesContainer( LibrariesContainer(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
libraries = libraries,
lazyListState = lazyListState, lazyListState = lazyListState,
colors = LibraryDefaults.libraryColors( colors = LibraryDefaults.libraryColors(
backgroundColor = MaterialTheme.colorScheme.background, libraryBackgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground, libraryContentColor = MaterialTheme.colorScheme.onBackground,
badgeBackgroundColor = MaterialTheme.colorScheme.primary, versionChipColors = chipColors,
badgeContentColor = MaterialTheme.colorScheme.onPrimary, licenseChipColors = chipColors,
fundingChipColors = chipColors,
) )
) )
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues)) 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.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.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
@@ -33,6 +34,7 @@ fun UpdatesSettingsScreen(
vm: UpdatesSettingsViewModel = koinViewModel(), vm: UpdatesSettingsViewModel = koinViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -57,7 +59,7 @@ fun UpdatesSettingsScreen(
modifier = Modifier.clickable { modifier = Modifier.clickable {
coroutineScope.launch { coroutineScope.launch {
if (!vm.isConnected) { if (!vm.isConnected) {
context.toast(context.getString(R.string.no_network_toast)) context.toast(resources.getString(R.string.no_network_toast))
return@launch return@launch
} }
if (vm.checkForUpdates()) onUpdateClick() if (vm.checkForUpdates()) onUpdateClick()
@@ -70,7 +72,7 @@ fun UpdatesSettingsScreen(
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { modifier = Modifier.clickable {
if (!vm.isConnected) { if (!vm.isConnected) {
context.toast(context.getString(R.string.no_network_toast)) context.toast(resources.getString(R.string.no_network_toast))
return@clickable return@clickable
} }
onChangelogClick() onChangelogClick()

View File

@@ -3,6 +3,9 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -11,6 +14,7 @@ import androidx.lifecycle.viewmodel.compose.saveable
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.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.ui.model.SelectedApp
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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -27,7 +31,7 @@ class AppSelectorViewModel(
private val app: Application, private val app: Application,
private val pm: PM, private val pm: PM,
fs: Filesystem, fs: Filesystem,
patchBundleRepository: PatchBundleRepository, private val patchBundleRepository: PatchBundleRepository,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val inputFile = savedStateHandle.saveable(key = "inputFile") { private val inputFile = savedStateHandle.saveable(key = "inputFile") {
@@ -38,19 +42,19 @@ class AppSelectorViewModel(
} }
val appList = pm.appList val appList = pm.appList
private val storageSelectionChannel = Channel<Pair<String, String>>() private val storageSelectionChannel = Channel<SelectedApp.Local>()
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow() val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default) val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
// var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null) var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
// private set private set
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" } fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
// fun dismissNonSuggestedVersionDialog() { fun dismissNonSuggestedVersionDialog() {
// nonSuggestedVersionDialogSubject = null nonSuggestedVersionDialogSubject = null
// } }
fun handleStorageResult(uri: Uri) = viewModelScope.launch { fun handleStorageResult(uri: Uri) = viewModelScope.launch {
val selectedApp = withContext(Dispatchers.IO) { val selectedApp = withContext(Dispatchers.IO) {
@@ -62,8 +66,11 @@ class AppSelectorViewModel(
return@launch return@launch
} }
// TODO: Disallow if 0 patches are compatible if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) {
storageSelectionChannel.send(selectedApp) storageSelectionChannel.send(selectedApp)
} else {
nonSuggestedVersionDialogSubject = selectedApp
}
} }
private fun loadSelectedFile(uri: Uri) = private fun loadSelectedFile(uri: Uri) =
@@ -73,7 +80,12 @@ class AppSelectorViewModel(
Files.copy(stream, toPath()) Files.copy(stream, toPath())
pm.getPackageInfo(this)?.let { packageInfo -> pm.getPackageInfo(this)?.let { packageInfo ->
Pair(packageInfo.packageName, path) SelectedApp.Local(
packageName = packageInfo.packageName,
version = packageInfo.versionName!!,
file = this,
temporary = true
)
} }
} }
} }

View File

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

View File

@@ -1,17 +1,26 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.SupportedLocales
import app.revanced.manager.util.resetListItemColorsCached import app.revanced.manager.util.resetListItemColorsCached
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale
class GeneralSettingsViewModel( class GeneralSettingsViewModel(
private val app: Application,
val prefs: PreferencesManager val prefs: PreferencesManager
) : ViewModel() { ) : ViewModel() {
fun setTheme(theme: Theme) = viewModelScope.launch { fun setTheme(theme: Theme) = viewModelScope.launch {
prefs.theme.update(theme) prefs.theme.update(theme)
resetListItemColorsCached() 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 import kotlin.io.path.inputStream
sealed class ResetDialogState( sealed class ResetDialogState(
@StringRes val titleResId: Int, @param:StringRes val titleResId: Int,
@StringRes val descriptionResId: Int, @param:StringRes val descriptionResId: Int,
val onConfirm: () -> Unit, val onConfirm: () -> Unit,
val dialogOptionName: String? = null val dialogOptionName: String? = null
) { ) {

View File

@@ -12,9 +12,11 @@ import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.SerializedSelection import app.revanced.manager.domain.repository.SerializedSelection
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@@ -29,14 +31,44 @@ import kotlinx.serialization.json.Json
class MainViewModel( class MainViewModel(
private val patchBundleRepository: PatchBundleRepository, private val patchBundleRepository: PatchBundleRepository,
private val patchSelectionRepository: PatchSelectionRepository, private val patchSelectionRepository: PatchSelectionRepository,
private val downloadedAppRepository: DownloadedAppRepository,
private val keystoreManager: KeystoreManager, private val keystoreManager: KeystoreManager,
private val app: Application, private val app: Application,
val prefs: PreferencesManager, val prefs: PreferencesManager,
private val json: Json private val json: Json
) : ViewModel() { ) : ViewModel() {
private val appSelectChannel = Channel<SelectedApp>()
val appSelectFlow = appSelectChannel.receiveAsFlow()
private val legacyImportActivityChannel = Channel<Intent>() private val legacyImportActivityChannel = Channel<Intent>()
val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow() val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow()
private suspend fun suggestedVersion(packageName: String) =
patchBundleRepository.suggestedVersions.first()[packageName]
private suspend fun findDownloadedApp(app: SelectedApp): SelectedApp.Local? {
if (app !is SelectedApp.Search) return null
val suggestedVersion = suggestedVersion(app.packageName) ?: return null
val downloadedApp =
downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true)
?: return null
return SelectedApp.Local(
downloadedApp.packageName,
downloadedApp.version,
downloadedAppRepository.getApkFileForApp(downloadedApp),
false
)
}
fun selectApp(app: SelectedApp) = viewModelScope.launch {
appSelectChannel.send(findDownloadedApp(app) ?: app)
}
fun selectApp(packageName: String) = viewModelScope.launch {
selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName)))
}
init { init {
viewModelScope.launch { viewModelScope.launch {
if (!prefs.firstLaunch.get()) return@launch if (!prefs.firstLaunch.get()) return@launch

View File

@@ -37,7 +37,7 @@ import app.revanced.manager.patcher.worker.PatcherWorker
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.InstallerModel import app.revanced.manager.ui.model.InstallerModel
import app.revanced.manager.ui.model.SelectedSource import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.Step
@@ -93,8 +93,9 @@ class PatcherViewModel(
private val ackpineInstaller: PackageInstaller = get() private val ackpineInstaller: PackageInstaller = get()
private var installedApp: InstalledApp? = null private var installedApp: InstalledApp? = null
val packageName = input.packageName private val selectedApp = input.selectedApp
val version = input.version val packageName = selectedApp.packageName
val version = selectedApp.version
var installedPackageName by savedStateHandle.saveable( var installedPackageName by savedStateHandle.saveable(
key = "installedPackageName", key = "installedPackageName",
@@ -159,7 +160,7 @@ class PatcherViewModel(
} }
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
generateSteps(app, input.selectedSource, input.selectedPatches).toMutableStateList() generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
} }
val progress by derivedStateOf { val progress by derivedStateOf {
@@ -177,9 +178,7 @@ class PatcherViewModel(
ParcelUuid( ParcelUuid(
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>( workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args( "patching", PatcherWorker.Args(
input.packageName, input.selectedApp,
input.version,
input.selectedSource,
outputFile.path, outputFile.path,
input.selectedPatches, input.selectedPatches,
input.options, input.options,
@@ -258,7 +257,7 @@ class PatcherViewModel(
super.onCleared() super.onCleared()
workManager.cancelWorkById(patcherWorkerId.uuid) workManager.cancelWorkById(patcherWorkerId.uuid)
if (input.selectedSource is SelectedSource.Installed && installedApp?.installType == InstallType.MOUNT) { if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
uiSafe(app, R.string.failed_to_mount, "Failed to mount") { uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
withTimeout(Duration.ofMinutes(1L)) { withTimeout(Duration.ofMinutes(1L)) {
@@ -382,7 +381,7 @@ class PatcherViewModel(
installedAppRepository.addOrUpdate( installedAppRepository.addOrUpdate(
installerPkgName, installerPkgName,
packageName, packageName,
input.version input.selectedApp.version
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! }, ?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
InstallType.DEFAULT, InstallType.DEFAULT,
input.selectedPatches input.selectedPatches
@@ -444,7 +443,7 @@ class PatcherViewModel(
} }
} }
val inputVersion = input.version val inputVersion = input.selectedApp.version
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName } ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
?: throw Exception("Failed to determine input APK version") ?: throw Exception("Failed to determine input APK version")
@@ -536,10 +535,10 @@ class PatcherViewModel(
fun generateSteps( fun generateSteps(
context: Context, context: Context,
selectedSource: SelectedSource, selectedApp: SelectedApp,
selectedPatches: PatchSelection selectedPatches: PatchSelection
): List<Step> = buildList { ): List<Step> = buildList {
if (selectedSource is SelectedSource.Plugin) if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
add( add(
Step( Step(
StepId.DownloadAPK, StepId.DownloadAPK,

View File

@@ -20,7 +20,7 @@ import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.model.navigation.SelectedAppInfo import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
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.saver.Nullable import app.revanced.manager.util.saver.Nullable
@@ -45,14 +45,14 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelParams) : class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
ViewModel(), KoinComponent { ViewModel(), KoinComponent {
private val app: Application = get() private val app: Application = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private val prefs: PreferencesManager = get() private val prefs: PreferencesManager = get()
private val packageName = input.packageName private val packageName = input.app.packageName
val appVersion = input.version val appVersion = input.app.version
var selectionWarningEnabled by mutableStateOf(true) var selectionWarningEnabled by mutableStateOf(true)
private set private set
@@ -62,7 +62,7 @@ class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelP
val allowIncompatiblePatches = val allowIncompatiblePatches =
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking() get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
val bundlesFlow = val bundlesFlow =
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.version) get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -88,7 +88,7 @@ class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelP
key = "selection", key = "selection",
stateSaver = selectionSaver, stateSaver = selectionSaver,
) { ) {
mutableStateOf(input.patchSelection?.toPersistentPatchSelection()) mutableStateOf(input.currentSelection?.toPersistentPatchSelection())
} }
private val patchOptions: PersistentOptions by savedStateHandle.saveable( private val patchOptions: PersistentOptions by savedStateHandle.saveable(

View File

@@ -1,188 +1,130 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Activity
import android.app.Application
import android.content.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.Parcelable import android.os.Parcelable
import android.util.Log
import androidx.activity.result.ActivityResult
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.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable import androidx.lifecycle.viewmodel.compose.saveable
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.DownloaderPluginRepository import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchOptionsRepository 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.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
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.ui.model.SelectedSource import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedVersion 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.SelectedAppInfo import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.patchCount import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
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 java.io.File
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) @OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class SelectedAppInfoViewModel( class SelectedAppInfoViewModel(
private val input: SelectedAppInfo.ViewModelParams input: SelectedApplicationInfo.ViewModelParams
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val app: Application = get()
private val bundleRepository: PatchBundleRepository = get() private val bundleRepository: PatchBundleRepository = get()
private val selectionRepository: PatchSelectionRepository = get() private val selectionRepository: PatchSelectionRepository = get()
private val optionsRepository: PatchOptionsRepository = get() private val optionsRepository: PatchOptionsRepository = get()
private val pluginsRepository: DownloaderPluginRepository = get() private val pluginsRepository: DownloaderPluginRepository = get()
private val installedAppRepository: InstalledAppRepository = get() private val installedAppRepository: InstalledAppRepository = get()
private val downloadedAppRepository: DownloadedAppRepository = get() private val rootInstaller: RootInstaller = get()
private val pm: PM = get() private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private val prefs: PreferencesManager = get() val prefs: PreferencesManager = get()
val plugins = pluginsRepository.loadedPluginsFlow val plugins = pluginsRepository.loadedPluginsFlow
val packageName = input.packageName val desiredVersion = input.app.version
val localPath = input.localPath val packageName = input.app.packageName
private val persistConfiguration = input.patches == null private val persistConfiguration = input.patches == null
val hasRoot = rootInstaller.hasRootAccess()
var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
private set
// User selection private var _selectedApp by savedStateHandle.saveable {
private var selectionFlow = MutableStateFlow( mutableStateOf(input.app)
input.patches?.let { selection ->
SelectionState.Customized(selection)
} ?: SelectionState.Default
)
private val _selectedVersion = MutableStateFlow<SelectedVersion>(SelectedVersion.Auto)
val selectedVersion: StateFlow<SelectedVersion> = _selectedVersion
private val _selectedSource = MutableStateFlow<SelectedSource>(SelectedSource.Auto)
val selectedSource: StateFlow<SelectedSource> = _selectedSource
fun updateVersion(version: SelectedVersion) {
_selectedVersion.value = version
} }
fun updateSource(source: SelectedSource) {
_selectedSource.value = source
}
fun updateConfiguration(
selection: PatchSelection?,
selectedOptions: Options
) = viewModelScope.launch {
selectionFlow.value = selection?.let(SelectionState::Customized) ?: SelectionState.Default
val filteredOptions = selectedOptions.filtered(bundleInfoFlow.first()) var selectedAppInfo: PackageInfo? by mutableStateOf(null)
options = filteredOptions private set
if (persistConfiguration) { var selectedApp
selection?.let { selectionRepository.updateSelection(packageName, it) } get() = _selectedApp
?: selectionRepository.resetSelectionForPackage(packageName) set(value) {
_selectedApp = value
optionsRepository.saveOptions(packageName, filteredOptions) invalidateSelectedAppInfo()
} }
}
init {
invalidateSelectedAppInfo()
viewModelScope.launch(Dispatchers.Main) {
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val installedAppDeferred =
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
// All patches for package installedAppData =
val bundles = bundleRepository.scopedBundleInfoFlow(packageName, null) packageInfo.await()?.let {
SelectedApp.Installed(
// Selection derived from selectionFlow packageName,
val patchSelection = combine( it.versionName!!
selectionFlow, ) to installedAppDeferred.await()
bundles,
) { selection, bundles ->
selection.patches(bundles, allowIncompatible = true)
}
val customSelection = combine(
selectionFlow,
bundles,
) { selection, bundles ->
(selection as? SelectionState.Customized)?.patches(bundles, allowIncompatible = true)
}
// Most compatible versions based on patch selection
@OptIn(ExperimentalCoroutinesApi::class)
val mostCompatibleVersions = patchSelection.flatMapLatest { patchSelection ->
bundleRepository.suggestedVersions(
packageName,
patchSelection
)
}
// Resolve actual version from user selection
val resolvedVersion = combine(
_selectedVersion,
mostCompatibleVersions,
) { selected, mostCompatible ->
when (selected) {
is SelectedVersion.Specific -> selected.version
is SelectedVersion.Any -> null
is SelectedVersion.Auto -> mostCompatible?.maxWithOrNull(
compareBy<Map.Entry<String, Int>> { it.value }
.thenBy { it.key }
)?.key
}
}
@OptIn(ExperimentalCoroutinesApi::class)
val scopedBundles = resolvedVersion.flatMapLatest { version ->
bundleRepository.scopedBundleInfoFlow(packageName, version)
}
val incompatiblePatchCount = scopedBundles.map { bundles ->
bundles.sumOf { bundle ->
bundle.incompatible.size
}
}
// Resolve actual source from user selection
val resolvedSource = combine(
_selectedSource,
resolvedVersion
) { source, version ->
when (source) {
is SelectedSource.Installed -> source
is SelectedSource.Local -> source
is SelectedSource.Downloaded -> source
is SelectedSource.Plugin -> source
is SelectedSource.Auto -> {
val app = version?.let {
downloadedAppRepository.get(packageName, it)
} }
val file = app?.let {
downloadedAppRepository.getApkFileForApp(it)
}
file?.let { SelectedSource.Downloaded(it.path, version) }
?: SelectedSource.Plugin(null)
}
} }
} }
val requiredVersion = combine(
prefs.suggestedVersionSafeguard.flow,
bundleRepository.suggestedVersions
) { suggestedVersionSafeguard, suggestedVersions ->
if (!suggestedVersionSafeguard) return@combine null
suggestedVersions[input.app.packageName]
}
val bundleInfoFlow by derivedStateOf { val bundleInfoFlow by derivedStateOf {
bundleRepository.scopedBundleInfoFlow(packageName, null) bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version)
} }
var options: Options by savedStateHandle.saveable { var options: Options by savedStateHandle.saveable {
@@ -200,42 +142,121 @@ class SelectedAppInfoViewModel(
} }
private set private set
private var selectionState: SelectionState by savedStateHandle.saveable {
if (input.patches != null)
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
val errorFlow = combine( // Try to get the previous selection if customization is enabled.
plugins, viewModelScope.launch {
resolvedSource, if (!prefs.disableSelectionWarning.get()) return@launch
) { pluginsList, source ->
val previous = selectionRepository.getSelection(packageName)
if (previous.values.sumOf { it.size } == 0) return@launch
selectionState = SelectionState.Customized(previous)
}
mutableStateOf(SelectionState.Default)
}
var showSourceSelector by mutableStateOf(false)
private set
private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null)
val activePluginAction get() = pluginAction?.first?.packageName
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
private val launchActivityChannel = Channel<Intent>()
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
when { when {
source is SelectedSource.Plugin && pluginsList.isEmpty() -> Error.NoPlugins app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
else -> null else -> null
} }
} }
fun showSourceSelector() {
dismissSourceSelector()
// var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null) showSourceSelector = true
// private set
private var _selectedApp by savedStateHandle.saveable {
mutableStateOf(null)
} }
var selectedAppInfo: PackageInfo? by mutableStateOf(null) private fun cancelPluginAction() {
private set pluginAction?.second?.cancel()
pluginAction = null
}
var selectedApp fun dismissSourceSelector() {
get() = _selectedApp cancelPluginAction()
set(value) { showSourceSelector = false
_selectedApp = value }
invalidateSelectedAppInfo()
fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) {
cancelPluginAction()
pluginAction = plugin to viewModelScope.launch {
try {
val scope = object : GetScope {
override val hostPackageName = app.packageName
override val pluginPackageName = plugin.packageName
override suspend fun requestStartActivity(intent: Intent) =
withContext(Dispatchers.Main) {
if (launchedActivity != null) error("Previous activity has not finished")
try {
val result = with(CompletableDeferred<ActivityResult>()) {
launchedActivity = this
launchActivityChannel.send(intent)
await()
}
when (result.resultCode) {
Activity.RESULT_OK -> result.data
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
else -> throw UserInteractionException.Activity.NotCompleted(
result.resultCode,
result.data
)
}
} finally {
launchedActivity = null
}
}
}
withContext(Dispatchers.IO) {
plugin.get(scope, packageName, desiredVersion)
}?.let { (data, version) ->
if (desiredVersion != null && version != desiredVersion) {
app.toast(app.getString(R.string.downloader_invalid_version))
return@launch
}
selectedApp = SelectedApp.Download(
packageName,
version,
ParceledDownloaderData(plugin, data)
)
} ?: app.toast(app.getString(R.string.downloader_app_not_found))
} catch (e: UserInteractionException.Activity) {
app.toast(e.message!!)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
Log.e(tag, "Downloader.get threw an exception", e)
} finally {
pluginAction = null
dismissSourceSelector()
}
}
}
fun handlePluginActivityResult(result: ActivityResult) {
launchedActivity?.complete(result)
}
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
val info = when (val app = selectedApp) {
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
else -> null
} }
selectedAppInfo = info
// TODO: Load from local file or downloaded app
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
selectedAppInfo = pm.getPackageInfo(packageName)
} }
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles) fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
@@ -251,50 +272,37 @@ class SelectedAppInfoViewModel(
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get() val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
val bundles = bundleInfoFlow.first() val bundles = bundleInfoFlow.first()
return Patcher.ViewModelParams( return Patcher.ViewModelParams(
input.packageName, selectedApp,
resolvedVersion.first(), getPatches(bundles, allowIncompatible),
resolvedSource.first(),
patchSelection.first(),
getOptionsFiltered(bundles) getOptionsFiltered(bundles)
) )
} }
init { fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
invalidateSelectedAppInfo() selectionState.patches(bundles, allowIncompatible)
input.localPath?.let { local -> fun getCustomPatches(
viewModelScope.launch { bundles: List<PatchBundleInfo.Scoped>,
val packageInfo = pm.getPackageInfo(File(local)) allowIncompatible: Boolean
): PatchSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
_selectedVersion.value = SelectedVersion.Specific(
packageInfo?.versionName ?: return@launch
)
_selectedSource.value = SelectedSource.Local(local)
}
}
// Get the previous selection if customization is enabled. fun updateConfiguration(
viewModelScope.launch { selection: PatchSelection?,
if (prefs.disableSelectionWarning.get()) { options: Options
val previous = selectionRepository.getSelection(packageName) ) = viewModelScope.launch {
if (previous.patchCount == 0) return@launch selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
selectionFlow.value = SelectionState.Customized(previous)
}
}
// Get installed app info val filteredOptions = options.filtered(bundleInfoFlow.first())
viewModelScope.launch { this@SelectedAppInfoViewModel.options = filteredOptions
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val installedAppDeferred =
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
// installedAppData = if (!persistConfiguration) return@launch
// packageInfo.await()?.let { viewModelScope.launch(Dispatchers.Default) {
// SelectedApp.Installed( selection?.let { selectionRepository.updateSelection(packageName, it) }
// packageName, ?: selectionRepository.resetSelectionForPackage(packageName)
// it.versionName!!
// ) to installedAppDeferred.await() optionsRepository.saveOptions(packageName, filteredOptions)
// }
} }
} }

View File

@@ -1,136 +0,0 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.network.downloader.DownloaderPluginState
import app.revanced.manager.ui.model.SelectedSource
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
import app.revanced.manager.util.PM
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.io.File
class SourceSelectorViewModel(
val input: SelectedAppInfo.SourceSelector.ViewModelParams
) : ViewModel(), KoinComponent {
private val app: Application = get()
private val downloadedAppRepository: DownloadedAppRepository = get()
private val pluginRepository: DownloaderPluginRepository = get()
private val installedAppRepository: InstalledAppRepository = get()
private val pm: PM = get()
var selectedSource by mutableStateOf(input.selectedSource)
private set
fun selectSource(source: SelectedSource) {
selectedSource = source
}
var localApp by mutableStateOf<SourceOption?>(null)
private set
val downloadedApps = downloadedAppRepository.get(input.packageName)
.map { apps ->
apps.sortedByDescending { app -> app.version }
.map {
SourceOption(
source = SelectedSource.Downloaded(
path = downloadedAppRepository.getApkFileForApp(it).path,
version = it.version
),
title = it.version,
category = "Downloaded",
key = it.version,
disableReason = if (input.version != null && it.version != input.version) {
DisableReason.VERSION_NOT_MATCHING
} else null
)
}
}
val plugins = pluginRepository.pluginStates.map { plugins ->
plugins.toList().sortedByDescending { it.second is DownloaderPluginState.Loaded }
.map {
val packageInfo = pm.getPackageInfo(it.first)
val label = packageInfo?.applicationInfo?.loadLabel(app.packageManager)
?.toString()
SourceOption(
source = SelectedSource.Plugin(it.first),
title = label ?: it.first,
category = "Plugin",
key = it.first,
disableReason = when (it.second) {
is DownloaderPluginState.Loaded -> null
is DownloaderPluginState.Untrusted -> DisableReason.NOT_TRUSTED
is DownloaderPluginState.Failed -> DisableReason.FAILED_TO_LOAD
}
)
}
}
fun getPackageInfo(packageName: String) = pm.getPackageInfo(packageName)
var installedSource by mutableStateOf<SourceOption?>(null)
private set
init {
viewModelScope.launch {
val packageInfo = pm.getPackageInfo(input.packageName) ?: return@launch
val installedApp = installedAppRepository.get(input.packageName)
installedSource = SourceOption(
source = SelectedSource.Installed,
title = packageInfo.versionName.toString(),
category = "Installed",
key = input.packageName,
disableReason = when {
installedApp != null -> DisableReason.ALREADY_PATCHED
input.version != null && packageInfo.versionName != input.version -> DisableReason.VERSION_NOT_MATCHING
else -> null
}
)
}
input.localPath?.let { local ->
viewModelScope.launch {
val packageInfo = pm.getPackageInfo(File(local))
?: return@launch
localApp = SourceOption(
source = SelectedSource.Local(local),
title = packageInfo.versionName.toString(),
category = "Local",
key = "local",
disableReason = if (input.version != null && packageInfo.versionName != input.version) {
DisableReason.VERSION_NOT_MATCHING
} else null
)
}
}
}
enum class DisableReason(val message: String) {
VERSION_NOT_MATCHING("Does not match the selected version"),
ALREADY_PATCHED("Already patched"),
NOT_TRUSTED("Not trusted"),
FAILED_TO_LOAD("Failed to load"),
}
data class SourceOption(
val source: SelectedSource,
val title: String,
val category: String? = null,
val key: String,
val disableReason: DisableReason? = null
)
}

View File

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

View File

@@ -1,85 +0,0 @@
package app.revanced.manager.ui.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.ui.model.SelectedVersion
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
import app.revanced.manager.util.PM
import app.revanced.manager.util.patchCount
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.io.File
class VersionSelectorViewModel(
val input: SelectedAppInfo.VersionSelector.ViewModelParams
) : ViewModel(), KoinComponent {
private val patchBundleRepository: PatchBundleRepository = get()
private val downloadedAppsRepository: DownloadedAppRepository = get()
private val installedAppRepository: InstalledAppRepository = get()
private val pm: PM = get()
val patchCount = input.patchSelection.patchCount
val downloadedVersions = downloadedAppsRepository.get(input.packageName)
.map { apps ->
apps.map { it.version }
}
private val _localVersion = MutableStateFlow<String?>(null)
val localVersion: StateFlow<String?> = _localVersion
val availableVersions = combine(
patchBundleRepository.suggestedVersions(input.packageName, input.patchSelection),
_localVersion,
) { versions, local ->
versions.orEmpty()
.let { versions ->
local?.let {
versions.toMutableMap().also { it.putIfAbsent(local, 0) }
} ?: versions
}
.map { (key, value) -> SelectedVersion.Specific(key) to patchCount - value }
.sortedWith(
compareBy<Pair<SelectedVersion.Specific, Int>>{ it.second }
.thenByDescending { it.first.version }
)
}
var installedAppVersion by mutableStateOf<String?>(null)
init {
viewModelScope.launch {
val currentApp = pm.getPackageInfo(input.packageName)
val patchedApp = installedAppRepository.get(input.packageName)
// Skip if installed app is patched
if (patchedApp?.currentPackageName == input.packageName) return@launch
installedAppVersion = currentApp?.versionName
}
input.localPath?.let { local ->
viewModelScope.launch {
val packageInfo = pm.getPackageInfo(File(local))
_localVersion.value = packageInfo?.versionName
}
}
}
var selectedVersion by mutableStateOf(input.selectedVersion)
private set
fun selectVersion(version: SelectedVersion) {
selectedVersion = version
}
}

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

View File

@@ -14,7 +14,7 @@ Second \"item\" text"</string>
--> -->
<resources> <resources>
<string name="app_name">ReVanced Manager</string> <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="patches">Patches</string>
<string name="cli">CLI</string> <string name="cli">CLI</string>
<string name="manager">Manager</string> <string name="manager">Manager</string>
@@ -61,15 +61,14 @@ Second \"item\" text"</string>
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</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">Patches</string> <string name="patch_selector_item">Select patches</string>
<string name="patch_selector_item_description">%d 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="version_selector_item">Version</string>
<string name="network_unavailable_warning">Your device is not connected to the internet. Downloading will fail later.</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="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">APK source</string> <string name="apk_source_selector_item">Select APK source</string>
<string name="apk_source_auto">Using all APK downloaders</string> <string name="apk_source_auto">Using all APK downloaders</string>
<string name="apk_source_downloader">Using %s</string> <string name="apk_source_downloader">Using %s</string>
<string name="apk_source_installed">Using installed APK</string> <string name="apk_source_installed">Using installed APK</string>
@@ -84,7 +83,7 @@ Second \"item\" text"</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">Language, theme, dynamic color</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">Downloads</string>
@@ -105,6 +104,9 @@ Second \"item\" text"</string>
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string> <string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="theme_description">Choose between light or dark 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="safeguards">Safeguards</string>
<string name="patch_compat_check">Disable version compatibility check</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> <string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
@@ -203,7 +205,7 @@ You will not be able to update the previously installed apps from this source."<
<string name="share">Share</string> <string name="share">Share</string>
<string name="patch">Patch</string> <string name="patch">Patch</string>
<string name="select_from_storage">Select from storage</string> <string name="select_from_storage">Select from storage</string>
<string name="select_from_storage_description">Select an APK file from storage</string> <string name="select_from_storage_description">Select an APK file from storage using file picker</string>
<string name="suggested_version_info">Suggested version: %s</string> <string name="suggested_version_info">Suggested version: %s</string>
<string name="type_anything">Type anything to continue</string> <string name="type_anything">Type anything to continue</string>
<string name="search">Search patches…</string> <string name="search">Search patches…</string>
@@ -218,6 +220,10 @@ You will not be able to update the previously installed apps from this source."<
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="appearance">Appearance</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="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 (experimental)</string>
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</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.serialization) apply false
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.about.libraries.android) apply false
alias(libs.plugins.compose.compiler) 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] [versions]
ktx = "1.16.0" ktx = "1.17.0"
material3 = "1.3.2" material3 = "1.4.0"
ui-tooling = "1.8.1" ui-tooling = "1.10.0"
viewmodel-lifecycle = "2.9.0" viewmodel-lifecycle = "2.10.0"
splash-screen = "1.0.1" splash-screen = "1.2.0"
activity = "1.10.1" activity = "1.12.2"
appcompat = "1.7.0" appcompat = "1.7.1"
preferences-datastore = "1.1.2" preferences-datastore = "1.2.0"
work-runtime = "2.10.1" work-runtime = "2.11.0"
compose-bom = "2025.05.00" compose-bom = "2025.12.01"
navigation = "2.8.6" navigation = "2.9.6"
accompanist = "0.37.0" accompanist = "0.37.3"
placeholder = "1.1.2" placeholder = "1.0.12"
reorderable = "2.4.3" reorderable = "3.0.0"
serialization = "1.8.0" serialization = "1.9.0"
collection = "0.3.8" collection = "0.4.0"
datetime = "0.6.1" datetime = "0.7.1"
room-version = "2.7.1" room-version = "2.8.4"
revanced-patcher = "21.0.0" revanced-patcher = "21.0.0"
revanced-library = "3.0.2" revanced-library = "3.0.2"
koin = "3.5.3" koin = "4.1.1"
ktor = "2.3.9" ktor = "3.3.3"
markdown-renderer = "0.30.0" markdown-renderer = "0.39.0"
fading-edges = "1.0.4" fading-edges = "1.0.4"
kotlin = "2.1.10" kotlin = "2.3.0"
android-gradle-plugin = "8.9.1" android-gradle-plugin = "8.13.2"
dev-tools-gradle-plugin = "2.1.10-1.0.29" dev-tools-gradle-plugin = "2.3.4"
about-libraries-gradle-plugin = "12.1.2" about-libraries = "13.2.1"
coil = "2.7.0" coil = "2.7.0"
app-icon-loader-coil = "1.5.0" app-icon-loader-coil = "1.5.0"
libsu = "6.0.0" libsu = "6.0.0"
@@ -34,10 +34,10 @@ scrollbars = "1.0.4"
enumutil = "1.1.1" enumutil = "1.1.1"
compose-icons = "1.2.4" compose-icons = "1.2.4"
kotlin-process = "1.5.1" kotlin-process = "1.5.1"
hidden-api-stub = "4.3.3" hidden-api-stub = "4.4.0"
binary-compatibility-validator = "0.17.0" binary-compatibility-validator = "0.18.1"
semver-parser = "3.0.0" semver-parser = "3.0.0"
ackpine = "0.18.5" ackpine = "0.19.1"
[libraries] [libraries]
# AndroidX Core # 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" } accompanist-drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "accompanist" }
# Placeholder # 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
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } 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" } koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
# About Libraries # 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
ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "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" } 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" }
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" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }