Compare commits

..

9 Commits

Author SHA1 Message Date
Robert
b28e9a15be Add local APK to version and source selector 2026-01-08 22:50:50 +01:00
Robert
9fc2b4fdef Show selected source in overview 2026-01-07 20:51:39 +01:00
Robert
08662b2132 Use selected source in patcher 2026-01-07 00:42:21 +01:00
Robert
0169fd2109 Merge branch 'dev' into feat/improve-user-flow 2026-01-06 19:33:12 +01:00
Robert
c53d0462d6 Show selected source in overview 2026-01-03 20:56:45 +01:00
Robert
af8f2afa36 Implement basic source selector UI 2025-12-31 00:14:54 +01:00
Robert
9cdb8eafb3 Update selected app info screen and version selector screen 2025-12-30 21:19:22 +01:00
Robert
fda0e1697b feat: replace SelectedApp with packageName in selector 2025-12-30 17:58:54 +01:00
Robert
2d98923f50 feat: Separate version and source selection 2025-12-30 16:15:16 +01:00
61 changed files with 1251 additions and 1115 deletions

View File

@@ -1,43 +0,0 @@
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)"

View File

@@ -1,26 +0,0 @@
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,6 +24,14 @@ 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
} }
@@ -77,6 +85,14 @@ 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 {
} }
@@ -143,6 +159,14 @@ 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,5 +1,3 @@
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)
@@ -19,16 +17,9 @@ 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 = 36 compileSdk = 35
defaultConfig { defaultConfig {
minSdk = 26 minSdk = 26
@@ -51,6 +42,10 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions {
jvmTarget = "17"
}
buildFeatures { buildFeatures {
aidl = true aidl = true
} }

View File

@@ -1,36 +1,3 @@
# 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,7 +1,4 @@
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 {
@@ -12,7 +9,6 @@ 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
} }
@@ -85,8 +81,7 @@ dependencies {
implementation(libs.koin.workmanager) implementation(libs.koin.workmanager)
// Licenses // Licenses
implementation(libs.about.libraries.core) implementation(libs.about.libraries)
implementation(libs.about.libraries.m3)
// Ktor // Ktor
implementation(libs.ktor.core) implementation(libs.ktor.core)
@@ -131,7 +126,7 @@ buildscript {
android { android {
namespace = "app.revanced.manager" namespace = "app.revanced.manager"
compileSdk = 36 compileSdk = 35
buildToolsVersion = "35.0.1" buildToolsVersion = "35.0.1"
defaultConfig { defaultConfig {
@@ -148,25 +143,13 @@ 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")
} }
@@ -238,14 +221,20 @@ 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
} }
androidResources { android {
generateLocaleConfig = true androidResources {
generateLocaleConfig = true
}
} }
externalNativeBuild { externalNativeBuild {
@@ -258,18 +247,6 @@ 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.20 version = 1.26.0-dev.16

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.SelectedApplicationInfo import app.revanced.manager.ui.model.navigation.SelectedAppInfo
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,7 +41,9 @@ 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
@@ -59,10 +61,11 @@ 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 : AppCompatActivity() { class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi @ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -94,23 +97,16 @@ class MainActivity : AppCompatActivity() {
dynamicColor = dynamicColor, dynamicColor = dynamicColor,
pureBlackTheme = pureBlackTheme pureBlackTheme = pureBlackTheme
) { ) {
ReVancedManager(vm) ReVancedManager()
} }
} }
} }
} }
@Composable @Composable
private fun ReVancedManager(vm: MainViewModel) { private fun ReVancedManager() {
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,
@@ -141,7 +137,12 @@ private fun ReVancedManager(vm: MainViewModel) {
val data = it.toRoute<InstalledApplicationInfo>() val data = it.toRoute<InstalledApplicationInfo>()
InstalledAppInfoScreen( InstalledAppInfoScreen(
onPatchClick = vm::selectApp, onPatchClick = { packageName ->
navController.navigateComplex(
SelectedAppInfo,
SelectedAppInfo.ViewModelParams(packageName)
)
},
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
viewModel = koinViewModel { parametersOf(data.packageName) } viewModel = koinViewModel { parametersOf(data.packageName) }
) )
@@ -149,8 +150,20 @@ private fun ReVancedManager(vm: MainViewModel) {
composable<AppSelector> { composable<AppSelector> {
AppSelectorScreen( AppSelectorScreen(
onSelect = vm::selectApp, onSelect = { packageName ->
onStorageSelect = vm::selectApp, navController.navigateComplex(
SelectedAppInfo,
SelectedAppInfo.ViewModelParams(packageName)
)
},
onStorageSelect = { packageName, localPath ->
navController.navigateComplex(
SelectedAppInfo,
SelectedAppInfo.ViewModelParams(
packageName, localPath
)
)
},
onBackClick = navController::popBackStack onBackClick = navController::popBackStack
) )
} }
@@ -178,13 +191,13 @@ private fun ReVancedManager(vm: MainViewModel) {
) )
} }
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) { navigation<SelectedAppInfo>(startDestination = SelectedAppInfo.Main) {
composable<SelectedApplicationInfo.Main> { composable<SelectedAppInfo.Main> {
val parentBackStackEntry = navController.navGraphEntry(it) val parentBackStackEntry = navController.navGraphEntry(it)
val data = val data =
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>() parentBackStackEntry.getComplexArg<SelectedAppInfo.ViewModelParams>()
val viewModel = val viewModel =
koinViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) { koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
parametersOf(data) parametersOf(data)
} }
@@ -198,23 +211,47 @@ private fun ReVancedManager(vm: MainViewModel) {
) )
} }
}, },
onPatchSelectorClick = { app, patches, options -> onPatchSelectorClick = { packageName, version, patchSelection, options ->
navController.navigateComplex( navController.navigateComplex(
SelectedApplicationInfo.PatchesSelector, SelectedAppInfo.PatchesSelector,
SelectedApplicationInfo.PatchesSelector.ViewModelParams( SelectedAppInfo.PatchesSelector.ViewModelParams(
app, packageName,
patches, version,
options patchSelection,
options,
) )
) )
}, },
onRequiredOptions = { app, patches, options -> onRequiredOptions = { packageName, version, patchSelection, options ->
navController.navigateComplex( navController.navigateComplex(
SelectedApplicationInfo.RequiredOptions, SelectedAppInfo.RequiredOptions,
SelectedApplicationInfo.PatchesSelector.ViewModelParams( SelectedAppInfo.PatchesSelector.ViewModelParams(
app, packageName,
patches, version,
options patchSelection,
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,
) )
) )
}, },
@@ -222,10 +259,10 @@ private fun ReVancedManager(vm: MainViewModel) {
) )
} }
composable<SelectedApplicationInfo.PatchesSelector> { composable<SelectedAppInfo.PatchesSelector> {
val data = val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>() it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>( val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it) viewModelStoreOwner = navController.navGraphEntry(it)
) )
@@ -239,10 +276,44 @@ private fun ReVancedManager(vm: MainViewModel) {
) )
} }
composable<SelectedApplicationInfo.RequiredOptions> { composable<SelectedAppInfo.VersionSelector> {
val data = val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>() it.getComplexArg<SelectedAppInfo.VersionSelector.ViewModelParams>()
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>( val selectedAppInfoVm = koinNavViewModel<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(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered()) fun isSafe() = isConnected() && isUnmetered()
} }

View File

@@ -2,7 +2,6 @@ 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
@@ -12,6 +11,9 @@ 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.core.module.dsl.* import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module import org.koin.dsl.module
val viewModelModule = module { val viewModelModule = module {
@@ -24,4 +24,6 @@ val viewModelModule = module {
viewModelOf(::InstalledAppInfoViewModel) viewModelOf(::InstalledAppInfoViewModel)
viewModelOf(::UpdatesSettingsViewModel) viewModelOf(::UpdatesSettingsViewModel)
viewModelOf(::BundleListViewModel) viewModelOf(::BundleListViewModel)
viewModelOf(::VersionSelectorViewModel)
viewModelOf(::SourceSelectorViewModel)
} }

View File

@@ -34,6 +34,4 @@ 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,6 +30,8 @@ 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,6 +26,7 @@ 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
@@ -74,6 +75,17 @@ 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()
@@ -286,29 +298,28 @@ class PatchBundleRepository(
State(sources.toPersistentMap(), info.toPersistentMap()) State(sources.toPersistentMap(), info.toPersistentMap())
} }
suspend fun createLocal(createStream: suspend () -> InputStream) = suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
dispatchAction("Add bundle") { with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) { try {
try { createStream().use { patches -> replace(patches) }
createStream().use { patches -> replace(patches) } } catch (e: Exception) {
} catch (e: Exception) { if (e is CancellationException) throw e
if (e is CancellationException) throw e Log.e(tag, "Got exception while importing bundle", e)
Log.e(tag, "Got exception while importing bundle", e) withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
}
deleteLocalFile()
} }
}
doReload() deleteLocalFile()
}
} }
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, force = true) update(src)
state.copy(sources = state.sources.put(src.uid, src)) state.copy(sources = state.sources.put(src.uid, src))
} }
@@ -330,38 +341,32 @@ class PatchBundleRepository(
state.copy(sources = state.sources.put(uid, newSrc)) state.copy(sources = state.sources.put(uid, newSrc))
} }
suspend fun update( suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
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, force = force) { it.uid in uids }) store.dispatch(Update(showToast = showToast) { it.uid in uids })
} }
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true)) suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
/** /**
* Updates all bundles that should be automatically updated. * Updates all bundles that should be automatically updated.
*/ */
suspend fun updateCheck() = suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
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 (redownload) "Redownload remote bundles" else "Update check" override fun toString() = if (force) "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(force)) { if (!networkInfo.isSafe()) {
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
} }
@@ -374,7 +379,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 (redownload) downloadLatest() else update() if (force) 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): Map<Int, Set<String>> = suspend fun getSelection(packageName: String): app.revanced.manager.util.PatchSelection =
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,11 +16,8 @@ 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
@@ -72,12 +69,14 @@ 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) {
val channel: ByteReadChannel = httpResponse.body() while (!channel.isClosedForRead) {
val sink = outputStream.asSink()
while (!channel.exhausted()) {
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong()) val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
packet.transferTo(sink) while (packet.isNotEmpty) {
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.SelectedApp import app.revanced.manager.ui.model.SelectedSource
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,7 +67,9 @@ class PatcherWorker(
private val rootInstaller: RootInstaller by inject() private val rootInstaller: RootInstaller by inject()
class Args( class Args(
val input: SelectedApp, val packageName: String,
val version: String?,
val source: SelectedSource,
val output: String, val output: String,
val selectedPatches: PatchSelection, val selectedPatches: PatchSelection,
val options: Options, val options: Options,
@@ -75,9 +77,7 @@ 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.input is SelectedApp.Installed) { if (args.source is SelectedSource.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,32 +155,23 @@ class PatcherWorker(
plugin, plugin,
data, data,
args.packageName, args.packageName,
args.input.version, args.version,
prefs.suggestedVersionSafeguard.get(), prefs.suggestedVersionSafeguard.get(),
!prefs.disablePatchVersionCompatCheck.get(), !prefs.disablePatchVersionCompatCheck.get(),
onDownload = { progress -> ) { 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 selectedApp = args.input) { val inputFile = when (val source = args.source) {
is SelectedApp.Download -> { is SelectedSource.Auto -> throw Exception("Auto source is not supported in worker.")
runStep(StepId.DownloadAPK, args.onEvent) {
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
selectedApp.data
)
download(plugin, data) is SelectedSource.Plugin -> {
}
}
is SelectedApp.Search -> {
runStep(StepId.DownloadAPK, args.onEvent) { runStep(StepId.DownloadAPK, args.onEvent) {
downloaderPluginRepository.loadedPluginsFlow.first() downloaderPluginRepository.loadedPluginsFlow.first()
.firstNotNullOfOrNull { plugin -> .firstNotNullOfOrNull { plugin ->
@@ -206,10 +197,10 @@ class PatcherWorker(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
plugin.get( plugin.get(
getScope, getScope,
selectedApp.packageName, args.packageName,
selectedApp.version args.version
) )
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } }?.takeIf { (_, version) -> args.version == null || version == args.version }
} catch (e: UserInteractionException.Activity.NotCompleted) { } catch (e: UserInteractionException.Activity.NotCompleted) {
throw e throw e
} catch (_: UserInteractionException) { } catch (_: UserInteractionException) {
@@ -219,8 +210,10 @@ class PatcherWorker(
} }
} }
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } is SelectedSource.Downloaded -> File(source.path)
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir) is SelectedSource.Local -> File(source.path)
is SelectedSource.Installed -> File(pm.getPackageInfo(args.packageName)!!.applicationInfo!!.sourceDir)
} }
val runtime = if (prefs.useProcessRuntime.get()) { val runtime = if (prefs.useProcessRuntime.get()) {
@@ -258,9 +251,7 @@ class PatcherWorker(
Result.failure() Result.failure()
} finally { } finally {
patchedApk.delete() patchedApk.delete()
if (args.input is SelectedApp.Local && args.input.temporary) { if (args.source is SelectedSource.Local) File(args.source.path).delete()
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 com.eygraber.compose.placeholder.material3.placeholder import io.github.fornewid.placeholder.material3.placeholder
@Composable @Composable
fun AppIcon( fun AppIcon(

View File

@@ -15,8 +15,9 @@ 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 com.eygraber.compose.placeholder.material3.placeholder import io.github.fornewid.placeholder.material3.placeholder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -47,6 +48,8 @@ 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.circularIndeterminateTrackColor, trackColor: Color = ProgressIndicatorDefaults.circularTrackColor,
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
) { ) {
progress()?.let { progress()?.let {

View File

@@ -18,6 +18,8 @@ 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,8 +29,7 @@ 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,8 +31,7 @@ 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(@param:StringRes val displayName: Int) { enum class StepCategory(@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

@@ -1,35 +0,0 @@
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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,11 @@
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,7 +1,8 @@
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.SelectedApp import app.revanced.manager.ui.model.SelectedSource
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
@@ -23,10 +24,11 @@ data class InstalledApplicationInfo(val packageName: String)
data class Update(val downloadOnScreenEntry: Boolean = false) data class Update(val downloadOnScreenEntry: Boolean = false)
@Serializable @Serializable
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> { data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams> {
@Parcelize @Parcelize
data class ViewModelParams( data class ViewModelParams(
val app: SelectedApp, val packageName: String,
val localPath: String? = null,
val patches: PatchSelection? = null val patches: PatchSelection? = null
) : Parcelable ) : Parcelable
@@ -37,12 +39,35 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> { data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
@Parcelize @Parcelize
data class ViewModelParams( data class ViewModelParams(
val app: SelectedApp, val packageName: String,
val currentSelection: PatchSelection?, val version: String?,
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>
} }
@@ -51,7 +76,9 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
data object Patcher : ComplexParameter<Patcher.ViewModelParams> { data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
@Parcelize @Parcelize
data class ViewModelParams( data class ViewModelParams(
val selectedApp: SelectedApp, val packageName: String,
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,7 +44,6 @@ 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
@@ -54,13 +53,13 @@ import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppSelectorScreen( fun AppSelectorScreen(
onSelect: (String) -> Unit, onSelect: (packageName: String) -> Unit,
onStorageSelect: (SelectedApp.Local) -> Unit, onStorageSelect: (packageName: String, path: String) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: AppSelectorViewModel = koinViewModel() vm: AppSelectorViewModel = koinViewModel()
) { ) {
EventEffect(flow = vm.storageSelectionFlow) { EventEffect(flow = vm.storageSelectionFlow) {
onStorageSelect(it) onStorageSelect(it.first, it.second)
} }
val pickApkLauncher = val pickApkLauncher =
@@ -83,12 +82,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(
@@ -115,8 +114,7 @@ fun AppSelectorScreen(
) )
}, },
headlineContent = { AppLabel(app.packageInfo) }, headlineContent = { AppLabel(app.packageInfo) },
supportingContent = { Text(app.packageName) }, supportingContent = app.patches?.let {
trailingContent = app.patches?.let {
{ {
Text( Text(
pluralStringResource( pluralStringResource(
@@ -214,12 +212,7 @@ fun AppSelectorScreen(
defaultText = app.packageName defaultText = app.packageName
) )
}, },
supportingContent = { supportingContent = app.patches?.let {
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.SecondaryTabRow import androidx.compose.material3.TabRow
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,7 +53,6 @@ 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
@@ -101,7 +100,6 @@ 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,
@@ -236,7 +234,7 @@ fun DashboardScreen(
when (pagerState.currentPage) { when (pagerState.currentPage) {
DashboardPage.DASHBOARD.ordinal -> { DashboardPage.DASHBOARD.ordinal -> {
if (availablePatches < 1) { if (availablePatches < 1) {
androidContext.toast(resources.getString(R.string.no_patch_found)) androidContext.toast(androidContext.getString(R.string.no_patch_found))
composableScope.launch { composableScope.launch {
pagerState.animateScrollToPage( pagerState.animateScrollToPage(
DashboardPage.BUNDLES.ordinal DashboardPage.BUNDLES.ordinal
@@ -261,7 +259,7 @@ fun DashboardScreen(
} }
) { paddingValues -> ) { paddingValues ->
Column(Modifier.padding(paddingValues)) { Column(Modifier.padding(paddingValues)) {
SecondaryTabRow( TabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) { ) {

View File

@@ -40,7 +40,6 @@ 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
@@ -70,7 +69,6 @@ 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)
@@ -81,7 +79,7 @@ fun PatcherScreen(
fun onPageBack() = when { fun onPageBack() = when {
patcherSucceeded == null -> showDismissConfirmationDialog = true patcherSucceeded == null -> showDismissConfirmationDialog = true
viewModel.isInstalling -> context.toast(resources.getString(R.string.patcher_install_in_progress)) viewModel.isInstalling -> context.toast(context.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.SecondaryScrollableTabRow import androidx.compose.material3.ScrollableTabRow
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,12 +49,10 @@ 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
@@ -83,11 +81,9 @@ 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, FlowPreview::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun PatchesSelectorScreen( fun PatchesSelectorScreen(
onSave: (PatchSelection?, Options) -> Unit, onSave: (PatchSelection?, Options) -> Unit,
@@ -235,8 +231,7 @@ 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 = patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
true
// Toggle the patch otherwise // Toggle the patch otherwise
else -> viewModel.togglePatch(uid, patch) else -> viewModel.togglePatch(uid, patch)
@@ -365,21 +360,6 @@ 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(
@@ -395,7 +375,8 @@ fun PatchesSelectorScreen(
contentDescription = stringResource(R.string.save) contentDescription = stringResource(R.string.save)
) )
}, },
expanded = expanded, expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true,
onClick = { onClick = {
onSave(viewModel.getCustomSelection(), viewModel.getOptions()) onSave(viewModel.getCustomSelection(), viewModel.getOptions())
} }
@@ -411,7 +392,7 @@ fun PatchesSelectorScreen(
.padding(top = 16.dp) .padding(top = 16.dp)
) { ) {
if (bundles.size > 1) { if (bundles.size > 1) {
SecondaryScrollableTabRow( ScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) { ) {
@@ -517,7 +498,7 @@ private fun PatchItem(
leadingContent = { leadingContent = {
HapticCheckbox( HapticCheckbox(
checked = selected, checked = selected,
onCheckedChange = { onToggle() }, onCheckedChange = null,
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.SecondaryScrollableTabRow import androidx.compose.material3.ScrollableTabRow
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) SecondaryScrollableTabRow( else if (list.size > 1) ScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) { ) {

View File

@@ -1,16 +1,12 @@
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.PaddingValues import androidx.compose.foundation.layout.ColumnScope
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
@@ -21,82 +17,69 @@ 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.SelectedApp import app.revanced.manager.ui.model.SelectedSource
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: (SelectedApp, PatchSelection?, Options) -> Unit, onPatchSelectorClick: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit, onRequiredOptions: (packageName: String, version: String?, 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.selectedApp.packageName val packageName = vm.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(
@@ -119,18 +102,18 @@ fun SelectedAppInfoScreen(
) )
}, },
onClick = patchClick@{ onClick = patchClick@{
if (selectedPatchCount == 0) { if (patchCount == 0) {
context.toast(resources.getString(R.string.no_patches_selected)) context.toast(context.getString(R.string.no_patches_selected))
return@patchClick return@patchClick
} }
composableScope.launch { composableScope.launch {
if (!vm.hasSetRequiredOptions(patches)) { if (!vm.hasSetRequiredOptions(fullPatchSelection)) {
onRequiredOptions( onRequiredOptions(
vm.selectedApp, vm.packageName,
vm.getCustomPatches(bundles, allowIncompatiblePatches), resolvedVersion,
vm.options customSelection,
vm.options,
) )
return@launch return@launch
} }
@@ -142,94 +125,96 @@ 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) {
Text( vm.selectedAppInfo?.let {
version ?: stringResource(R.string.selected_app_meta_any_version), Text(
color = MaterialTheme.colorScheme.onSurfaceVariant, it.packageName,
style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant,
) style = MaterialTheme.typography.bodyMedium,
)
}
} }
PageItem( PageItem(
R.string.patch_selector_item, R.string.patch_selector_item,
stringResource( stringResource(R.string.patch_selector_item_description, patchCount),
R.string.patch_selector_item_description,
selectedPatchCount
),
onClick = { onClick = {
onPatchSelectorClick( onPatchSelectorClick(
vm.selectedApp, vm.packageName,
vm.getCustomPatches( resolvedVersion,
bundles, customSelection,
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,
when (val app = vm.selectedApp) { sourceDescription,
is SelectedApp.Search -> stringResource(R.string.apk_source_auto) onClick = { onSourceClick(
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed) packageName,
is SelectedApp.Download -> stringResource( resolvedVersion,
R.string.apk_source_downloader, selectedSource,
plugins.find { it.packageName == app.data.pluginPackageName }?.name vm.localPath,
?: 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 = 24.dp) modifier = Modifier.padding(horizontal = 16.dp)
) )
} }
Column( if (resolvedSource is SelectedSource.Plugin) Column(
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.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,
@@ -238,7 +223,6 @@ fun SelectedAppInfoScreen(
onDismiss = null onDismiss = null
) )
} }
networkMetered -> { networkMetered -> {
NotificationCard( NotificationCard(
isWarning = true, isWarning = true,
@@ -254,11 +238,17 @@ fun SelectedAppInfoScreen(
} }
@Composable @Composable
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) { private fun PageItem(
@StringRes title: Int,
description: String,
onClick: () -> Unit,
enabled: Boolean = true,
extraDescription: @Composable (ColumnScope.() -> Unit)? = null,
) {
ListItem( ListItem(
modifier = Modifier modifier = Modifier
.clickable(onClick = onClick) .clickable(enabled, onClick = onClick)
.padding(start = 8.dp), .enabled(enabled),
headlineContent = { headlineContent = {
Text( Text(
stringResource(title), stringResource(title),
@@ -267,99 +257,17 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
) )
}, },
supportingContent = { supportingContent = {
Text( Column {
description, Text(
color = MaterialTheme.colorScheme.outline, description,
style = MaterialTheme.typography.bodyMedium color = MaterialTheme.colorScheme.outline,
) 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(
@param:StringRes val name: Int, @StringRes val name: Int,
@param:StringRes val description: Int, @StringRes val description: Int,
val image: ImageVector, val image: ImageVector,
val destination: Settings.Destination, val destination: Settings.Destination,
) )

View File

@@ -0,0 +1,154 @@
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

@@ -0,0 +1,139 @@
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,6 +1,5 @@
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
@@ -41,7 +40,6 @@ 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
@@ -69,9 +67,8 @@ 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(resources) { val icon = rememberDrawablePainter(drawable = remember {
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring) AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
}) })
@@ -79,7 +76,7 @@ fun AboutSettingsScreen(
viewModel.socials.partition(ReVancedSocial::preferred) viewModel.socials.partition(ReVancedSocial::preferred)
} }
val preferredSocialButtons = remember(resources, preferredSocials, viewModel.donate, viewModel.contact) { val preferredSocialButtons = remember(preferredSocials, viewModel.donate, viewModel.contact) {
preferredSocials.map { preferredSocials.map {
Triple( Triple(
getSocialIcon(it.name), getSocialIcon(it.name),
@@ -92,7 +89,7 @@ fun AboutSettingsScreen(
viewModel.donate?.let { viewModel.donate?.let {
Triple( Triple(
Icons.Outlined.FavoriteBorder, Icons.Outlined.FavoriteBorder,
resources.getString(R.string.donate), context.getString(R.string.donate),
third = { third = {
context.openUrl(it) context.openUrl(it)
} }
@@ -101,7 +98,7 @@ fun AboutSettingsScreen(
viewModel.contact?.let { viewModel.contact?.let {
Triple( Triple(
Icons.Outlined.MailOutline, Icons.Outlined.MailOutline,
resources.getString(R.string.contact), context.getString(R.string.contact),
third = { third = {
context.openUrl("mailto:$it") context.openUrl("mailto:$it")
} }
@@ -134,7 +131,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(resources.getString(R.string.no_network_toast)) context.toast(context.getString(R.string.no_network_toast))
return@nav return@nav
} }
@@ -156,7 +153,7 @@ fun AboutSettingsScreen(
LaunchedEffect(developerTaps) { LaunchedEffect(developerTaps) {
if (developerTaps == 0) return@LaunchedEffect if (developerTaps == 0) return@LaunchedEffect
if (showDeveloperSettings) { if (showDeveloperSettings) {
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_already_enabled)) snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
developerTaps = 0 developerTaps = 0
return@LaunchedEffect return@LaunchedEffect
} }
@@ -164,7 +161,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(
resources.getString( context.getString(
R.string.developer_options_taps, R.string.developer_options_taps,
remaining remaining
), ),
@@ -172,7 +169,7 @@ fun AboutSettingsScreen(
) )
} else if (remaining == 0) { } else if (remaining == 0) {
viewModel.showDeveloperSettings.update(true) viewModel.showDeveloperSettings.update(true)
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_enabled)) snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
} }
// Reset the counter // Reset the counter

View File

@@ -38,7 +38,6 @@ 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
@@ -65,10 +64,9 @@ fun AdvancedSettingsScreen(
viewModel: AdvancedSettingsViewModel = koinViewModel() viewModel: AdvancedSettingsViewModel = koinViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
val resources = LocalResources.current val memoryLimit = remember {
val memoryLimit = remember(resources) {
val activityManager = context.getSystemService<ActivityManager>()!! val activityManager = context.getSystemService<ActivityManager>()!!
resources.getString( context.getString(
R.string.device_memory_limit_format, R.string.device_memory_limit_format,
activityManager.memoryClass, activityManager.memoryClass,
activityManager.largeMemoryClass activityManager.largeMemoryClass
@@ -185,7 +183,7 @@ fun AdvancedSettingsScreen(
ClipData.newPlainText("Device Information", deviceContent) ClipData.newPlainText("Device Information", deviceContent)
) )
context.toast(resources.getString(R.string.toast_copied_to_clipboard)) context.toast(context.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,8 +8,6 @@ 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
@@ -21,7 +19,6 @@ 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
@@ -41,7 +38,6 @@ 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
@@ -52,7 +48,6 @@ 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(
@@ -60,17 +55,6 @@ 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(
@@ -90,24 +74,6 @@ 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 },
@@ -139,14 +105,6 @@ 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
)
} }
} }
} }
@@ -191,63 +149,3 @@ 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,7 +36,6 @@ 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
@@ -65,7 +64,6 @@ 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) }
@@ -110,7 +108,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(resources.getString(R.string.import_keystore_wrong_credentials)) if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
} }
} }
} }
@@ -168,7 +166,7 @@ fun ImportExportSettingsScreen(
GroupItem( GroupItem(
onClick = { onClick = {
if (!vm.canExport()) { if (!vm.canExport()) {
context.toast(resources.getString(R.string.export_keystore_unavailable)) context.toast(context.getString(R.string.export_keystore_unavailable))
return@GroupItem return@GroupItem
} }
exportKeystoreLauncher.launch("Manager.keystore") exportKeystoreLauncher.launch("Manager.keystore")

View File

@@ -7,18 +7,15 @@ 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.android.produceLibraries import com.mikepenz.aboutlibraries.ui.compose.libraryColors
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
@@ -36,23 +33,16 @@ 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(
libraryBackgroundColor = MaterialTheme.colorScheme.background, backgroundColor = MaterialTheme.colorScheme.background,
libraryContentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
versionChipColors = chipColors, badgeBackgroundColor = MaterialTheme.colorScheme.primary,
licenseChipColors = chipColors, badgeContentColor = MaterialTheme.colorScheme.onPrimary,
fundingChipColors = chipColors,
) )
) )
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues)) Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))

View File

@@ -12,7 +12,6 @@ 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
@@ -34,7 +33,6 @@ 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())
@@ -59,7 +57,7 @@ fun UpdatesSettingsScreen(
modifier = Modifier.clickable { modifier = Modifier.clickable {
coroutineScope.launch { coroutineScope.launch {
if (!vm.isConnected) { if (!vm.isConnected) {
context.toast(resources.getString(R.string.no_network_toast)) context.toast(context.getString(R.string.no_network_toast))
return@launch return@launch
} }
if (vm.checkForUpdates()) onUpdateClick() if (vm.checkForUpdates()) onUpdateClick()
@@ -72,7 +70,7 @@ fun UpdatesSettingsScreen(
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { modifier = Modifier.clickable {
if (!vm.isConnected) { if (!vm.isConnected) {
context.toast(resources.getString(R.string.no_network_toast)) context.toast(context.getString(R.string.no_network_toast))
return@clickable return@clickable
} }
onChangelogClick() onChangelogClick()

View File

@@ -3,9 +3,6 @@ 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
@@ -14,7 +11,6 @@ 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
@@ -31,7 +27,7 @@ class AppSelectorViewModel(
private val app: Application, private val app: Application,
private val pm: PM, private val pm: PM,
fs: Filesystem, fs: Filesystem,
private val patchBundleRepository: PatchBundleRepository, patchBundleRepository: PatchBundleRepository,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val inputFile = savedStateHandle.saveable(key = "inputFile") { private val inputFile = savedStateHandle.saveable(key = "inputFile") {
@@ -42,19 +38,19 @@ class AppSelectorViewModel(
} }
val appList = pm.appList val appList = pm.appList
private val storageSelectionChannel = Channel<SelectedApp.Local>() private val storageSelectionChannel = Channel<Pair<String, String>>()
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) {
@@ -66,11 +62,8 @@ class AppSelectorViewModel(
return@launch return@launch
} }
if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) { // TODO: Disallow if 0 patches are compatible
storageSelectionChannel.send(selectedApp) storageSelectionChannel.send(selectedApp)
} else {
nonSuggestedVersionDialogSubject = selectedApp
}
} }
private fun loadSelectedFile(uri: Uri) = private fun loadSelectedFile(uri: Uri) =
@@ -80,12 +73,7 @@ class AppSelectorViewModel(
Files.copy(stream, toPath()) Files.copy(stream, toPath())
pm.getPackageInfo(this)?.let { packageInfo -> pm.getPackageInfo(this)?.let { packageInfo ->
SelectedApp.Local( Pair(packageInfo.packageName, path)
packageName = packageInfo.packageName,
version = packageInfo.versionName!!,
file = this,
temporary = true
)
} }
} }
} }

View File

@@ -54,7 +54,6 @@ class BundleListViewModel : ViewModel(), KoinComponent {
patchBundleRepository.update( patchBundleRepository.update(
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(), *getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
showToast = true, showToast = true,
force = true
) )
} }
} }
@@ -66,7 +65,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, force = true) patchBundleRepository.update(src, showToast = true)
} }
enum class Event { enum class Event {

View File

@@ -1,26 +1,17 @@
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(
@param:StringRes val titleResId: Int, @StringRes val titleResId: Int,
@param:StringRes val descriptionResId: Int, @StringRes val descriptionResId: Int,
val onConfirm: () -> Unit, val onConfirm: () -> Unit,
val dialogOptionName: String? = null val dialogOptionName: String? = null
) { ) {

View File

@@ -12,11 +12,9 @@ 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
@@ -31,44 +29,14 @@ 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.SelectedApp import app.revanced.manager.ui.model.SelectedSource
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,9 +93,8 @@ class PatcherViewModel(
private val ackpineInstaller: PackageInstaller = get() private val ackpineInstaller: PackageInstaller = get()
private var installedApp: InstalledApp? = null private var installedApp: InstalledApp? = null
private val selectedApp = input.selectedApp val packageName = input.packageName
val packageName = selectedApp.packageName val version = input.version
val version = selectedApp.version
var installedPackageName by savedStateHandle.saveable( var installedPackageName by savedStateHandle.saveable(
key = "installedPackageName", key = "installedPackageName",
@@ -160,7 +159,7 @@ class PatcherViewModel(
} }
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList() generateSteps(app, input.selectedSource, input.selectedPatches).toMutableStateList()
} }
val progress by derivedStateOf { val progress by derivedStateOf {
@@ -178,7 +177,9 @@ class PatcherViewModel(
ParcelUuid( ParcelUuid(
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>( workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args( "patching", PatcherWorker.Args(
input.selectedApp, input.packageName,
input.version,
input.selectedSource,
outputFile.path, outputFile.path,
input.selectedPatches, input.selectedPatches,
input.options, input.options,
@@ -257,7 +258,7 @@ class PatcherViewModel(
super.onCleared() super.onCleared()
workManager.cancelWorkById(patcherWorkerId.uuid) workManager.cancelWorkById(patcherWorkerId.uuid)
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { if (input.selectedSource is SelectedSource.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)) {
@@ -381,7 +382,7 @@ class PatcherViewModel(
installedAppRepository.addOrUpdate( installedAppRepository.addOrUpdate(
installerPkgName, installerPkgName,
packageName, packageName,
input.selectedApp.version input.version
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! }, ?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
InstallType.DEFAULT, InstallType.DEFAULT,
input.selectedPatches input.selectedPatches
@@ -443,7 +444,7 @@ class PatcherViewModel(
} }
} }
val inputVersion = input.selectedApp.version val inputVersion = input.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")
@@ -535,10 +536,10 @@ class PatcherViewModel(
fun generateSteps( fun generateSteps(
context: Context, context: Context,
selectedApp: SelectedApp, selectedSource: SelectedSource,
selectedPatches: PatchSelection selectedPatches: PatchSelection
): List<Step> = buildList { ): List<Step> = buildList {
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search) if (selectedSource is SelectedSource.Plugin)
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.SelectedApplicationInfo import app.revanced.manager.ui.model.navigation.SelectedAppInfo
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: SelectedApplicationInfo.PatchesSelector.ViewModelParams) : class PatchesSelectorViewModel(input: SelectedAppInfo.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.app.packageName private val packageName = input.packageName
val appVersion = input.app.version val appVersion = input.version
var selectionWarningEnabled by mutableStateOf(true) var selectionWarningEnabled by mutableStateOf(true)
private set private set
@@ -62,7 +62,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
val allowIncompatiblePatches = val allowIncompatiblePatches =
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking() get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
val bundlesFlow = val bundlesFlow =
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version) get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.version)
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -88,7 +88,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
key = "selection", key = "selection",
stateSaver = selectionSaver, stateSaver = selectionSaver,
) { ) {
mutableStateOf(input.currentSelection?.toPersistentPatchSelection()) mutableStateOf(input.patchSelection?.toPersistentPatchSelection())
} }
private val patchOptions: PersistentOptions by savedStateHandle.saveable( private val patchOptions: PersistentOptions by savedStateHandle.saveable(

View File

@@ -1,130 +1,188 @@
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.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.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
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.ui.model.SelectedSource
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedVersion
import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.ui.model.navigation.SelectedAppInfo
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.simpleMessage import app.revanced.manager.util.patchCount
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.Job import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow
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.receiveAsFlow import kotlinx.coroutines.flow.flatMapLatest
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(
input: SelectedApplicationInfo.ViewModelParams private val input: SelectedAppInfo.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 rootInstaller: RootInstaller = get() private val downloadedAppRepository: DownloadedAppRepository = get()
private val pm: PM = get() private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
val prefs: PreferencesManager = get() private val prefs: PreferencesManager = get()
val plugins = pluginsRepository.loadedPluginsFlow val plugins = pluginsRepository.loadedPluginsFlow
val desiredVersion = input.app.version val packageName = input.packageName
val packageName = input.app.packageName val localPath = input.localPath
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
private var _selectedApp by savedStateHandle.saveable { // User selection
mutableStateOf(input.app) private var selectionFlow = MutableStateFlow(
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())
options = filteredOptions
if (persistConfiguration) {
selection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.resetSelectionForPackage(packageName)
optionsRepository.saveOptions(packageName, filteredOptions)
}
} }
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
private set
var selectedApp // All patches for package
get() = _selectedApp val bundles = bundleRepository.scopedBundleInfoFlow(packageName, null)
set(value) {
_selectedApp = value // Selection derived from selectionFlow
invalidateSelectedAppInfo() val patchSelection = combine(
selectionFlow,
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
} }
}
init { @OptIn(ExperimentalCoroutinesApi::class)
invalidateSelectedAppInfo() val scopedBundles = resolvedVersion.flatMapLatest { version ->
viewModelScope.launch(Dispatchers.Main) { bundleRepository.scopedBundleInfoFlow(packageName, version)
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) } }
val installedAppDeferred =
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
installedAppData = val incompatiblePatchCount = scopedBundles.map { bundles ->
packageInfo.await()?.let { bundles.sumOf { bundle ->
SelectedApp.Installed( bundle.incompatible.size
packageName, }
it.versionName!! }
) to installedAppDeferred.await()
// 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, selectedApp.version) bundleRepository.scopedBundleInfoFlow(packageName, null)
} }
var options: Options by savedStateHandle.saveable { var options: Options by savedStateHandle.saveable {
@@ -142,121 +200,42 @@ class SelectedAppInfoViewModel(
} }
private set private set
private var selectionState: SelectionState by savedStateHandle.saveable {
if (input.patches != null)
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
// Try to get the previous selection if customization is enabled. val errorFlow = combine(
viewModelScope.launch { plugins,
if (!prefs.disableSelectionWarning.get()) return@launch resolvedSource,
) { 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 {
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins source is SelectedSource.Plugin && pluginsList.isEmpty() -> Error.NoPlugins
else -> null else -> null
} }
} }
fun showSourceSelector() {
dismissSourceSelector()
showSourceSelector = true // var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
// private set
private var _selectedApp by savedStateHandle.saveable {
mutableStateOf(null)
} }
private fun cancelPluginAction() { var selectedAppInfo: PackageInfo? by mutableStateOf(null)
pluginAction?.second?.cancel() private set
pluginAction = null
}
fun dismissSourceSelector() { var selectedApp
cancelPluginAction() get() = _selectedApp
showSourceSelector = false set(value) {
} _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)
}
// TODO: Load from local file or downloaded app
private fun invalidateSelectedAppInfo() = viewModelScope.launch { private fun invalidateSelectedAppInfo() = viewModelScope.launch {
val info = when (val app = selectedApp) { selectedAppInfo = pm.getPackageInfo(packageName)
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
else -> null
}
selectedAppInfo = info
} }
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles) fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
@@ -272,37 +251,50 @@ 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(
selectedApp, input.packageName,
getPatches(bundles, allowIncompatible), resolvedVersion.first(),
resolvedSource.first(),
patchSelection.first(),
getOptionsFiltered(bundles) getOptionsFiltered(bundles)
) )
} }
fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) = init {
selectionState.patches(bundles, allowIncompatible) invalidateSelectedAppInfo()
fun getCustomPatches( input.localPath?.let { local ->
bundles: List<PatchBundleInfo.Scoped>, viewModelScope.launch {
allowIncompatible: Boolean val packageInfo = pm.getPackageInfo(File(local))
): PatchSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
_selectedVersion.value = SelectedVersion.Specific(
packageInfo?.versionName ?: return@launch
)
_selectedSource.value = SelectedSource.Local(local)
}
}
fun updateConfiguration( // Get the previous selection if customization is enabled.
selection: PatchSelection?, viewModelScope.launch {
options: Options if (prefs.disableSelectionWarning.get()) {
) = viewModelScope.launch { val previous = selectionRepository.getSelection(packageName)
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default if (previous.patchCount == 0) return@launch
selectionFlow.value = SelectionState.Customized(previous)
}
}
val filteredOptions = options.filtered(bundleInfoFlow.first()) // Get installed app info
this@SelectedAppInfoViewModel.options = filteredOptions viewModelScope.launch {
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val installedAppDeferred =
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
if (!persistConfiguration) return@launch // installedAppData =
viewModelScope.launch(Dispatchers.Default) { // packageInfo.await()?.let {
selection?.let { selectionRepository.updateSelection(packageName, it) } // SelectedApp.Installed(
?: selectionRepository.resetSelectionForPackage(packageName) // packageName,
// it.versionName!!
optionsRepository.saveOptions(packageName, filteredOptions) // ) to installedAppDeferred.await()
// }
} }
} }

View File

@@ -0,0 +1,136 @@
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(false) && !ignoreInternetCheck) { if (!networkInfo.isSafe() && !ignoreInternetCheck) {
showInternetCheckDialog = true showInternetCheckDialog = true
} else { } else {
state = State.DOWNLOADING state = State.DOWNLOADING
@@ -90,10 +90,8 @@ class UpdateViewModel(
http.download(location) { http.download(location) {
url(release.downloadUrl) url(release.downloadUrl)
onDownload { bytesSentTotal, contentLength -> onDownload { bytesSentTotal, contentLength ->
withContext(Dispatchers.Main) { downloadedSize = bytesSentTotal
downloadedSize = bytesSentTotal totalSize = contentLength
contentLength?.let { totalSize = it }
}
} }
} }
installUpdate() installUpdate()

View File

@@ -0,0 +1,85 @@
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

@@ -1,40 +0,0 @@
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,10 +15,12 @@ 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.produceState import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.setValue
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
@@ -41,6 +43,7 @@ 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
@@ -48,15 +51,16 @@ 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) {
@@ -168,7 +172,7 @@ fun LocalDateTime.relativeTime(context: Context): String {
else -> LocalDateTime.Format { else -> LocalDateTime.Format {
monthName(MonthNames.ENGLISH_ABBREVIATED) monthName(MonthNames.ENGLISH_ABBREVIATED)
char(' ') char(' ')
day() dayOfMonth()
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) { if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
chars(", ") chars(", ")
year() year()
@@ -195,12 +199,7 @@ val transparentListItemColors
.also { transparentListItemColorsCached = it } .also { transparentListItemColorsCached = it }
@Composable @Composable
fun <T> EventEffect( fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = Lifecycle.State.STARTED, block: suspend (T) -> Unit) {
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)
@@ -216,36 +215,40 @@ fun <T> EventEffect(
const val isScrollingUpSensitivity = 10 const val isScrollingUpSensitivity = 10
@Composable @Composable
fun LazyListState.isScrollingUp() = produceState(true, this) { fun LazyListState.isScrollingUp(): State<Boolean> {
var previousIndex = firstVisibleItemIndex return remember(this) {
var previousScrollOffset = firstVisibleItemScrollOffset var previousIndex by mutableIntStateOf(firstVisibleItemIndex)
var previousScrollOffset by mutableIntStateOf(firstVisibleItemScrollOffset)
snapshotFlow { derivedStateOf {
firstVisibleItemIndex to firstVisibleItemScrollOffset val indexChanged = previousIndex != firstVisibleItemIndex
}.collect { (index, scrollOffset) -> val offsetChanged =
val indexChanged = previousIndex != index kotlin.math.abs(previousScrollOffset - firstVisibleItemScrollOffset) > isScrollingUpSensitivity
val offsetChanged = abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity
value = when { if (indexChanged) {
indexChanged -> previousIndex > index previousIndex > firstVisibleItemIndex
offsetChanged -> previousScrollOffset > scrollOffset } else if (offsetChanged) {
else -> value previousScrollOffset > firstVisibleItemScrollOffset
} else {
true
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
} }
previousIndex = index
previousScrollOffset = scrollOffset
} }
} }
// TODO: support sensitivity
@Composable @Composable
fun ScrollState.isScrollingUp() = produceState(true, this) { fun ScrollState.isScrollingUp(): State<Boolean> {
var previousScrollOffset = this@isScrollingUp.value return remember(this) {
var previousScrollOffset by mutableIntStateOf(value)
snapshotFlow { this@isScrollingUp.value }.collect { scrollOffset -> derivedStateOf {
if (abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity) { (previousScrollOffset >= value).also {
value = previousScrollOffset >= scrollOffset previousScrollOffset = value
}
} }
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 test</string> <string name="patcher">Patcher</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,14 +61,15 @@ 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">Select patches</string> <string name="patch_selector_item">Patches</string>
<string name="patch_selector_item_description">%d patches selected</string> <string name="patch_selector_item_description">%d 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">Select APK source</string> <string name="apk_source_selector_item">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>
@@ -83,7 +84,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">Language, theme, dynamic color</string> <string name="general_description">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>
@@ -104,9 +105,6 @@ 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>
@@ -205,7 +203,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 using file picker</string> <string name="select_from_storage_description">Select an APK file from storage</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>
@@ -220,10 +218,6 @@ 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,6 +6,5 @@ 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
} }

View File

@@ -1,9 +0,0 @@
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.17.0" ktx = "1.16.0"
material3 = "1.4.0" material3 = "1.3.2"
ui-tooling = "1.10.0" ui-tooling = "1.8.1"
viewmodel-lifecycle = "2.10.0" viewmodel-lifecycle = "2.9.0"
splash-screen = "1.2.0" splash-screen = "1.0.1"
activity = "1.12.2" activity = "1.10.1"
appcompat = "1.7.1" appcompat = "1.7.0"
preferences-datastore = "1.2.0" preferences-datastore = "1.1.2"
work-runtime = "2.11.0" work-runtime = "2.10.1"
compose-bom = "2025.12.01" compose-bom = "2025.05.00"
navigation = "2.9.6" navigation = "2.8.6"
accompanist = "0.37.3" accompanist = "0.37.0"
placeholder = "1.0.12" placeholder = "1.1.2"
reorderable = "3.0.0" reorderable = "2.4.3"
serialization = "1.9.0" serialization = "1.8.0"
collection = "0.4.0" collection = "0.3.8"
datetime = "0.7.1" datetime = "0.6.1"
room-version = "2.8.4" room-version = "2.7.1"
revanced-patcher = "21.0.0" revanced-patcher = "21.0.0"
revanced-library = "3.0.2" revanced-library = "3.0.2"
koin = "4.1.1" koin = "3.5.3"
ktor = "3.3.3" ktor = "2.3.9"
markdown-renderer = "0.39.0" markdown-renderer = "0.30.0"
fading-edges = "1.0.4" fading-edges = "1.0.4"
kotlin = "2.3.0" kotlin = "2.1.10"
android-gradle-plugin = "8.13.2" android-gradle-plugin = "8.9.1"
dev-tools-gradle-plugin = "2.3.4" dev-tools-gradle-plugin = "2.1.10-1.0.29"
about-libraries = "13.2.1" about-libraries-gradle-plugin = "12.1.2"
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.4.0" hidden-api-stub = "4.3.3"
binary-compatibility-validator = "0.18.1" binary-compatibility-validator = "0.17.0"
semver-parser = "3.0.0" semver-parser = "3.0.0"
ackpine = "0.19.1" ackpine = "0.18.5"
[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 = "com.eygraber", name = "compose-placeholder-material3", version.ref = "placeholder" } placeholder-material3 = { group = "io.github.fornewid", name = "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,8 +91,7 @@ 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-core = { group = "com.mikepenz", name = "aboutlibraries-compose-core", version.ref = "about-libraries" } about-libraries = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" }
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" }
@@ -147,6 +146,5 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" } devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries" } about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
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" }